Verrouillage prioritaire dans .NET

Chaque programmeur qui utilise plus d'un thread dans son programme a rencontré des primitives de synchronisation. Dans le contexte .NET, il y en a beaucoup, je ne les énumérerai pas, MSDN l' a déjà fait pour moi.

J'ai dû utiliser bon nombre de ces primitives, et elles ont parfaitement aidé à faire face aux tâches. Mais dans cet article, je veux parler du verrou habituel dans l'application de bureau et de la façon dont la nouvelle primitive (du moins pour moi) est apparue, qui peut être appelée PriorityLock.

Le problème


Lors du développement d'une application multithread hautement chargée, un gestionnaire apparaît quelque part qui traite d'innombrables threads. C'était donc avec moi. Et ce gestionnaire a travaillé, traité des tonnes de demandes provenant de plusieurs centaines de threads. Et tout allait bien pour lui, mais à l'intérieur de la serrure habituelle fonctionnait.

Et puis un jour, un utilisateur (par exemple, je) clique sur un bouton dans l'interface de l'application, le flux vole vers le gestionnaire (pas le flux d'interface, bien sûr) et s'attend à voir une réception super conviviale, mais à la place, il est rencontré par tante Klava de la réception la plus dense de la clinique la plus dense avec les mots «je m'en fiche complètement qui vous a dirigé. J'en ai 950 de plus comme toi. Allez les chercher. Je me fiche de savoir comment vous le comprenez. " Voici comment fonctionne le verrouillage dans .NET. Et tout semble aller bien, tout sera exécuté correctement, mais l'utilisateur n'avait clairement pas prévu d'attendre quelques secondes une réponse à son action.

C'est là que se termine l'histoire déchirante et que commence la solution au problème technique.

Solution


Après avoir étudié les primitives standard, je n'ai pas trouvé d'option appropriée. Par conséquent, j'ai décidé d'écrire mon cadenas, qui aurait une entrée standard et de haute priorité. Au fait, après avoir écrit, j’ai aussi étudié le nuget, je n’ai rien trouvé de tel là-bas, même si j’ai mal cherché.

Pour écrire une telle primitive (ou plus une primitive), j'avais besoin d'opérations SemaphoreSlim, SpinWait et Interlocked. Dans le spoiler, j'ai cité la première version de mon PriorityLock (uniquement du code synchrone, mais c'est le plus important), et des explications.

Texte masqué
En termes de synchronisation, il n'y a pas de découvertes, alors que quelqu'un est dans la serrure, les autres ne peuvent pas entrer. Si une priorité élevée est venue, elle est poussée par tous ceux qui attendent une priorité faible.

La classe LockMgr, il est proposé de travailler avec elle dans votre code. C'est lui qui est l'objet même de la synchronisation. Crée des objets Locker et HighLocker, contient des sémaphores, SpinWait, des compteurs souhaitant entrer dans la section critique, le thread actuel et le compteur de récursivité.

public class LockMgr { internal int HighCount; internal int LowCount; internal Thread CurThread; internal int RecursionCount; internal readonly SemaphoreSlim Low = new SemaphoreSlim(1); internal readonly SemaphoreSlim High = new SemaphoreSlim(1); internal SpinWait LowSpin = new SpinWait(); internal SpinWait HighSpin = new SpinWait(); public Locker HighLock() { return new HighLocker(this); } public Locker Lock(bool high = false) { return new Locker(this, high); } } 

La classe Locker implémente l'interface IDisposable. Pour implémenter la récursivité lors de la capture d'un verrou, nous nous souvenons de l'ID du flux, puis le vérifions. De plus, selon la priorité, dans le cas d'une priorité élevée, nous disons immédiatement que nous sommes arrivés (augmenter le compteur HighCount), obtenir le sémaphore élevé et attendre (si nécessaire) pour libérer le verrou de la priorité faible, après quoi nous sommes prêts à obtenir le verrou. Dans le cas d'une faible priorité, reçoit le sémaphore bas, puis nous attendons l'achèvement de tous les flux de haute priorité et, en prenant le sémaphore haut pendant un certain temps, augmentons le bas nombre.

