Interlocked.Increment不是线程安全吗?

时间:2014-06-22 05:24:39

标签: c# multithreading interlocked

我在一行代码中发现了编译器错误:

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

定义如下:

static int messagesIndex = -1;
public const int MaxMessages = 0x10000;
const int indexMask = MaxMessages-1;
任何其他代码行都不会访问

messagesIndex

如果我在一个线程中运行该代码数十亿次,我就不会有任何错误。

如果我在多个线程上运行上面的行,我会得到两次相同的数字,并且每隔一千倍就跳过另一个数字。

以下行我在6个线程上运行了数十亿次而没有出现错误:

int thisIndex = Interlocked.Increment(ref messagesIndex);

结论和问题

Interlocked.Increment()似乎按预期工作,但Interlocked.Increment()& indexMask不会: - (

我知道如何让它始终正常工作,而不仅仅是99.99%?

我尝试将Interlocked.Increment(ref messagesIndex)分配给易变的整数变量,并对该变量执行"& indexMask"操作:

[ThreadStatic]
volatile static int nextIncrement;

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

它会导致同样的问题,就像我在一行中写一样。

拆卸

也许有人可以从反汇编中猜出编译器引入了什么问题:

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 

错误检测

为了发现这个bug,我在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 !!!!这绝不应该发生     线程0使用2637
    线程2使用2638
    线程1使用2639
    任何线程都不使用2640!这是测试检测到的错误     线程0使用2641
    线程3使用2642

3 个答案:

答案 0 :(得分:9)

这不是错误的联锁。也没有竞争条件。它甚至与多线程无关。

  • Interlocked.Increment(ref variable)&amp;面具

没有竞争条件,也没有原子性问题,因为它被推测。 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&amp; 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开始计数到数千的线程。

总结一下:互锁和屏蔽工作。您的测试存在缺陷,可能还有一些代码依赖于无效的假设。

答案 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。

取消屏蔽,每个值都将保持唯一。蒙面,A最终会看到所有0-4999两次,B将看到5000-9999两次。

您没有说明您的最终目标是什么,但更好的选择可能是TLS:

[ThreadStatic]
static int perThreadIndex = -1;

int myIndex = ++perThreadIndex;

使用ThreadStatic属性,每个线程只会看到自己的perThreadIndex私有实例,因此您不必担心线程会看到重复的索引。

答案 2 :(得分:3)

没有错误。

很容易看出增量是原子的,因为它是作为单个机器指令执行的,在第二段代码中的偏移量a0处。同样,and操作不是原子操作,因为它是作为从偏移量b7开始的指令序列执行的。

您可以使用原子库在C ++中执行原子按位操作。您还可以基于原子比较和交换实现更复杂的操作。如果你愿意用C ++编写代码并用C#调用它,这将是一个解决方案(Interop工作得很好)。

如果您需要仅使用C#的解决方案,您仍然可以使用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寄存器上的操作很好,但是从内存加载和存储依赖于那些稳定的内存值以及可能不是这样的多线程环境。