Comment obtenir un comportement sans blocage mais bloquant?

Je suis en train de mettre en place une queue unique pour un seul producteur et sans locking pour une application réseau intensive. J’ai un tas de threads de travail qui travaillent dans leurs propres files d’attente, qu’ils mettent ensuite en queue et traitent.

La suppression des verrous de ces files d’attente a considérablement amélioré les performances sous une charge élevée, mais ils ne bloquent plus lorsque les files d’attente sont vides , ce qui entraîne une montée en flèche de l’utilisation du processeur.

Comment puis-je bloquer efficacement un thread jusqu’à ce qu’il parvienne à extraire quelque chose ou soit tué / interrompu?

Si vous êtes sous Linux, utilisez un Futex . Il fournit les performances d’une implémentation sans locking en utilisant des opérations atomiques plutôt que des appels du kernel comme un mutex, mais si vous deviez mettre le processus en veille à cause d’une condition non vraie (c’est-à-dire un conflit de locking) puis faites les appels appropriés du kernel pour mettre le processus en veille et le réactiver lors d’un événement ultérieur. C’est comme un sémaphore très rapide.

Sous Linux, futex peut être utilisé pour bloquer un thread. Mais sachez que Futexes Are Tricky !

UPDATE: les variables de condition sont beaucoup plus sûres à utiliser que les futex et sont plus portables. Cependant, une variable de condition est utilisée en combinaison avec un mutex, si bien que le résultat ne sera plus ssortingctement bloqué. Cependant, si votre objective principal est la performance (et non la garantie de la progression globale) et que la partie verrouillée (c.-à-d. Une condition pour vérifier après le réveil du thread) est faible, vous obtiendrez des résultats satisfaisants sans avoir à entrer dans subtilités de l’intégration de futex dans l’algorithme.

Si vous êtes sous Windows, vous ne pourrez pas utiliser les futex, mais Windows Vista a un mécanisme similaire appelé événements clés . Malheureusement, cela ne fait pas partie de l’API publiée (il s’agit d’une API NTDLL native), mais vous pouvez l’utiliser aussi longtemps que vous acceptez l’avertissement que cela pourrait changer dans les futures versions de Windows (et vous n’avez pas besoin de l’exécuter). kernelx pré-Vista). Assurez-vous de lire l’article que j’ai lié ci-dessus. Voici un croquis non testé de la façon dont cela pourrait fonctionner:

 /* Interlocked SList queue using keyed event signaling */ struct queue { SLIST_HEADER slist; // Note: Multiple queues can (and should) share a keyed event handle HANDLE keyed_event; // Initial value: 0 // Prior to blocking, the queue_pop function increments this to 1, then // rechecks the queue. If it finds an item, it attempts to compxchg back to // 0; if this fails, then it's racing with a push, and has to block LONG block_flag; }; void init_queue(queue *qPtr) { NtCreateKeyedEvent(&qPtr->keyed_event, -1, NULL, 0); InitializeSListHead(&qPtr->slist); qPtr->blocking = 0; } void queue_push(queue *qPtr, SLIST_ENTRY *entry) { InterlockedPushEntrySList(&qPtr->slist, entry); // Transition block flag 1 -> 0. If this succeeds (block flag was 1), we // have committed to a keyed-event handshake LONG oldv = InterlockedCompareExchange(&qPtr->block_flag, 0, 1); if (oldv) { NtReleaseKeyedEvent(qPtr->keyed_event, (PVOID)qPtr, FALSE, NULL); } } SLIST_ENTRY *queue_pop(queue *qPtr) { SLIST_ENTRY *entry = InterlockedPopEntrySList(&qPtr->slist); if (entry) return entry; // fast path // Transition block flag 0 -> 1. We must recheck the queue after this point // in case we race with queue_push; however since ReleaseKeyedEvent // blocks until it is matched up with a wait, we must perform the wait if // queue_push sees us LONG oldv = InterlockedCompareExchange(&qPtr->block_flag, 1, 0); assert(oldv == 0); entry = InterlockedPopEntrySList(&qPtr->slist); if (entry) { // Try to abort oldv = InterlockedCompareExchange(&qPtr->block_flag, 0, 1); if (oldv == 1) return entry; // nobody saw us, we can just exit with the value } // Either we don't have an entry, or we are forced to wait because // queue_push saw our block flag. So do the wait NtWaitForKeyedEvent(qPtr->keyed_event, (PVOID)qPtr, FALSE, NULL); // block_flag has been reset by queue_push if (!entry) entry = InterlockedPopEntrySList(&qPtr->slist); assert(entry); return entry; } 

Vous pouvez également utiliser un protocole similaire utilisant les verrous Slim Read Write et les variables de condition , avec un raccourci rapide. Il s’agit d’encapsuleurs sur les événements saisis au clavier, de sorte qu’ils peuvent entraîner une surcharge par rapport à l’utilisation d’événements saisis directement.

Avez-vous essayé l’attente conditionnelle? Lorsque la queue devient vide, attendez simplement un nouveau travail. Le thread qui place les tâches dans la queue doit déclencher le signal. De cette façon, vous utilisez uniquement des verrous lorsque la queue est vide.

https://computing.llnl.gov/tutorials/pthreads/#ConditionVariables

Vous pouvez faire en sorte qu’un thread s’endorme en utilisant la fonction sigwait (). Vous pouvez réveiller le thread avec pthread_kill. C’est beaucoup plus rapide que les variables de condition.

Vous pouvez append des dortoirs en attendant. Choisissez simplement la plus grande attente que vous souhaitez avoir, puis faites quelque chose comme ça (pseudocode car je ne me souviens plus de la syntaxe pthread):

 WAIT_TIME = 100; // Set this to whatever you're happy with while(loop_condition) { thing = get_from_queue() if(thing == null) { sleep(WAIT_TIME); } else { handle(thing); } } 

Même quelque chose de court comme un sumil de 100 ms devrait réduire considérablement l’utilisation du processeur. Je ne suis pas sûr à quel point le changement de contexte le rendra pire que occupé à attendre si.