【问题标题】:Threads behaving oddly线程表现异常
【发布时间】:2021-12-14 10:13:47
【问题描述】:

在 Windows 10 上运行的以下 .Net 5 多线程代码应该不确定地计算 _sum,例如 2,4 和 8。但是,_sum 始终为 10,即正确答案。好像已经在_sum 上应用了锁。

谁能解释一下这个问题?

using System;
using System.Threading;

namespace MtAdd
{
    class Program
    {
        private static int _sum = 0; //shared resource  
        
        static void Main(string[] args)
        {
            var threads = new Thread[10];

            //Add 1 to _sum for ten times. 
            for (var i = 0; i < threads.Length; i++)
            {
                threads[i] = new Thread(AddOne);
                threads[i].Start();
            }

            foreach (var t in threads)
            {
                t.Join();
            }
            
            Console.WriteLine("[{0}] sum = {1}", 
                              Thread.CurrentThread.ManagedThreadId, 
                              _sum);
        }

        private static void AddOne()
        {
            Console.WriteLine("[{0}] AddOne called", Thread.CurrentThread.ManagedThreadId);
            _sum++; //critical section
        }
    }
}
[4] AddOne called
[5] AddOne called
[7] AddOne called
[6] AddOne called
[8] AddOne called
[9] AddOne called
[10] AddOne called
[11] AddOne called
[12] AddOne called
[13] AddOne called
[1] sum = 10

【问题讨论】:

  • Protip:一般来说你应该从不需要直接使用Thread。如果您认为需要使用Thread,那么您可能不需要。
  • 线程竞赛错误依赖于时机来做他们的坏事。在这种情况下,两个线程必须在完全相同的时间读取 sum 的值,然后再递增它以演示竞争。这肯定会发生,但是您使用 Console.WriteLine() 使其不太可能发生。这需要一个内部锁来确保两个线程不能同时写入控制台。从而确保这些线程不太可能同时到达 sum++。现在机器变得很忙,不小心延迟了一个线程。去掉Console.WriteLine(),加起来一百万增加几率。
  • 除了@HansPassant 评论之外,线程内完成的事情也非常小,一个线程可以在开始返回之前完成,下一个线程被创建并被启动。可能在线程中使用信号量等待并在所有线程启动并运行时释放它以增加可能性。
  • 如果你将AddOne() 的实现改为循环,你会发现它出错了:for (int i = 0; i &lt; 100000; ++i) _sum++;
  • @Llama 啊,是的,对不起,现在是凌晨 3:30……我该走了

标签: c# .net windows multithreading


【解决方案1】:

三件事增加得到“错误”答案的概率:

  • 摆脱Console.WriteLine:见cmets中@Hans的解释

  • 增加并发:即如果10个不够,试试1_000_000个线程

  • 使用Task.Run(AddOne):它将在thread pool 中运行AddOne。从这里启动一个线程只需要很少的运行时间,因为 CLR 保留了一个已经启动的线程池。而new Thread(action).Start 实际创建一个新线程可能需要大约 100 毫秒。

下面的代码在我的计算机上打印 982266 而不是 1000000。试穿你的,告诉我们:

    var tasks = new Task[1_000_000];

    for (var i = 0; i < tasks.Length; i++)
        tasks[i] = Task.Run(AddOne);

    Task.WaitAll(tasks);

    Console.WriteLine(_sum);

【讨论】:

  • “尝试 1_000_000 个线程”var tasks = new Task[1_000_000];
  • 确实,任务不一定由线程支持。但是以 Task.Run 启动的任务是由线程池中的一个线程支持的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-11-06
  • 1970-01-01
  • 2011-12-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多