Я обнаружил ошибку компилятора только в одной строке кода:
int thisIndex = Interlocked.Increment(ref messagesIndex) & indexMask;
Определения:
static int messagesIndex = -1;
public const int MaxMessages = 0x10000;
const int indexMask = MaxMessages-1;
messagesIndex
не имеет доступа ни к одной другой строке кода.
Если я запускаю этот код миллиарды раз в одном потоке, я не получаю никаких ошибок.
Если я запускаю указанную выше строку на нескольких потоках, я получаю один и тот же номер дважды, а другой номер пропускается каждые 1 × тысячу раз.
Следующая строка, которую я запускал миллиарды раз на 6 потоках, никогда не получал ошибку:
int thisIndex = Interlocked.Increment(ref messagesIndex);
Заключение и вопрос
Кажется, что Interlocked.Increment()
работает самостоятельно, как и ожидалось, но Interlocked.Increment()
& indexMask
не делает :-(
Любая идея, как я могу заставить ее работать правильно все время, а не только 99,99%?
Я попытался назначить Interlocked.Increment(ref messagesIndex)
переменной volatile integer и выполнить операцию "& indexMask"
для этой переменной:
[ThreadStatic]
volatile static int nextIncrement;
nextIncrement = Interlocked.Increment(ref mainIndexIncrementModTest);
indexes[testThreadIndex++] = nextIncrement & maskIncrementModTest;
Это вызывает ту же проблему, что и при записи в 1 строке.
разборка
Возможно, кто-то может догадаться из разборки, какую проблему представляет компилятор:
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
Обнаружение ошибок
Чтобы обнаружить ошибку, я запускаю проблемную строку из 6 потоков бесконечно, и каждый поток записывает возвращенные целые числа в огромные массивы с целыми числами. Через некоторое время я останавливаю потоки и просматриваю все шесть целых массивов, если каждое число возвращается ровно один раз (конечно, я допускаю операцию "& indexMask").
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
}
}
Это приводит к следующей ошибке:
Содержимое 6 внутренних массивов (1 на поток)
Тема: 0 [30851] = 2637, 2641
Тема 1 [31214] = 2639, 2644
Тема 2 [48244] = 2638, 2643
Тема 3 [26512] = 2635, 2642
Резьба 4 [0] = 2636, 2775
Тема 5 [9173] = 2629, 2636
Объяснение:
В теме 4 используется 2636
Тема 5 также использует 2636 !!!! Это никогда не должно произойти
Тема 3 использовали 2637
Резьба 2 б/у 2638
Тема 1 использовали 2639
2640 нигде не используется !!! То, что ошибка, обнаруженная тестом
Тема 0 использовали 2641
Резьба 3 б/у 2642
Это не взаимосвязано, что неправильно. Также нет состояния гонки. Это не имеет никакого отношения к многопоточности.
Нет никаких условий гонки и нет проблем с атомарностью, как это предполагалось. Interlocked возвращает значение чтения в регистре (eax). Маскировка происходит при чтении значения внутри регистра, который не имеет ничего общего с моделями памяти или атомарностью. Регистры и локальные переменные не видны из других потоков и, следовательно, не мешают.
Вы проверяете, что все значения видны с помощью int [nThreads], где вы проверяете каждый поток, если он видел значение в индексе n, и предполагайте, что следующее значение должно быть замечено на этом или любом другом потоке.
Console.WriteLine("Verify");
int[] threadIndexes = new int[nThreads];
for (int counter = 0; counter < GlobalCounter; counter++)
{
int nThread = 0;
for (; nThread < nThreads; nThread++)
{
if (ThreadArrays[nThread][threadIndexes[nThread]]==counter)
{
threadIndexes[nThread]++;
break;
}
}
if (nThread==nThreads)
{
throw new Exception("Could not find index: " + counter);
}
}
Я переименовал переменные, чтобы получить более четкое именование. Я изменил маскированный тест, что битмаскирование не выполняется в потоке, но во время проверки, которое также утверждает. Это показывает, что у вас есть логическая проблема в вашем тестовом коде. Функция с резьбой хранит только увеличиваемое значение, как в тесте, который не замаскирован.
//verify that EVERY index is used exactly by one thread.
Console.WriteLine("Verify");
int[] threadIndexes = new int[nThreads];
int expectedIncrement = 0;
for (int counter = 0; counter < GlobalCounter; counter++)
{
int threadIndex = 0;
for (; threadIndex < nThreads; threadIndex++)
{
if (threadIndexes[threadIndex]<LoopCount && (ThreadArrays[threadIndex][threadIndexes[threadIndex]] & MaxIncrementBitMask)==expectedIncrement)
{
threadIndexes[threadIndex]++;
expectedIncrement++;
if (expectedIncrement == MaxIncrementBit)
{
expectedIncrement = 0;
}
break;
}
}
if (threadIndex==nThreads)
{
Ваша проверка ломается, когда в значениях есть значение. Например, 0 - первое значение. В 0x1000 и 0xFFF это снова 0. Теперь может случиться так, что вы учтите некоторые из обернутых значений в неправильный поток, и вы нарушите неявное предположение, что каждый поток имеет только уникальные значения. В отладчике я вижу, например, значение 8
threadIndexes [0] = 1
threadIndexes [1] = 4
threadIndexes [2] = 0
threadIndexes [3] = 1
threadIndexes [4] = 1
threadIndexes [5] = 1
хотя вы должны учитывать первые 8 значений для threadIndexes [1], который является первым потоком, который начинает отсчет от 0 до нескольких тысяч.
Подводя итог: работа с блокировкой и маскировкой. Ваш тест испорчен и, возможно, часть вашего кода основана на неверных предположениях.
Будьте уверены, Interlocked.Increment
является потокобезопасным. Это вся его цель!
Вы проверяете, что каждый поток видел каждый индекс ровно один раз. Это будет работать, если потоки будут выполняться по одному. Скажите, что количество ваших потоков составляет 10 000:
A получит 0-9999, B получит 10000-19999 и т.д. - при маскировке каждый будет видеть 0-9999 ровно один раз.
Но ваши потоки выполняются одновременно. Таким образом, индексы ваших потоков видны спорадическим, непредсказуемым чередованием:
A получает 0-4999, B получает 5000-9999, A получает 10000-14999, B получает 15000-19999.
Unmasked, каждое значение останется уникальным. Masked, A в конечном итоге будет видеть все 0-4999 дважды, а B будет видеть 5000-9999 два раза.
Вы не указываете, какова ваша конечная цель, но лучшим выбором для вас может быть TLS:
[ThreadStatic]
static int perThreadIndex = -1;
int myIndex = ++perThreadIndex;
Используя атрибут ThreadStatic
, каждый поток будет видеть только собственный личный экземпляр perThreadIndex
, поэтому вам не придется беспокоиться о потоке, просматривающем дублирующий индекс.
Ошибок нет.
Легко видеть, что приращение является атомарным, поскольку оно выполняется как единая машинная команда при смещении a0 во втором фрагменте кода. В равной степени, and
операция не является атомарным, так как он выполнен в виде последовательности команд, начиная со смещением b7.
Вы можете выполнить атомную побитовую операцию в C++ с использованием атомной библиотеки. Вы также можете реализовать более сложные операции на основе атомного сравнения и обмена. если вы хотите написать свой код в C++ и назвать его с С#, который будет решением (Interop работает очень хорошо).
Если вам требуется только С# решение, вы все равно можете сделать это с помощью Interlocked.Exchange. По сути, стратегия состоит в том, чтобы выполнить вычисление в цикле до тех пор, пока значение, возвращаемое вами на Exchange, не будет таким же, как значение, которое вы использовали для расчета, тем самым гарантируя, что никто другой не изменит его.
И тогда вы можете использовать блокировки. Я никогда не использую блокировки, если есть разумная альтернатива.
Позвольте мне объяснить атомарность. Операция является атомарной, если она либо полностью или полностью выполняется, но и ничего между ними. Единая машинная инструкция является атомарной, потому что инструкции никогда (видимо) не прерываются. Любая последовательность инструкций неатомна, потому что последовательность может быть прервана. Во время этого прерывания в многопоточной среде содержимое памяти может быть изменено, чтобы сделать недействительным расчет.
Существует несколько стратегий, чтобы сделать последовательности инструкций атомными на практике, если не теоретически.
Итак, эта строка кода является атомарной, потому что она генерирует одну машинную инструкцию.
nextIncrement = Interlocked.Increment(ref mainIndexIncrementModTest);
000000a0 inc dword ptr [ebp-48h]
Эта строка кода не является атомарной, потому что она генерирует много инструкций, и каждая команда выполняется отдельно.
indexes[testThreadIndex++] = nextIncrement & maskIncrementModTest;
000000b7 mov eax, dword ptr [ebp-60h] ; <=== load
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 ; <=== store
Проблема связана с загрузкой и хранением памяти. Операция в регистре AX прекрасна, но загрузка и хранение в память зависит от того, что эти значения памяти являются стабильными и в многопоточной среде, которые могут быть не такими.
&
являются двумя операциями, поэтому нет причин, по которым комбинация должна быть атомарной. Используйте блокировки для решения такой комбинации операций.