Tout au long des ressources que j'ai lues sur le multithreading, le mutex est plus souvent utilisé et discuté que le sémaphore. Ma question est quand utilisez-vous un sémaphore sur un mutex? Je ne vois pas de sémaphores dans le fil Boost. Cela signifie-t-il que les sémaphores n'utilisent plus beaucoup ces jours-ci?
Pour autant que je sache, les sémaphores permettent à une ressource d'être partagée par plusieurs threads. Cela n'est possible que si ces threads lisent uniquement la ressource mais pas l'écriture. Est-ce correct?
Boost.Thread a des mutex et des variables de condition. En termes de fonctionnalité, les sémaphores sont donc redondants [*], bien que je ne sache pas si c'est pourquoi ils sont omis.
Les sémaphores sont une primitive plus basique, plus simple et peut-être implémentée pour être plus rapide, mais n'ont pas d'évitement d'inversion de priorité. Ils sont sans doute plus difficiles à utiliser que les variables de condition, car ils nécessitent le code client pour garantir que le nombre de publications "correspond" au nombre d'attentes d'une manière appropriée. Avec les variables de condition, il est facile de tolérer les messages parasites, car personne en fait ne rien sans vérifier la condition.
Les ressources de lecture et d'écriture sont un OMI redingue, cela n'a rien à voir avec la différence entre un mutex et un sémaphore. Si vous utilisez un sémaphore de comptage, vous pourriez avoir une situation où plusieurs threads accèdent simultanément à la même ressource, auquel cas il faudrait vraisemblablement que ce soit un accès en lecture seule. Dans ce cas, vous pourrez peut-être utiliser shared_mutex
de Boost.Thread à la place. Mais les sémaphores ne sont pas "pour" protéger les ressources de la même manière que les mutex, ils sont "pour" envoyer un signal d'un thread à un autre. Il est possible de tiliser eux pour contrôler l'accès à une ressource.
Cela ne signifie pas que toutes les utilisations des sémaphores doivent se rapporter à des ressources en lecture seule. Par exemple, vous pouvez utiliser un sémaphore binaire pour protéger une ressource en lecture/écriture. Cela pourrait ne pas être une bonne idée, car un mutex vous donne souvent un meilleur comportement de planification.
[*] Voici comment vous implémentez un sémaphore de comptage à l'aide d'un mutex et d'une variable de condition. Pour implémenter un sémaphore partagé, bien sûr, vous avez besoin d'un mutex/condvar partagé:
struct sem {
mutex m;
condvar cv;
unsigned int count;
};
sem_init(s, value)
mutex_init(s.m);
condvar_init(s.cv);
count = value;
sem_wait(s)
mutex_lock(s.m);
while (s.count <= 0) {
condvar_wait(s.cv, s.m);
}
--s.count;
mutex_unlock(s.m);
sem_post(s)
mutex_lock(s.m);
++s.count;
condvar_broadcast(s.cv)
mutex_unlock(s.m);
Par conséquent, tout ce que vous pouvez faire avec des sémaphores, vous pouvez le faire avec des mutex et des variables de condition. Pas nécessairement en implémentant un sémaphore.
Le cas d'utilisation typique d'un mutex (n'autorisant qu'un seul thread à accéder à une ressource à tout moment) est beaucoup plus courant que les utilisations typiques d'un sémaphore. Mais un sémaphore est en fait le concept le plus général: un mutex est (presque) un cas particulier de sémaphore.
Les applications typiques seraient: Vous ne voulez pas créer plus de (par exemple) 5 connexions à la base de données. Peu importe le nombre de threads de travail, ils doivent partager ces 5 connexions. Ou, si vous exécutez sur une machine N-core, vous voudrez peut-être vous assurer que certaines tâches gourmandes en CPU/mémoire ne s'exécutent pas dans plus de N threads en même temps (car cela ne ferait que réduire le débit en raison des changements de contexte et effets de débordement du cache). Vous pouvez même vouloir limiter le nombre de tâches parallèles gourmandes en CPU/mémoire à N-1, afin que le reste du système ne meure pas de faim. Ou imaginez qu'une certaine tâche nécessite beaucoup de mémoire, donc exécuter plus de N instances de cette tâche en même temps conduirait à la pagination. Vous pouvez utiliser un sémaphore ici, pour vous assurer que pas plus de N instances de cette tâche particulière ne s'exécutent en même temps.
EDIT/PS: D'après votre question "Ceci n'est possible que si ces threads ne font que lire la ressource mais pas écrire. Est-ce correct?" et votre commentaire, il me semble que vous envisagez une ressource comme une variable ou un flux, qui peut être lu ou écrit et qui ne peut être écrit que par un thread à la fois. Non. C'est trompeur dans ce contexte.
Pensez à des ressources comme "l'eau". Vous pouvez utiliser de l'eau pour laver votre vaisselle. Je peux utiliser de l'eau pour laver ma vaisselle en même temps. Nous n'avons besoin d'aucune sorte de synchronisation pour cela, car il y a assez d'eau pour nous deux. Nous n'utilisons pas nécessairement la même eau. (Et vous ne pouvez pas "lire" ou "écrire" de l'eau.) Mais la quantité totale d'eau est finie. Il n'est donc pas possible pour tout nombre de soirées de laver leur vaisselle en même temps. Ce type de synchronisation se fait avec un sémaphore. Seulement généralement pas avec de l'eau mais avec d'autres ressources finies comme la mémoire, l'espace disque, le débit IO ou les cœurs CPU).
L'essence de la différence entre un mutex et un sémaphore a à voir avec le concept de propriété. Lorsqu'un mutex est pris, nous pensons que ce thread possède le mutex et que le même thread doit ensuite libérer le mutex pour libérer la ressource.
Pour un sémaphore, pensez à prendre le sémaphore comme consommant la ressource, mais pas à en prendre possession. Ceci est généralement appelé le sémaphore étant "vide" plutôt que détenu par un thread. La caractéristique du sémaphore est alors qu'un thread différent peut "remplir" le sémaphore à l'état "complet".
Par conséquent, les mutex sont généralement utilisés pour la protection simultanée des ressources (par exemple: MUTual EXlusion) tandis que les sémaphores sont utilisés pour la signalisation entre les threads (comme les drapeaux de sémaphore signalant entre les navires). Un mutex en soi ne peut pas vraiment être utilisé pour la signalisation, mais les sémaphores le peuvent. Donc, sélectionner l'un sur l'autre dépend de ce que vous essayez de faire.
Voir un autre ne de mes réponses ici pour plus de discussion sur un sujet connexe couvrant la distinction entre mutex récursifs et non récursifs.
Pour contrôler l'accès à un nombre limité de ressources partagées par plusieurs threads (inter ou intra-processus).
Dans notre application, nous avions une ressource très lourde et que nous ne voulions pas allouer un pour chacun des M threads de travail. Étant donné qu'un thread de travail n'avait besoin de la ressource que pour une petite partie de son travail, nous utilisions rarement plus de quelques ressources simultanément.
Nous avons donc alloué N de ces ressources et les avons placées derrière un sémaphore initialisé à N. Lorsque plus de N threads essayaient d'utiliser la ressource, ils bloquaient simplement jusqu'à ce que l'un soit disponible.
J'ai l'impression qu'il n'y a pas de moyen simple de [~ # ~] vraiment [~ # ~] répondre à votre question sans négliger certaines informations importantes sur les sémaphores. Les gens ont écrit de nombreux livres sur les sémaphores , donc toute réponse à un ou deux paragraphes est un mauvais service. Un livre populaire est Le petit livre des sémaphores ... pour ceux qui n'aiment pas les gros livres :).
Voici un bon article long qui va dans BEAUCOUP de détails sur la façon dont les sémaphores sont utilisés et comment ils sont destinés à être utilisés.
Mettre à jour:
Dan a signalé quelques erreurs dans mes exemples, je laisse les références qui offrent BEAUCOUP de meilleures explications que les miennes :).
Voici les références montrant les bonnes façons d'utiliser un sémaphore:
1. Article IBM
2. Conférence de classe de l'Université de Chicago
. L'article Netrino que j'ai publié à l'origine.
4. Le papier "vendre des billets" + code.
Tiré de cet article :
Un mutex permet la synchronisation inter-processus. Si vous instanciez un mutex avec un nom (comme dans le code ci-dessus), le mutex devient à l'échelle du système. Ceci est vraiment utile si vous partagez la même bibliothèque entre de nombreuses applications différentes et devez bloquer l'accès à une section critique de code qui accède à des ressources qui ne peuvent pas être partagées.
Enfin, la classe Semaphore. Supposons que vous ayez une méthode qui consomme beaucoup de CPU et utilise également les ressources dont vous avez besoin pour contrôler l'accès (à l'aide de Mutex :)). Vous avez également déterminé qu'un maximum de cinq appels à la méthode est à peu près tout ce que votre machine peut suspendre sans qu'elle ne réponde. Votre meilleure solution ici est d'utiliser la classe Semaphore qui vous permet de limiter l'accès d'un certain nombre de threads à une ressource.
Autant que je sache, les sémaphores sont de nos jours un terme fortement lié à l'IPC. Cela signifie toujours une variable protégée que de nombreux processus peuvent modifier, mais parmi les processus et cette fonctionnalité est prise en charge par le système d'exploitation.
Habituellement, nous n'avons pas besoin d'une variable et un simple mutex couvre toutes nos exigences. Si nous avons encore besoin d'une variable, probablement, nous la codons nous-mêmes - "variable + mutex" pour obtenir plus de contrôle.
Résumé: nous n'utilisons pas de sémaphores dans le multithreading car nous utilisons généralement mutex pour la simplicité et le contrôle, et nous utilisons des sémaphores pour IPC car il est pris en charge par le système d'exploitation et est un nom officiel pour le mécanisme de synchronisation des processus.
Les sémaphores ont été conçus à l'origine pour la synchronisation entre les processus. Windows utilise WaitForMultipleObjects qui est comme un sémaphore. Dans le monde Linux, l'implémentation initiale de pthread ne permettait pas de partager un mutex entre les processus. Maintenant ils le font. Le concept de rupture de l'incrément atomique (incrément interverrouillé dans Windows) avec un mutex léger est la mise en œuvre la plus pratique de nos jours après que les threads sont devenus l'unité de planification pour le processeur. Si l'incrément et le verrou étaient ensemble (sémaphore), le temps pour acquérir/libérer des verrous sera trop long et nous ne pouvons pas diviser ces 2 fonctions d'unité comme nous le faisons aujourd'hui pour des performances et de meilleures constructions de synchronisation.