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.
Vous devez vous connecter pour publier un commentaire.
Il n'est pas Interloqué qui est faux. Il ya aussi pas de condition de course. Il n'a même pas quelque chose à faire avec le multithreading.
Il n'y a pas de condition de course et pas de problème avec l'atomicité comme il a été spéculé. Interloqué renvoie la valeur lue dans un registre (eax). Le ruban qui se passe sur la lecture de la valeur à l'intérieur d'un registre qui n'a rien à voir avec la mémoire de modèles ou de l'atomicité. Les registres et les variables locales ne peuvent pas être vus à partir d'autres threads et donc de ne pas interférer.
Permet de vérifier que toutes les valeurs sont vus par l'aide d'un int[nThreads] lorsque vous vérifiez chaque thread si elle a vu la valeur de l'indice n et supposons que la valeur suivante doit être vu sur ce site ou un autre thread.
J'ai renommé les variables pour avoir une vue plus claire de nommage. J'ai changé le masqués test que le bitmasking n'est pas fait dans le fil, mais au cours de vérifier le temps qui affirme. Cela montre que vous avez un problème de logique dans votre code de test. Le filetage de la fonction ne fait que stocker la valeur incrémentée que dans votre pas masqué test.
Votre chèque pauses quand il y a un bouclage dans les valeurs. E. g. 0 est la valeur première. Au 0x1000 & 0xFFF il est à 0. Maintenant, il peut arriver que vous compte de certaines de enveloppés valeurs au fil mal, et vous briser l'hypothèse implicite que chaque thread a que des valeurs uniques.
Dans le débogueur, j'voir, par exemple, pour la valeur 8
bien que vous devez prendre en compte les 8 premières valeurs de threadIndexes[1], qui est le premier thread qui commence à compter à partir de 0 à plusieurs milliers.
Pour résumer: Contrefil et le masque de travail. Votre test est erroné et peut-être certains de votre code s'appuyant sur des invalides hypothèses.
Increment
ne retourne pas de valeur lue, mais modifié (c'est à dire incrémenté) une.Rassurez-vous,
Interlocked.Increment
est thread-safe. C'est son but en entier!Vous testez que chaque thread a vu chaque indice exactement une fois. Cela ne peut fonctionner que si les fils ont été exécutés un à un. Dire votre par nombre de threads est de 10 000:
Un voudrais obtenir de 0 à 9999, B, obtenir 10000-19999, etc. -- quand masqué, chacun allait voir les 0 à 9999 exactement une fois.
Mais vos discussions sont en cours d'exécution simultanément. Si l'index de votre fils voir sont sporadiques et imprévisibles, interleave:
Un obtient 0-4999, B obtient 5000-9999, Un obtient 10000-14999, B obtient 15000-19999.
Démasqué, chaque valeur restera unique. Masqués, peut voir de toutes 0-4999 deux fois, et B voir 5000-9999 deux fois.
Vous n'avez pas état de ce que votre objectif ultime est, mais un meilleur choix pour vous pourrait être TLS:
À l'aide de la
ThreadStatic
attribut, chaque thread va seulement voir sa propre instance deperThreadIndex
, de sorte que vous n'aurez plus jamais besoin de s'inquiéter au sujet d'un thread de voir un double index.Il n'y a pas de bug.
Il est facile de voir que l'incrément est atomique, car il est exécuté comme une seule instruction machine, à l'offset a0 dans la deuxième partie du code. De même, la
and
opération n'est pas atomique, car il est exécuté comme une séquence d'instructions à partir de offset b7.Vous pouvez effectuer une atomique opération au niveau du bit en C++ à l'aide de la atomique en de la bibliothèque. Vous pouvez également mettre en place des opérations plus complexes basés sur atomiques de comparer et d'échanger. si vous êtes prêt à écrire votre code en C++ et de l'appeler à partir de C# qui serait une solution (Interop fonctionne vraiment bien).
Si vous avez besoin d'un C# seule solution, vous pouvez toujours le faire à l'aide de Contrefil.Exchange. Essentiellement, la stratégie consiste à effectuer le calcul dans une boucle jusqu'à ce que la valeur que vous obtenez de retour sur l'Échange est la même que la valeur utilisée pour le calcul, ainsi, de garantir que personne d'autre n'a changé.
Et puis vous pouvez utiliser des verrous. Je n'ai jamais utiliser des verrous si il existe une solution de rechange raisonnable.
Laissez-moi vous expliquer l'atomicité. Une opération est atomique si elle s'exécute entièrement ou pas du tout, mais rien entre les deux. Une seule instruction machine est atomique, parce que les instructions ne sont jamais (visiblement) interrompu. Toute séquence d'instructions est non-atomique, parce que la séquence peut être interrompue. Au cours de cette interruption dans un environnement multithread, le contenu de la mémoire peut être modifié de manière à invalider le calcul.
Il existe plusieurs stratégies pour faire des séquences d'instructions atomiques dans la pratique, si ce n'est dans la théorie.
Donc, cette ligne de code est atomique, car il génère une seule instruction machine.
Cette ligne de code n'est pas atomique, parce qu'il génère de nombreuses instructions et chaque instruction s'exécute séparément.
Le problème est avec la mémoire charger et stocker. L'opération sur le registre AX est très bien, mais le chargement et le stockage de la mémoire s'appuie sur les valeurs de la mémoire étant stable et dans un environnement multi-threads qui peuvent ne pas l'être.