Il s'agit d'une condition de course. EDIT: En fait, il existe plusieurs conditions de course.
Il peut arriver à tout moment en cas _state
est de 3 et les deux fils d'atteindre juste au-delà de l' if
déclaration, soit en même temps grâce à la commutation de contexte dans un seul core, simultanément ou en parallèle dans plusieurs cœurs.
C'est parce que l' ++
opérateur de lit pour la première fois _state
et l'incrémente. Il est possible que l'un a eu assez de temps, après la première if
déclaration qu'il va lire 5 ou même 6.
EDIT: Si vous voulez généraliser cet exemple pour N threads, vous pouvez observer un nombre aussi élevé que 3 + N+1.
Ce peut être le droit lorsque les fils commencent à courir, ou lorsque l'on vient de mettre en _state
à 3.
Pour éviter cela, utilisez un verrou autour de l' if
déclaration, ou utiliser Interlocked
accès _state
, comme if (System.Threading.Interlocked.CompareAndExchange(ref _state, 3, 4) == 3)
et System.Threading.Interlocked.Exchange(ref _state, 3)
.
Si vous voulez garder la condition de la course, vous devez déclarer _state
comme volatile
, sinon vous risquez de chaque thread voir _state
localement, sans les mises à jour des autres threads.
En alternative, vous pouvez utiliser System.Threading.Volatile.Read
et System.Threading.Volatile.Write
, dans le cas où vous changez de mise en œuvre de disposer _state
comme une variable et d' Tr
comme une fermeture qui capte cette variable, ainsi que les variables locales peuvent pas (et ne sera pas en mesure d'être) a déclaré, volatile
. Dans ce cas, même l'initialisation doit être fait avec une écriture volatile.
EDIT: peut-être les conditions de course sont de plus en plus apparente si l'on change le code légèrement par l'expansion de tous les lire:
// Without some sort of memory barrier (volatile, lock, Interlocked.*),
// a thread is allowed to see _state as if other threads hadn't touched it
private static volatile int _state = 3;
// ...
for (int i = 0; i < 10000000; i++)
{
int currentState;
currentState = _state;
if (currentState == 3)
{
// RACE CONDITION: re-read the variable
currentState = _state;
currentState = currentState + 1:
// RACE CONDITION: non-atomic write
_state = currentState;
currentState = _state;
if (currentState != 4)
{
// RACE CONDITION: re-read the variable
currentState = _state;
Console.Write(currentState);
}
_state = 3;
}
}
J'ai ajouté des commentaires dans les endroits où l' _state
peuvent être différentes de celles assumées par les précédents variable de lire une déclaration.
Voici un long diagramme, ce qui montre qu'il est même possible d'imprimer 6 deux fois dans une rangée, une fois dans chaque thread, comme l'image que l' op posté. Rappelez-vous, les threads ne peuvent pas s'exécuter dans synch, généralement en raison de préemption de commutation de contexte, la mise en cache des stalles, le cœur des différences de vitesse (en raison d'économie d'énergie ou temporaire de la vitesse turbo):
![Race condition prints 6]()
Celui-ci est semblable à l'original, mais il utilise l' Volatile
classe, où l' state
est maintenant une variable capturée par une fermeture. Le montant et l'ordre de la volatilité des accès devient évident:
static void Main(string[] args)
{
int state = 3;
ThreadStart tr = () =>
{
for (int i = 0; i < 10000000; i++)
{
if (Volatile.Read(ref state) == 3)
{
Volatile.Write(ref state, Volatile.Read(state) + 1);
if (Volatile.Read(ref state) != 4)
{
Console.Write(Volatile.Read(ref state));
}
Volatile.Write(ref state, 3);
}
}
};
Thread firstThread = new Thread(tr);
Thread secondThread = new Thread(tr);
firstThread.Start();
secondThread.Start();
firstThread.Join();
secondThread.Join();
Console.ReadLine();
}
Certains thread-safe approches:
private static object _lockObject;
// ...
// Do not allow concurrency, blocking
for (int i = 0; i < 10000000; i++)
{
lock (_lockObject)
{
// original code
}
}
// Do not allow concurrency, non-blocking
for (int i = 0; i < 10000000; i++)
{
bool lockTaken = false;
try
{
Monitor.TryEnter(_lockObject, ref lockTaken);
if (lockTaken)
{
// original code
}
}
finally
{
if (lockTaken) Monitor.Exit(_lockObject);
}
}
// Do not allow concurrency, non-blocking
for (int i = 0; i < 10000000; i++)
{
// Only one thread at a time will succeed in exchanging the value
try
{
int previousState = Interlocked.CompareExchange(ref _state, 4, 3);
if (previousState == 3)
{
// Allow race condition on purpose (for no reason)
int currentState = Interlocked.CompareExchange(ref _state, 0, 0);
if (currentState != 4)
{
// This branch is never taken
Console.Write(currentState);
}
}
}
finally
{
Interlocked.CompareExchange(ref _state, 3, 4);
}
}
// Allow concurrency
for (int i = 0; i < 10000000; i++)
{
// All threads increment the value
int currentState = Interlocked.Increment(ref _state);
if (currentState == 4)
{
// But still, only one thread at a time enters this branch
// Allow race condition on purpose (it may actually happen here)
currentState = Interlocked.CompareExchange(ref _state, 0, 0);
if (currentState != 4)
{
// This branch might be taken with a maximum value of 3 + N
Console.Write(currentState);
}
}
Interlocked.Decrement(ref _state);
}
Celui-ci est un peu différent, il prend la dernière valeur connue de l' _state
après l'incrémentation effectuer quelque chose:
// Allow concurrency
for (int i = 0; i < 10000000; i++)
{
// All threads increment the value
int currentState = Interlocked.Increment(ref _state);
if (currentState != 4)
{
// Only the thread that incremented 3 will not take the branch
// This can happen indefinitely after the first increment for N > 1
// This branch might be taken with a maximum value of 3 + N
Console.Write(currentState);
}
Interlocked.Decrement(ref _state);
}
Notez que l' Interlocked.Increment
/Interlocked.Decrement
exemples ne sont pas sûrs, contrairement à l' lock
/Monitor
et Interlocked.CompareExchange
exemples, comme il n'y a aucun moyen fiable de savoir si l'augmentation a été couronnée de succès ou non.
Une approche courante consiste à incrémenter, puis suivre avec un try
/finally
où vous décrémenter dans l' finally
bloc. Cependant, asynchrone exception peut être levée (par exemple, ThreadAbortException
)
Asynchrone des exceptions peuvent être jetés dans des lieux inattendus, chaque instruction machine: ThreadAbortException, StackOverflowException, et OutOfMemoryException.
Une autre approche consiste à initialiser currentState
de quelque chose en dessous de 3, et à condition que la décrémentation en finally
bloc. Mais encore une fois, dans l'entre - Interlocked.Increment
de la retourner et de currentState
être attribuée au résultat, asynchrone exception peut se produire, si l' currentState
pourrait encore avoir de la valeur initiale, même si l' Interlocked.Increment
réussi.