web-dev-qa-db-fra.com

Est-ce que la garantie lock () est acquis dans l’ordre demandé?

Lorsque plusieurs threads demandent un verrou sur le même objet, le CLR garantit-il que les verrous seront acquis dans l'ordre dans lequel ils ont été demandés?

J'ai écrit un test pour voir si c'était vrai, et il semble indiquer oui, mais je ne suis pas sûr que ce soit définitif.

class LockSequence
{
    private static readonly object _lock = new object();

    private static DateTime _dueTime;

    public static void Test()
    {
        var states = new List<State>();

        _dueTime = DateTime.Now.AddSeconds(5);

        for (int i = 0; i < 10; i++)
        {
            var state = new State {Index = i};
            ThreadPool.QueueUserWorkItem(Go, state);
            states.Add(state);
            Thread.Sleep(100);
        }

        states.ForEach(s => s.Sync.WaitOne());
        states.ForEach(s => s.Sync.Close());
    }

    private static void Go(object state)
    {
        var s = (State) state;

        Console.WriteLine("Go entered: " + s.Index);

        lock (_lock)
        {
            Console.WriteLine("{0,2} got lock", s.Index);
            if (_dueTime > DateTime.Now)
            {
                var time = _dueTime - DateTime.Now;
                Console.WriteLine("{0,2} sleeping for {1} ticks", s.Index, time.Ticks);
                Thread.Sleep(time);
            }
            Console.WriteLine("{0,2} exiting lock", s.Index);
        }

        s.Sync.Set();
    }

    private class State
    {
        public int Index;
        public readonly ManualResetEvent Sync = new ManualResetEvent(false);
    }
}

Impressions:

Go entré: 0

0 a le verrou

0 dormant pour 49979998 tiques

Go entré: 1

Go entré: 2

Go entré: 3

Go entré: 4

Go entré: 5

Go entré: 6

Go entré: 7

Go entré: 8

Go entré: 9

0 verrou sortant

1 verrou obtenu

1 dormant pour 5001 tiques

1 serrure de sortie

2 ont le verrou

2 couchages pour 5001 tiques

2 serrure de sortie

3 obtenu le verrou

3 couchages pour 5001 tiques

3 serrure de sortie

4 obtenu le verrou

4 couchages pour 5001 tiques

4 serrure de sortie

5 obtenu le verrou

5 couchages pour 5001 tiques

5 serrure de sortie

6 obtenu le verrou

6 serrure de sortie

7 ont le verrou

7 serrure de sortie

8 obtenu le verrou

8 serrure de sortie

9 ont le verrou

9 serrure de sortie

58
Samuel Neff

IIRC, il est très probable être dans cet ordre, mais ce n'est pas garanti. Je crois qu'il y a au moins théoriquement des cas où un fil va être réveillé de façon fausse, notez qu'il n'a toujours pas le verrou, et allez au fond de la file d'attente. Il est possible que ce ne soit que pour Wait/Notify, mais je soupçonne que c'est aussi pour le verrouillage.

I certainement ne compterais pas dessus - si vous souhaitez que les choses se déroulent dans l'ordre, créez un Queue<T> ou quelque chose de similaire.

EDIT: Je viens de trouver cela dans Programmation concurrente de Joe Duffy qui est fondamentalement d’accord:

Dans la mesure où les moniteurs utilisent les objets du noyau en interne, ils présentent le même comportement à peu près FIFO que les mécanismes de synchronisation du système d'exploitation (décrits dans le chapitre précédent). Les moniteurs sont injustes. Par conséquent, si un autre thread tente d'acquérir le verrou avant qu'un thread en attente éveillé tente d'acquérir le verrou, le thread sournois est autorisé à acquérir un verrou.

Le bit "approximativement-FIFO" est ce à quoi je pensais auparavant, et le bit "fil sournois" est une preuve supplémentaire qu'il ne faut pas présumer de la commande de FIFO.

71
Jon Skeet

Il n’est pas garanti que les verrous CLR normaux soient FIFO.

Cependant, il existe une classe QueuedLock dans cette réponsequi fournira un comportement de verrouillage garanti FIFO _.

10
Bryan Legend

L'instruction lock utilise la classe Monitor pour implémenter son comportement, et la documentation de la classe Monitor ne fait aucune mention (que je puisse trouver) d'équité. Par conséquent, vous ne devez pas vous fier à l’acquisition des verrous demandés dans l’ordre de la demande.

En fait, un article de Jeffery Richter indique que lock n’est pas juste:

Certes, c'est un ancien article, donc les choses ont peut-être changé, mais étant donné qu'aucune promesse n'est faite dans le contrat pour la classe Monitor concernant l'équité, vous devez assumer le pire.

10
Michael Burr

Légèrement tangent à la question, mais ThreadPool ne garantit même pas qu'il exécutera les éléments de travail en file d'attente dans l'ordre dans lequel ils ont été ajoutés. Si vous avez besoin d'une exécution séquentielle de tâches asynchrones, une option consiste à utiliser des tâches TPL (également transférées vers .NET 3.5 via Reactive Extensions ). Cela ressemblerait à ceci:

    public static void Test()
    {
        var states = new List<State>();

        _dueTime = DateTime.Now.AddSeconds(5);

        var initialState = new State() { Index = 0 };
        var initialTask = new Task(Go, initialState);
        Task priorTask = initialTask;

        for (int i = 1; i < 10; i++)
        {
            var state = new State { Index = i };
            priorTask = priorTask.ContinueWith(t => Go(state));

            states.Add(state);
            Thread.Sleep(100);
        }
        Task finalTask = priorTask;

        initialTask.Start();
        finalTask.Wait();
    }

Cela présente quelques avantages:

  1. L'ordre d'exécution est garanti.

  2. Vous n'avez plus besoin d'un verrou explicite (la TPL s'occupe de ces détails).

  3. Vous n'avez plus besoin d'événements ni d'attendre tous les événements. Vous pouvez simplement dire: attendez que la dernière tâche soit terminée.

  4. Si une exception était levée dans l'une des tâches, les tâches suivantes seraient abandonnées et l'exception serait renumérotée par l'appel à Wait. Cela peut correspondre ou non au comportement souhaité, mais constitue généralement le meilleur comportement pour les tâches séquentielles dépendantes.

  5. En utilisant la TPL, vous avez davantage de flexibilité pour les extensions futures, telles que la prise en charge des annulations, l'attente de la poursuite de tâches parallèles, etc.

2
Dan Bryant

J'utilise cette méthode pour verrouiller FIFO

public class QueuedActions
{
    private readonly object _internalSyncronizer = new object();
    private readonly ConcurrentQueue<Action> _actionsQueue = new ConcurrentQueue<Action>();


    public void Execute(Action action)
    {
        // ReSharper disable once InconsistentlySynchronizedField
        _actionsQueue.Enqueue(action);

        lock (_internalSyncronizer)
        {
            Action nextAction;
            if (_actionsQueue.TryDequeue(out nextAction))
            {
                nextAction.Invoke();
            }
            else
            {
                throw new Exception("Something is wrong. How come there is nothing in the queue?");
            }
        }
    }
}

La ConcurrentQueue ordonnera l'exécution des actions pendant que les threads attendent dans le verrou.

0
Noam