首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >为什么这个多线程代码有时会打印6呢?

为什么这个多线程代码有时会打印6呢?
EN

Stack Overflow用户
提问于 2015-10-25 22:30:06
回答 2查看 2.6K关注 0票数 31

我正在创建两个线程,并传递给它们一个函数,它执行下面的代码显示,10,000,000次。

大多数情况下,"5“被打印到控制台上。有时是"3“或"4”。很清楚为什么会发生这种事。

然而,它也打印"6“。这怎麽可能?

代码语言:javascript
复制
class Program
{
    private static int _state = 3;

    static void Main(string[] args)
    {
        Thread firstThread = new Thread(Tr);
        Thread secondThread = new Thread(Tr);

        firstThread.Start();
        secondThread.Start();

        firstThread.Join();
        secondThread.Join();

        Console.ReadLine();
    }

    private static void Tr()
    {
        for (int i = 0; i < 10000000; i++)
        {
            if (_state == 3)
            {
                _state++;
                if (_state != 4)
                {
                    Console.Write(_state);
                }
                _state = 3;
            }
        }
    }
}

这是输出:

EN

回答 2

Stack Overflow用户

回答已采纳

发布于 2015-10-25 22:46:30

我想我已经弄清楚了导致这一问题的一系列事件:

线程1进入if (_state == 3)

上下文切换

线程2进入if (_state == 3)

线程2增量状态(state = 4)

上下文切换

线程1将_state读入4

上下文切换

线程2集_state = 3

线程2进入if (_state == 3)

上下文切换

线程1执行_state = 4 + 1

上下文切换

线程2将_state读入5

线程2执行_state = 5 + 1

票数 43
EN

Stack Overflow用户

发布于 2015-10-25 23:25:42

这是一个典型的种族条件。编辑:事实上,有多个种族条件。

如果_state为3,两个线程都可以通过if语句并发地在单个核中进行上下文切换,或者在多个核中同时并行。

这是因为++操作符首先读取_state,然后递增它。在第一次if声明之后,有可能会有足够的时间来读5甚至6。

编辑:如果您将这个示例概括为N个线程,您可能会观察到一个高达3+ N+1的数字。

当线程开始运行时,或者当一个线程刚刚将_state设置为3时,这是正确的。

要避免这种情况,可以在if语句周围使用锁,或者使用Interlocked访问_state,例如if (System.Threading.Interlocked.CompareAndExchange(ref _state, 3, 4) == 3)System.Threading.Interlocked.Exchange(ref _state, 3)

如果您想保持争用条件,您应该使用volatile,否则您将使每个线程在本地看到_state而不需要其他线程的更新。

另外,您可以使用System.Threading.Volatile.ReadSystem.Threading.Volatile.Write,以防您将实现切换为以_state作为变量,而将Tr作为捕获该变量的闭包,因为局部变量不能被声明为volatile (也不可能)。在这种情况下,即使初始化也必须使用易失性写入。

编辑:如果我们通过扩展每次读取来稍微修改代码,那么竞争条件可能会更加明显:

代码语言:javascript
复制
    // Without some sort of memory barrier (volatile, lock, Interlocked.*),
    // a thread is allowed to see _state as if other threads hadn't touched it
    private static volatile int _state = 3;

// ...

        for (int i = 0; i < 10000000; i++)
        {
            int currentState;
            currentState = _state;
            if (currentState == 3)
            {
                // RACE CONDITION: re-read the variable
                currentState = _state;
                currentState = currentState + 1:
                // RACE CONDITION: non-atomic read-modify-write
                _state = currentState;
                
                currentState = _state;
                if (currentState != 4)
                {
                    // RACE CONDITION: re-read the variable
                    currentState = _state;
                    Console.Write(currentState);
                }
                _state = 3;
            }
        }

我在_state可能不同于以前变量read语句假设的地方添加了注释。

这是一个很长的图表,它甚至可以连续打印6次,每个线程一次,就像op发布的图像一样。请记住,线程可能不会同步运行,通常是由于抢占式上下文切换、缓存暂停或核心速度差异(由于省电或临时涡轮速度):

