Thread.MemoryBarrier et lock difference pour une propriété simple

Pour le scénario suivant, existe-t-il une différence entre la sécurité des threads, les résultats et les performances entre l’utilisation de MemoryBarrier

 private SomeType field; public SomeType Property { get { Thread.MemoryBarrier(); SomeType result = field; Thread.MemoryBarrier(); return result; } set { Thread.MemoryBarrier(); field = value; Thread.MemoryBarrier(); } } 

et instruction de lock ( Monitor.Enter et Monitor.Exit )

 private SomeType field; private readonly object syncLock = new object(); public SomeType Property { get { lock (syncLock) { return field; } } set { lock (syncLock) { field = value; } } } 

Parce que l’affectation de référence est atomique, je pense que dans ce scénario, nous avons besoin de tout mécanisme de locking.

Performances MemeoryBarrier est environ deux fois plus rapide que l’implémentation du locking pour Release. Voici mes résultats de test:

 Lock Normaly: 5397 ms Passed as interface: 5431 ms Double Barrier Normaly: 2786 ms Passed as interface: 3754 ms volatile Normaly: 250 ms Passed as interface: 668 ms Volatile Read/Write Normaly: 253 ms Passed as interface: 697 ms ReaderWriterLockSlim Normaly: 9272 ms Passed as interface: 10040 ms Single Barrier: freshness of Property Normaly: 1491 ms Passed as interface: 2510 ms Single Barrier: other not reodering Normaly: 1477 ms Passed as interface: 2275 ms 

Voici comment je l’ai testé dans LINQPad (avec l’optimisation définie dans Préférences):

 void Main() { "Lock".Dump(); ssortingng temp; var a = new A(); var watch = Stopwatch.StartNew(); for (int i = 0; i < 100000000; ++i) { temp = a.Property; a.Property = temp; } Console.WriteLine("Normaly: " + watch.ElapsedMilliseconds + " ms"); Test(a); "Double Barrier".Dump(); var b = new B(); watch.Restart(); for (int i = 0; i < 100000000; ++i) { temp = b.Property; b.Property = temp; } Console.WriteLine("Normaly: " + watch.ElapsedMilliseconds + " ms"); Test(b); "volatile".Dump(); var c = new C(); watch.Restart(); for (int i = 0; i < 100000000; ++i) { temp = c.Property; c.Property = temp; } Console.WriteLine("Normaly: " + watch.ElapsedMilliseconds + " ms"); Test(c); "Volatile Read/Write".Dump(); var d = new D(); watch.Restart(); for (int i = 0; i < 100000000; ++i) { temp = d.Property; d.Property = temp; } Console.WriteLine("Normaly: " + watch.ElapsedMilliseconds + " ms"); Test(d); "ReaderWriterLockSlim".Dump(); var e = new E(); watch.Restart(); for (int i = 0; i < 100000000; ++i) { temp = e.Property; e.Property = temp; } Console.WriteLine("Normaly: " + watch.ElapsedMilliseconds + " ms"); Test(e); "Single Barrier: freshness of Property".Dump(); var f = new F(); watch.Restart(); for (int i = 0; i < 100000000; ++i) { temp = f.Property; f.Property = temp; } Console.WriteLine("Normaly: " + watch.ElapsedMilliseconds + " ms"); Test(f); "Single Barrier: other not reodering".Dump(); var g = new G(); watch.Restart(); for (int i = 0; i < 100000000; ++i) { temp = g.Property; g.Property = temp; } Console.WriteLine("Normaly: " + watch.ElapsedMilliseconds + " ms"); Test(g); } void Test(I a) { string temp; var watch = Stopwatch.StartNew(); for (int i = 0; i < 100000000; ++i) { temp = a.Property; a.Property = temp; } Console.WriteLine("Passed as interface: " + watch.ElapsedMilliseconds + " ms\n"); } interface I { string Property { get; set; } } class A : I { private string field; private readonly object syncLock = new object(); public string Property { get { lock (syncLock) { return field; } } set { lock (syncLock) { field = value; } } } } class B : I { private string field; public string Property { get { Thread.MemoryBarrier(); string result = field; Thread.MemoryBarrier(); return result; } set { Thread.MemoryBarrier(); field = value; Thread.MemoryBarrier(); } } } class C : I { private volatile string field; public string Property { get { return field; } set { field = value; } } } class D : I { private string field; public string Property { get { return Volatile.Read(ref field); } set { Volatile.Write(ref field, value); } } } class E : I { private string field; private ReaderWriterLockSlim locker = new ReaderWriterLockSlim(); public string Property { get { locker.EnterReadLock(); string result = field; locker.ExitReadLock(); return result; } set { locker.EnterReadLock(); field = value; locker.ExitReadLock(); } } } class F : I { private string field; public string Property { get { Thread.MemoryBarrier(); return field; } set { field = value; Thread.MemoryBarrier(); } } } class G : I { private string field; public string Property { get { string result = field; Thread.MemoryBarrier(); return result; } set { Thread.MemoryBarrier(); field = value; } } } 

