Interlocked.Increment не потокобезопасный?

1

Я обнаружил ошибку компилятора только в одной строке кода:

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

  • 12
    Инкремент и & являются двумя операциями, поэтому нет причин, по которым комбинация должна быть атомарной. Используйте блокировки для решения такой комбинации операций.
  • 0
    Ваш вопрос получил несколько близких голосов. Некоторые отзывы: Держите это очень целенаправленным. Скажите: а) что вы пытаетесь сделать, б) как вы это делаете, в) что вы ожидали, г) что на самом деле произошло. Если есть вероятность, что вы считаете, что проблема в вашем коде, то сначала сосредоточьтесь на своем коде. Это нормально, чтобы включить ваши предположения, но это должно быть оставлено до конца, чтобы не отвлекать внимание от проблемы. Если вы считаете, что в CLR есть ошибка, тогда сосредоточьтесь на этом - не включайте что-либо для проверки вашего кода - включите метод для проверки кода CLR и машинный код, который он генерирует.
Показать ещё 4 комментария
Теги:
multithreading
interlocked

3 ответа

9
Лучший ответ

Это не взаимосвязано, что неправильно. Также нет состояния гонки. Это не имеет никакого отношения к многопоточности.

  • Interlocked.Increment(ref variable) & Mask

Нет никаких условий гонки и нет проблем с атомарностью, как это предполагалось. 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 до нескольких тысяч.

Подводя итог: работа с блокировкой и маскировкой. Ваш тест испорчен и, возможно, часть вашего кода основана на неверных предположениях.

  • 2
    Привет Алоис. Наконец кто-то, кто на самом деле читает то, что я написал. Вы можете быть на что-то, потому что, когда я делаю "const int maxIncrementModTest = 2 * maxThreadIndexIncrementModTest;", который не позволяет "%" маскировать что-либо, ошибки не возникает. Спасибо за подсказку. Мне нужно больше времени, чтобы исследовать это.
  • 1
    Вы правы, мой тест был неверным. При поиске следующего числа он всегда начинал искать нить 0, затем 1, 2, пока не нашел ожидаемое число. Когда mainIndexIncrementTest перезапустился с 0, должна была возникнуть ситуация, когда правильное число было бы в более высоком потоке, но нулевой поток имел случайно такое же число. Поэтому я написал другой тест, который просто подсчитывает каждое возвращаемое число mainIndexIncrementTest. Все происходило одинаковое количество раз, то есть ни один не был пропущен или использован дважды. Я принял твой ответ.
Показать ещё 1 комментарий
8

Будьте уверены, 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, поэтому вам не придется беспокоиться о потоке, просматривающем дублирующий индекс.

  • 1
    Ваше предположение «Вы проверяете, что каждый поток видел каждый индекс ровно один раз», совершенно НЕПРАВИЛЬНО! Если вы посмотрите на код теста, который я разместил, тест проверяет, что каждое число используется только один раз во всех потоках. Тест достаточно умен, чтобы определить, вернется ли счетчик к 0.
3

Ошибок нет.

Легко видеть, что приращение является атомарным, поскольку оно выполняется как единая машинная команда при смещении 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 прекрасна, но загрузка и хранение в память зависит от того, что эти значения памяти являются стабильными и в многопоточной среде, которые могут быть не такими.

  • 0
    Если я напишу код в 2 строки, очевидно, что только первая строка должна быть атомарной. nextIncrement является локальной переменной и не может быть замечен другими потоками. Но это генерирует ту же ошибку. int nextIncrement = Interlocked.Increment (ref mainIndexIncrementModTest); indexes [testThreadIndex ++] = nextIncrement & maskIncrementModTest;
  • 3
    Операция является атомарной, если она генерирует одну машинную инструкцию. Сколько строк кода вы написали несущественно.
Показать ещё 3 комментария

Ещё вопросы

Сообщество Overcoder
Наверх
Меню