Il convient de mentionner que la signification de HighCount et LowCount est différente, HighCount affiche le nombre de threads prioritaires qui sont arrivés au verrou, lorsque LowCount signifie simplement que le thread (un seul) avec une priorité faible est entré dans le verrou.

 public class Locker : IDisposable { private readonly bool _isHigh; private LockMgr _mgr; public Locker(LockMgr mgr, bool isHigh = false) { _isHigh = isHigh; _mgr = mgr; if (mgr.CurThread == Thread.CurrentThread) { mgr.RecursionCount++; return; } if (_isHigh) { Interlocked.Increment(ref mgr.HighCount); mgr.High.Wait(); while (Interlocked.CompareExchange(ref mgr.LowCount, 0, 0) != 0) mgr.HighSpin.SpinOnce(); } else { mgr.Low.Wait(); while (Interlocked.CompareExchange(ref mgr.HighCount, 0, 0) != 0) mgr.LowSpin.SpinOnce(); try { mgr.High.Wait(); Interlocked.Increment(ref mgr.LowCount); } finally { mgr.High.Release(); } } mgr.CurThread = Thread.CurrentThread; } public void Dispose() { if (_mgr.RecursionCount > 0) { _mgr.RecursionCount--; _mgr = null; return; } _mgr.RecursionCount = 0; _mgr.CurThread = null; if (_isHigh) { _mgr.High.Release(); Interlocked.Decrement(ref _mgr.HighCount); } else { _mgr.Low.Release(); Interlocked.Decrement(ref _mgr.LowCount); } _mgr = null; } } public class HighLocker : Locker { public HighLocker(LockMgr mgr) : base(mgr, true) { } } 


L'utilisation de l'objet de classe LockMgr était très concise. L'exemple montre clairement la possibilité de réutiliser _lockMgr à l'intérieur de la section critique, tandis que la priorité n'est plus importante.

 private PriorityLock.LockMgr _lockMgr = new PriorityLock.LockMgr(); public void LowPriority() { using (_lockMgr.Lock()) { using (_lockMgr.HighLock()) { // your code } } } public void HighPriority() { using (_lockMgr.HighLock()) { using (_lockMgr.Lock()) { // your code } } } 

J'ai donc résolu mon problème. Le traitement des actions des utilisateurs a commencé à être effectué avec une priorité élevée, personne n'a été blessé, tout le monde a gagné.

Asynchronie


Étant donné que les objets de la classe SemaphoreSlim prennent en charge l'attente asynchrone, je me suis également ajouté cette opportunité. Le code diffère de façon minimale et à la fin de l'article, je fournirai un lien vers le code source.

Il est important de noter ici que la tâche n'est pas attachée au thread en aucune façon, par conséquent, la réutilisation asynchrone du verrou ne peut pas être implémentée de manière similaire. De plus, la propriété Task.CurrentId telle que décrite par MSDN ne garantit rien. C'est là que mes options se sont terminées.

À la recherche d'une solution, je suis tombé sur le projet NeoSmart.AsyncLock , dans la description duquel la prise en charge de la réutilisation du verrou asynchrone était indiquée. Techniquement, la réutilisation fonctionne. Mais malheureusement, la serrure elle-même n'est pas une serrure. Soyez prudent si vous utilisez ce package, sachez qu'il ne fonctionne pas correctement!

Conclusion


Le résultat est une classe qui prend en charge les opérations synchrones avec réutilisation et les opérations asynchrones sans réutilisation. Les opérations asynchrones et synchrones peuvent être utilisées côte à côte, mais ne peuvent pas être utilisées ensemble! Tout cela en raison du manque de support pour la réutilisation de l'option asynchrone.

J'espère que je ne suis pas seul dans de tels problèmes et ma solution sera utile à quelqu'un. J'ai posté la bibliothèque sur github et nuget.

Il existe des tests dans le référentiel qui montrent la santé de PriorityLock. Sur la partie asynchrone de ce test, NeoSmart.AsyncLock a été testé et le test a échoué.

Lien vers nuget
Lien Github

Source: https://habr.com/ru/post/fr458160/


All Articles