Interloqué.Incrément pas thread-safe?

J'ai trouvé un bug du compilateur en une seule ligne de code:

int thisIndex = Interlocked.Increment(ref messagesIndex) & indexMask;

Les définitions sont les suivantes:

static int messagesIndex = -1;
public const int MaxMessages = 0x10000;
const int indexMask = MaxMessages-1;

messagesIndex n'est pas accessible par d'autres ligne de code.

Si j'exécute ce code des milliards de fois dans un seul thread, je n'ai pas toutes les erreurs.

Si j'exécute la ligne ci-dessus sur plusieurs threads, j'obtiens le même nombre deux fois et un autre nombre est sautée chaque 1x-mille fois.

La ligne suivante, j'ai des milliards de fois sur 6 fils sans jamais avoir une erreur:

int thisIndex = Interlocked.Increment(ref messagesIndex);

Conclusion et Question

Il semble que Interlocked.Increment() sur son propre fonctionne comme prévu, mais Interlocked.Increment() & indexMask ne le fait pas 🙁

Aucune idée de comment je peux le faire fonctionner correctement tous les temps, et pas seulement à 99,99% ?

J'ai essayé d'attribuer Interlocked.Increment(ref messagesIndex) à une volatilité variable de type entier et de faire le "& indexMask" opération sur cette variable:

[ThreadStatic]
volatile static int nextIncrement;

nextIncrement = Interlocked.Increment(ref mainIndexIncrementModTest);
indexes[testThreadIndex++] = nextIncrement & maskIncrementModTest;

Il provoque le même genre de problème quand j'écris en 1 ligne.

Démontage

Peut-être quelqu'un peut le deviner à partir du démontage de ce problème le compilateur introduit:

indexes[testThreadIndex++] = Interlocked.Increment(ref mainIndexIncrementTest);
0000009a  mov         eax, dword ptr [ebp-48h] 
0000009d  mov         dword ptr [ebp-58h], eax 
000000a0  inc         dword ptr [ebp-48h] 
000000a3  mov         eax, dword ptr [ebp-44h] 
000000a6  mov         dword ptr [ebp-5Ch], eax 
000000a9  lea         ecx, ds:[00198F84h] 
000000af  call        6D758403 
000000b4  mov         dword ptr [ebp-60h], eax 
000000b7  mov         eax, dword ptr [ebp-58h] 
000000ba  mov         edx, dword ptr [ebp-5Ch] 
000000bd  cmp         eax, dword ptr [edx+4] 
000000c0  jb          000000C7 
000000c2  call        6D9C2804 
000000c7  mov         ecx, dword ptr [ebp-60h] 
000000ca  mov         dword ptr [edx+eax*4+8], ecx 
indexes[testThreadIndex++] = Interlocked.Increment(ref mainIndexIncrementModTest) & maskIncrementModTest;
0000009a  mov         eax, dword ptr [ebp-48h] 
0000009d  mov         dword ptr [ebp-58h], eax 
000000a0  inc         dword ptr [ebp-48h] 
000000a3  mov         eax, dword ptr [ebp-44h] 
000000a6  mov         dword ptr [ebp-5Ch], eax 
000000a9  lea         ecx,ds:[001D8F88h] 
000000af  call        6D947C8B 
000000b4  mov         dword ptr [ebp-60h], eax 
000000b7  mov         eax, dword ptr [ebp-60h] 
000000ba  and         eax, 0FFFh 
000000bf  mov         edx, dword ptr [ebp-58h] 
000000c2  mov         ecx, dword ptr [ebp-5Ch] 
000000c5  cmp         edx, dword ptr [ecx+4] 
000000c8  jb          000000CF 
000000ca  call        6DBB208C 
000000cf  mov         dword ptr [ecx+edx*4+8], eax 

La Détection Des Punaises De

Pour découvrir le bug, je lance le problème de la ligne en 6 fils à l'infini et chaque thread écrit le retourné entiers dans une énorme entier tableaux. Après un certain temps, j'ai arrêter les threads et la recherche de toutes les six entier tableaux si chaque nombre est retourné exactement une fois (bien sûr, je laisse pour le "& indexMask de l'opération").