y a-t-il une différence en ce qui concerne la sécurité du filetage?

Les deux veillent à ce que des barrières appropriées soient établies autour de la lecture et de l’écriture.

résultat?

Dans les deux cas, deux threads peuvent courir pour écrire une valeur. Cependant, les lectures et les écritures ne peuvent pas avancer ou reculer dans le temps, que ce soit devant le verrou ou les clôtures complètes.

performance?

Vous avez écrit le code dans les deux sens. Maintenant, lancez-le . Si vous voulez savoir lequel est le plus rapide, lancez-le et découvrez! Si vous avez deux chevaux et que vous voulez savoir lequel est le plus rapide, courez-les. Ne demandez pas à des inconnus sur Internet quel cheval, selon eux, est le plus rapide.

Cela dit, une meilleure technique consiste à définir un objective de performance , à écrire le code pour qu’il soit clairement correct, puis à tester pour voir si vous avez atteint votre objective. Si vous le faites, ne perdez pas votre temps précieux à essayer d’optimiser du code qui est déjà assez rapide; dépensez-le à optimiser quelque chose qui n’est pas assez rapide.

Une question que vous n’avez pas posée:

Qu’est-ce que tu ferais?

Je n’écrirais pas un programme multithread, c’est ce que je ferais. J’utiliserais les processus comme unité de concurrence si je devais le faire.

Si je devais écrire un programme multithread, j’utiliserais l’outil de plus haut niveau disponible. J’utilisais la bibliothèque parallèle de tâches, j’utilisais async-wait, j’utilisais Lazy , etc. J’éviterais la mémoire partagée; Je traiterais les threads comme des processus légers renvoyant une valeur de manière asynchrone.

Si je devais écrire un programme multithread à mémoire partagée, je verrouillerais tout, tout le temps . De nos jours, nous écrivons régulièrement des programmes qui extraient un milliard d’octets de vidéo sur une liaison par satellite et l’envoient à un téléphone. Vingt nanosecondes passées à prendre un verrou ne vont pas vous tuer.

Je ne suis pas assez intelligent pour essayer d’écrire du code low-lock, alors je ne le ferais pas du tout. Si je devais le faire , j’utiliserais ce code à faible locking pour créer une abstraction de niveau supérieur et utiliser cette abstraction. Heureusement, je n’ai pas à le faire car quelqu’un a déjà construit les abstractions dont j’ai besoin.

Tant que la variable en question est l’un des ensembles limités de variables pouvant être extraites / définies de manière atomique (c’est-à-dire des types de référence), alors oui, les deux solutions appliquent les mêmes contraintes liées aux threads.

Cela dit, honnêtement, je m’attendrais à ce que la solution MemoryBarrier se comporte moins bien qu’un verrou. L’access à un bloc de lock non contesté est très rapide. Il a été optimisé spécifiquement pour ce cas. Par ailleurs, l’introduction d’une barrière de mémoire, qui affecte non seulement l’access à cette variable , comme c’est le cas pour un lock , mais également l’ ensemble de la mémoire , pourrait avoir des conséquences négatives importantes sur les performances dans d’autres aspects de l’application. Vous devez bien sûr effectuer des tests pour être sûr (de vos applications réelles, car tester ces deux applications isolément ne va pas révéler le fait que la barrière de mémoire oblige toute la mémoire de l’application à être synchronisée. , pas seulement celle-ci variable).

Il n’y a pas de différence en ce qui concerne la sécurité du fil. Cependant, je préférerais:

 private SomeType field public SomeType Property { get { return Volatile.Read(ref field); } set { Volatile.Write(ref field, value); } } 

Ou,

 private volatile SomeType field public SomeType Property { get { return field; } set { field = value; } }