J'ai un objet Queue dont je dois m'assurer qu'il est thread-safe. Serait-il préférable d'utiliser un objet verrou comme celui-ci:
lock(myLockObject)
{
//do stuff with the queue
}
Ou est-il recommandé d'utiliser Queue.Synchronized comme ceci:
Queue.Synchronized(myQueue).whatever_i_want_to_do();
En lisant les documents MSDN, il est dit que je devrais utiliser Queue.Synchronized pour le rendre thread-safe, mais il donne ensuite un exemple en utilisant un objet de verrouillage. De l'article MSDN:
Pour garantir la sécurité des threads de la file d'attente, toutes les opérations doivent être effectuées uniquement via ce wrapper.
L'énumération via une collection n'est pas intrinsèquement une procédure thread-safe. Même lorsqu'une collection est synchronisée, d'autres threads peuvent toujours modifier la collection, ce qui oblige l'énumérateur à lever une exception. Pour garantir la sécurité des threads pendant l'énumération, vous pouvez soit verrouiller la collection pendant toute l'énumération, soit intercepter les exceptions résultant des modifications apportées par d'autres threads.
Si appeler Synchronized () ne garantit pas la sécurité des threads, à quoi ça sert? Est-ce que j'ai râté quelque chose?
Personnellement, je préfère toujours le verrouillage. Cela signifie que vous décidez de la granularité. Si vous vous appuyez uniquement sur l'encapsuleur synchronisé, chaque opération individuelle est synchronisée, mais si vous avez besoin de faire plus d'une chose (par exemple, itérer sur toute la collection), vous devez de toute façon verrouiller. Dans un souci de simplicité, je préfère n'avoir qu'une chose à retenir: verrouiller correctement!
EDIT: Comme indiqué dans les commentaires, si vous pouvez utiliser des abstractions de niveau supérieur, c'est super. Et si vous faites utilisez le verrouillage, soyez prudent avec cela - documentez ce que vous attendez d'être verrouillé où, et acquérez/libérez les verrous pour une période aussi courte que possible (plus pour l'exactitude que pour les performances). Évitez d'appeler en code inconnu tout en maintenant un verrou, évitez les verrous imbriqués, etc.
Dans .NET 4, il y a beaucoup plus de support pour les abstractions de niveau supérieur (y compris le code sans verrouillage). Quoi qu'il en soit, je ne recommanderais toujours pas d'utiliser les wrappers synchronisés.
Il y a un problème majeur avec les méthodes Synchronized
dans l'ancienne bibliothèque de collection, car elles se synchronisent à un niveau de granularité trop faible (par méthode plutôt que par unité de travail).
Il existe une condition de concurrence classique avec une file d'attente synchronisée, illustrée ci-dessous où vous vérifiez le Count
pour voir s'il est sûr de retirer la file d'attente, mais la méthode Dequeue
lève une exception indiquant que la file d'attente est vide. Cela se produit car chaque opération individuelle est thread-safe, mais la valeur de Count
peut varier entre lorsque vous l'interrogez et lorsque vous utilisez la valeur.
object item;
if (queue.Count > 0)
{
// at this point another thread dequeues the last item, and then
// the next line will throw an InvalidOperationException...
item = queue.Dequeue();
}
Vous pouvez écrire ceci en toute sécurité en utilisant un verrou manuel autour de l'ensemble de l'unité d'oeuvre (c'est-à-dire vérifier le nombre et retirer la file d'attente de l'article) comme suit:
object item;
lock (queue)
{
if (queue.Count > 0)
{
item = queue.Dequeue();
}
}
Donc, comme vous ne pouvez pas retirer quoi que ce soit d'une file d'attente synchronisée en toute sécurité, je ne m'en soucierais pas et utiliserais simplement le verrouillage manuel.
.NET 4.0 devrait avoir tout un tas de collections thread-safe correctement implémentées, mais c'est encore près d'un an malheureusement.
Il existe souvent une tension entre les demandes de "collections thread-safe" et la nécessité d'effectuer plusieurs opérations sur la collection de manière atomique.
Synchronized () vous donne donc une collection qui ne se cassera pas si plusieurs threads y ajoutent des éléments simultanément, mais cela ne vous donne pas comme par magie une collection qui sait que pendant une énumération, personne d'autre ne doit la toucher.
En plus de l'énumération, les opérations courantes telles que "cet élément est-il déjà dans la file d'attente? Non, je vais l'ajouter" nécessitent également une synchronisation qui est plus large que la seule file d'attente.
De cette façon, nous n'avons pas besoin de verrouiller la file d'attente juste pour découvrir qu'elle était vide.
object item;
if (queue.Count > 0)
{
lock (queue)
{
if (queue.Count > 0)
{
item = queue.Dequeue();
}
}
}
Il me semble clair que l'utilisation d'un verrou (...) {...} est la bonne réponse.
Pour garantir la sécurité des threads de la file d'attente, toutes les opérations doivent être effectuées uniquement via ce wrapper.
Si d'autres threads accèdent à la file d'attente sans utiliser .Synchronized (), alors vous serez dans un ruisseau - à moins que tout votre accès à la file d'attente ne soit verrouillé.