using System;
using System.Text;
using System.Threading;
namespace RealTimeTracer 
{
class Test 
{
#region Test Increment Multi Threads
//     ----------------------------
const int maxThreadIndexIncrementTest = 0x200000;
static int mainIndexIncrementTest = -1; //the counter gets incremented before its use
static int[][] threadIndexThraces;
private static void testIncrementMultiThread() 
{
const int maxTestThreads = 6;
Thread.CurrentThread.Name = "MainThread";
//start writer test threads
Console.WriteLine("start " + maxTestThreads + " test writer threads.");
Thread[] testThreads = testThreads = new Thread[maxTestThreads];
threadIndexThraces = new int[maxTestThreads][];
int testcycle = 0;
do 
{
testcycle++;
Console.WriteLine("testcycle " + testcycle);
for (int testThreadIndex = 0; testThreadIndex < maxTestThreads; testThreadIndex++) 
{
Thread testThread = new Thread(testIncrementThreadBody);
testThread.Name = "TestThread " + testThreadIndex;
testThreads[testThreadIndex] = testThread;
threadIndexThraces[testThreadIndex] = new int[maxThreadIndexIncrementTest+1]; //last int will be never used, but easier for programming
}
mainIndexIncrementTest = -1; //the counter gets incremented before its use
for (int testThreadIndex = 0; testThreadIndex < maxTestThreads; testThreadIndex++) 
{
testThreads[testThreadIndex].Start(testThreadIndex);
}
//wait for writer test threads
Console.WriteLine("wait for writer threads.");
foreach (Thread testThread in testThreads)
{
testThread.Join();
}
//verify that EVERY index is used exactly by one thread.
Console.WriteLine("Verify");
int[] threadIndexes = new int[maxTestThreads];
for (int counter = 0; counter < mainIndexIncrementTest; counter++) 
{
int threadIndex = 0;
for (; threadIndex < maxTestThreads; threadIndex++) 
{
if (threadIndexThraces[threadIndex][threadIndexes[threadIndex]]==counter) 
{
threadIndexes[threadIndex]++;
break;
}
}
if (threadIndex==maxTestThreads) 
{
throw new Exception("Could not find index: " + counter);
}
}
} while (!Console.KeyAvailable);
}
public static void testIncrementThreadBody(object threadNoObject)
{
int threadNo = (int)threadNoObject;
int[] indexes = threadIndexThraces[threadNo];
int testThreadIndex = 0;
try
{
for (int counter = 0; counter < maxThreadIndexIncrementTest; counter++)      
{
indexes[testThreadIndex++] = Interlocked.Increment(ref mainIndexIncrementTest);
}
} 
catch (Exception ex) 
{
OneTimeTracer.Trace(Thread.CurrentThread.Name + ex.Message);
}
}
#endregion
#region Test Increment Mod Multi Threads
//     --------------------------------
const int maxThreadIndexIncrementModTest = 0x200000;
static int mainIndexIncrementModTest = -1; //the counter gets incremented before its use
const int maxIncrementModTest = 0x1000;
const int maskIncrementModTest = maxIncrementModTest - 1;
private static void testIncrementModMultiThread() 
{
const int maxTestThreads = 6;
Thread.CurrentThread.Name = "MainThread";
//start writer test threads 
Console.WriteLine("start " + maxTestThreads + " test writer threads.");
Thread[] testThreads = testThreads = new Thread[maxTestThreads];
threadIndexThraces = new int[maxTestThreads][];
int testcycle = 0;
do 
{
testcycle++;
Console.WriteLine("testcycle " + testcycle);
for (int testThreadIndex = 0; testThreadIndex < maxTestThreads; testThreadIndex++)
{
Thread testThread = new Thread(testIncrementModThreadBody);
testThread.Name = "TestThread " + testThreadIndex;
testThreads[testThreadIndex] = testThread;
threadIndexThraces[testThreadIndex] = new int[maxThreadIndexIncrementModTest+1]; //last int will be never used, but easier for programming
}
mainIndexIncrementModTest = -1; //the counter gets incremented before its use
for (int testThreadIndex = 0; testThreadIndex < maxTestThreads; testThreadIndex++) 
{
testThreads[testThreadIndex].Start(testThreadIndex);
}
//wait for writer test threads
Console.WriteLine("wait for writer threads.");
foreach (Thread testThread in testThreads) 
{
testThread.Join();
}
//verify that EVERY index is used exactly by one thread.
Console.WriteLine("Verify");
int[] threadIndexes = new int[maxTestThreads];
int expectedIncrement = 0;
for (int counter = 0; counter < mainIndexIncrementModTest; counter++) 
{
int threadIndex = 0;
for (; threadIndex < maxTestThreads; threadIndex++) 
{
if (threadIndexes[threadIndex]<maxThreadIndexIncrementModTest     && 
threadIndexThraces[threadIndex][threadIndexes[threadIndex]]==expectedIncrement) 
{
threadIndexes[threadIndex]++;
expectedIncrement++;
if (expectedIncrement==maxIncrementModTest) 
{
expectedIncrement = 0;
}
break;
}
}
if (threadIndex==maxTestThreads) 
{
StringBuilder stringBuilder = new StringBuilder();
for (int threadErrorIndex = 0; threadErrorIndex < maxTestThreads; threadErrorIndex++)
{
int index = threadIndexes[threadErrorIndex];
if (index<0) 
{
stringBuilder.AppendLine("Thread " + threadErrorIndex + " is empty");
}
else if (index==0)
{
stringBuilder.AppendLine("Thread " + threadErrorIndex + "[0]=" +
threadIndexThraces[threadErrorIndex][0]);
}
else if (index>=maxThreadIndexIncrementModTest) 
{
stringBuilder.AppendLine("Thread " + threadErrorIndex + "[" + (index-1) + "]=" +
threadIndexThraces[threadErrorIndex][maxThreadIndexIncrementModTest-2] + ", " + 
threadIndexThraces[threadErrorIndex][maxThreadIndexIncrementModTest-1]);
} 
else 
{
stringBuilder.AppendLine("Thread " + threadErrorIndex + "[" + (index-1) + "]=" +
threadIndexThraces[threadErrorIndex][index-1] + ", " + 
threadIndexThraces[threadErrorIndex][index]);
}
} 
string exceptionString = "Could not find index: " + expectedIncrement + " for counter " + counter + Environment.NewLine + stringBuilder.ToString();
Console.WriteLine(exceptionString);
return;
//throw new Exception(exceptionString);
}
}
} while (!Console.KeyAvailable);
}
public static void testIncrementModThreadBody(object threadNoObject)
{
int threadNo = (int)threadNoObject;
int[] indexes = threadIndexThraces[threadNo];
int testThreadIndex = 0;
try
{
for (int counter = 0; counter < maxThreadIndexIncrementModTest; counter++) 
{
//indexes[testThreadIndex++] = Interlocked.Increment(ref mainIndexIncrementModTest) & maskIncrementModTest;
int nextIncrement = Interlocked.Increment(ref mainIndexIncrementModTest);
indexes[testThreadIndex++] = nextIncrement & maskIncrementModTest;
}
} 
catch (Exception ex) 
{
OneTimeTracer.Trace(Thread.CurrentThread.Name + ex.Message);
}
}
#endregion
}
}