这个类似于最初的,但它使用Volatile类,其中state现在是一个由闭包捕获的变量。易失性访问的数量和顺序变得明显:

代码语言:javascript
复制
    static void Main(string[] args)
    {
        int state = 3;

        ThreadStart tr = () =>
        {
            for (int i = 0; i < 10000000; i++)
            {
                if (Volatile.Read(ref state) == 3)
                {
                    Volatile.Write(ref state, Volatile.Read(state) + 1);
                    if (Volatile.Read(ref state) != 4)
                    {
                        Console.Write(Volatile.Read(ref state));
                    }
                    Volatile.Write(ref state, 3);
                }
            }
        };

        Thread firstThread = new Thread(tr);
        Thread secondThread = new Thread(tr);

        firstThread.Start();
        secondThread.Start();

        firstThread.Join();
        secondThread.Join();

        Console.ReadLine();
    }

一些线程安全的方法:

代码语言:javascript
复制
    private static object _lockObject;

// ...

        // Do not allow concurrency, blocking
        for (int i = 0; i < 10000000; i++)
        {
            lock (_lockObject)
            {
                // original code
            }
        }

        // Do not allow concurrency, non-blocking
        for (int i = 0; i < 10000000; i++)
        {
            bool lockTaken = false;
            try
            {
                Monitor.TryEnter(_lockObject, ref lockTaken);
                if (lockTaken)
                {
                    // original code
                }
            }
            finally
            {
                if (lockTaken) Monitor.Exit(_lockObject);
            }
        }

代码语言:javascript
复制
        // Do not allow concurrency, non-blocking
        for (int i = 0; i < 10000000; i++)
        {
            // Only one thread at a time will succeed in exchanging the value
            try
            {
                int previousState = Interlocked.CompareExchange(ref _state, 4, 3);
                if (previousState == 3)
                {
                    // Allow race condition on purpose (for no reason)
                    int currentState = Interlocked.CompareExchange(ref _state, 0, 0);
                    if (currentState != 4)
                    {
                        // This branch is never taken
                        Console.Write(currentState);
                    }
                }
            }
            finally
            {
                Interlocked.CompareExchange(ref _state, 3, 4);
            }
        }

代码语言:javascript
复制
        // Allow concurrency
        for (int i = 0; i < 10000000; i++)
        {
            // All threads increment the value
            int currentState = Interlocked.Increment(ref _state);
            if (currentState == 4)
            {
                // But still, only one thread at a time enters this branch
                // Allow race condition on purpose (it may actually happen here)
                currentState = Interlocked.CompareExchange(ref _state, 0, 0);
                if (currentState != 4)
                {
                    // This branch might be taken with a maximum value of 3 + N
                    Console.Write(currentState);
                }
            }
            Interlocked.Decrement(ref _state);
        }

这个值有点不同,它使用增量后的_state的最后一个已知值来执行以下操作:

代码语言:javascript
复制
        // Allow concurrency
        for (int i = 0; i < 10000000; i++)
        {
            // All threads increment the value
            int currentState = Interlocked.Increment(ref _state);
            if (currentState != 4)
            {
                // Only the thread that incremented 3 will not take the branch
                // This can happen indefinitely after the first increment for N > 1
                // This branch might be taken with a maximum value of 3 + N
                Console.Write(currentState);
            }
            Interlocked.Decrement(ref _state);
        }

注意,Interlocked.Increment/Interlocked.Decrement示例与lock/MonitorInterlocked.CompareExchange示例不同,不安全,因为没有可靠的方法可以知道增量是否成功。

一种常见的方法是增量,然后在try/finally中减少finally块中的值。然而,)

异步异常可以抛出在意想不到的位置,可能是每个机器指令:ThreadAbortExceptionStackOverflowExceptionOutOfMemoryException

另一种方法是将currentState初始化为低于3的内容,并在finally块中有条件地减少。但是,在Interlocked.Increment返回和分配给结果的currentState之间,可能会出现异步异常,因此即使Interlocked.Increment成功,currentState仍然可以具有初始值。

票数 16
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/33335838

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档