Ce qui produit l'erreur suivante:

Contenu du 6 int-tableaux (1 par thread)

Thread 0[30851]=2637, 2641

Thread 1[31214]=2639, 2644

Thread 2[48244]=2638, 2643

Fil 3[26512]=2635, 2642

Fil 4[0]=2636, 2775

Fil 5[9173]=2629, 2636

Explication:

Enfiler 4 utilise 2636

Fil 5 utilise aussi 2636 !!!! Cela ne doit jamais arriver

Thread 0 utilisé 2637

Thread 2 2638

1 fil utilisé 2639

2640 n'est pas utilisé par n'importe quel thread !!! C'est l'erreur le test détecte

Thread 0 utilisé 2641

Fil 3 2642

  • L'incrément et & sont deux opérations, il n'y a aucune raison pour laquelle l'association doit être atomique. Utiliser des verrous à l'adresse de l'association de ces opérations.
  • Votre question a reçu quelques voix près. Quelques commentaires: le Garder très ciblées. Dire: a) ce que vous essayez de faire, b) la façon dont vous le faites, c) ce que vous attendiez, d), de ce qui s'est réellement passé. Si il n'y a aucune chance de vous croire que le problème est dans votre code, puis se concentrer sur votre premier code. C'est d'accord pour inclure vos spéculations, mais qui devrait être laissé pour la fin afin de ne pas détourner l'attention de la question. Si vous croyez qu'il y a une erreur avec le CLR, puis se concentrer sur le qui-ne comprend pas quelque chose pour tester votre code -- inclure une méthode de test de code CLR, et la machine, le code qu'il génère.
  • Candide commentaire est faux. Seulement Incrément doit être le multithreading, coffre-fort (atomique). Une fois l'Incrément renvoie la valeur correcte, toute opération sur cette valeur n'a pas d'influence messagesIndex mais une valeur locale, qui ne peut être vu par les autres threads.
  • Cory: votre réponse montre que vous n'avez pas compris le problème que j'ai décrit ni essayé le test de code que j'ai fourni. Veuillez ne pas utiliser votre incompréhension de l'état que la question devrait être fermé. "Mon code" consiste exactement 1 ligne: "int thisIndex = Interloqué.Incrément(réf messagesIndex) & indexMask;" Puis-je fournir le code de test qui prouve que cette déclaration donne parfois le même numéro, qui de toute évidence ne devrait jamais se produire. Et non, l'erreur ne peut pas se produire lorsque thisIndex retourne à zéro.
  • Bien que vous avez obtenu beaucoup de upvotes atomicité ne joue aucun rôle ici. Voir ma réponse.
  • Au lieu de changer à cette question, il est préférable de la fermer. Il semble que les gens ne comprennent pas ce qu'est le problème, ce qui signifie que je dois réécrire la question complètement, ce qui rendrait certaines des réponses fournies hors de leur contexte. Je voudrais fermer la question moi-même, mais je ne sais pas comment faire. J'espère que la réécriture de tout à partir de zéro et de poster une nouvelle question n'est pas contre les règles.

InformationsquelleAutor Peter Huber | 2014-06-22