【问题标题】:Consequence of running multiple concurrent threads?运行多个并发线程的后果?
【发布时间】:2014-05-08 21:17:08
【问题描述】:

在过去的几个小时里,我一直对此感到头疼,所以就这样吧。对于没有多线程经验的人来说,这可能是一个常见的错误?谁知道呢。

在包含的代码中,我实例化了 3 个运行方法 DisplayValues(DateTime Now, int Period) 的线程。调试器在每个 if 语句中停止 3 次,并且对于每个语句,它都会使用正确的值进行方法调用。问题是Console.WriteLine 显示的值不稳定,与调用方式完全不同。

控制台调用DisplayValues() 3次,参数如下,正确: DisplayValues('{5/8/2014 4:20:00 AM}', 0); DisplayValues('{5/8/2014 4:35:00 AM}', 1); DisplayValues('{5/8/2014 4:50:00 AM}', 2);

但输出完全不同:

2014 年 5 月 8 日凌晨 4:35:00 期间:0

2014 年 5 月 8 日凌晨 4:50:00 期间:1

2014 年 5 月 8 日上午 4:51:00 时段:2

调试器确认了这一点。由于它是一个控制台应用程序,我认为可能所有方法都是静态的,所以我将DisplayValues() 移动到一个类。然后我以为三个类实例都同名,所以我改了名字。然后我认为它可能是CancellationTokenSource 对象,所以我也删除了它。

不用说,没有线程输出是正确的。

我知道有一个明显的原因,我只是不知道它是什么。

感谢任何帮助。 谢谢。

bool thread0Running = false;
bool thread1Running = false;
bool thread2Running = false;
DateTime DateNow = new DateTime(2014, 5, 8, 4, 0, 0);

while ((!thread0Running || !thread1Running || !thread2Running) && DateNow.Hour == 4)
{
    if ((DateNow.Hour == TaskDateTime.Hour) && (DateNow.Minute == 20))
    {
        thread0Running = true;
        Class myClass0 = new Class();
        new Thread(() => myClass0.DisplayValues(DateNow, 0, cts0.Token)).Start();

    }
    else if ((DateNow.Hour == TaskDateTime.Hour) && (DateNow.Minute == 35))
    {
        thread1Running = true;
        Class myClass1 = new Class();
        new Thread(() => myClass1.DisplayValues(DateNow, 1, cts1.Token)).Start(); 
    }
    else if ((DateNow.Hour == TaskDateTime.Hour) && (DateNow.Minute == 50))
    {
        thread2Running = true;
        Class myClass2 = new Class();
        new Thread(() => myClass2.DisplayValues(DateNow, 2, cts2.Token)).Start();
    }
    DateNow = DateNow.AddMinutes(1);
}
public void DisplayValues(DateTime Now, int Period, Object obj)
{
        Console.WriteLine(Now.ToString() + " Period: " + Period.ToString());
}

【问题讨论】:

  • DateNow = DateNow.AddMinutes(1); 之前放一个Thread.Sleep(10000),我很确定第2 期的输出将是5/8/2014 4:50:00 AM Period: 2 - 试试看,然后告诉我。
  • 添加Thread.Sleep(1000) 并离开DateNow.AddMinutes(1) 将需要很长时间,所以我添加了 Sleep 并更改为 AddMinutes(15)。输出是正确的。然后我删除了 Sleep [kept AddMinutes(15)] 并且输出仍然正确。
  • 显然,它与分钟增量有关。不幸的是,我需要更改为DateNow = DateTime.Now,问题将继续存在。我不能让它保持原样。

标签: c# multithreading .net-4.0


【解决方案1】:

Thread.Start 并不意味着线程立即开始运行,它使操作系统将当前实例的状态更改为 ThreadState.Running。一旦线程处于 ThreadState.Running 状态,操作系统可以调度它执行,但这并不意味着首先创建的线程会先执行。这就是问题的原因。

如果你想让3个线程依次运行,你应该研究线程同步。

【讨论】:

  • 再次感谢。即使我将使用局部变量,在这种情况下实现线程同步有多困难?
  • 对于您的情况,您可以只使用一个标志来标记该线程已启动,并将该标志传递给该线程,该线程将在退出时将该标志标记为已完成。其他线程会检查这个标志,看是否可以启动
【解决方案2】:

正如其他人已经指出的那样, Console.WriteLine 可能比变量的增加慢。解决此问题的另一种方法是使用线程局部变量。它们不应受到其他线程更改的影响。对于 C#,我找到了这个链接:http://msdn.microsoft.com/en-us/library/dd642243(v=vs.110).aspx

这种方法的优点是,你不必像线程那样维护那么多的变量,线程的工作可以同时完成。

祝你好运!

【讨论】:

    【解决方案3】:

    我认为原因是主线程在更改DateNow 的同时其他线程读取了相同的值。

    考虑一下: 其中一个条件为真 - 因此创建了一个新线程,但您无法控制该线程何时计划运行。 所以与此同时 - 你的主线程改变了DateNow...所以.. 当新创建的线程实际运行时 - 他看到并打印的值 - 不同于通过条件的“理智”值.. .

    考虑一下这个更奇怪的情况: C# 在编写 32 位或更少的变量时提供原子性

    原子性意味着(以非常笼统和不准确的方式)操作不能在中间中断...... 所有线程都获得一些 CPU,然后操作系统停止它,并安排另一个线程运行.. 一段时间后 - 操作系统将再次安排我们的线程,它将从停止的地方继续。 原子操作不能在中间停止……它要么还没有开始,要么已经完成。

    但 DateTime 实际上是 64 位。这意味着操作系统可以中断您的主线程 - 在写入其新值的过程中。这意味着直到再次调度主线程 - DateNow 将有一些奇怪的、不一致的值 - 同时任何其他线程都可以读取该值。

    【讨论】:

    • 可能是这样,但通话已经开始。它没有接受我在调用它时传递的参数?
    • 您作为ThreadStart 委托(Thread 构造函数的参数)提供了一个 lambda - 在运行时 - 采用 DateNow(是的 - 相同的 @ 987654330@... 不是副本)并将其发送到DisplayValue(而DisplayValue 是获得它的副本的那个)。但是那个 lambda 只会在线程启动时运行(将来的某个时间)
    • 再次感谢。即使我将使用局部变量,在这种情况下实现线程同步有多困难?
    • 线程同步的解决方案有很多,甚至有些是为了避免需要同步,各有优缺点。异步编程是一个庞大而复杂的编程领域(可能是最复杂的领域之一)。一个简单的lock 围绕DateNow 可以解决它。使用十几种同步机制中的一种也会有所帮助……我什至可以说,在 C# 中使用线程和异步有很多更高级的工具——尤其是从 .Net 4.0 开始。所以..这里没有单一的简单解决方案适合所有。
    • 例如,如果你检查这个article,你会发现.Net 中没有一个异步模型——不要使用Thread 类......它只是水平太低。检查this...我希望它能教你基础知识。
    【解决方案4】:

    由于您对线程函数使用 lambda 表达式,因此直到线程执行开始后的某个时间才会复制 DateNow 的值。由于线程之间没有同步,这是完全不可预测的。在您开始创建第二个线程并显示 DateNow 的当前值 (4:35) 之前,第一个线程 (Period 0) 可能不会获得任何 cpu。周期 1 也是如此,然后在增加 1 分钟后再次进入循环后,周期 2 终于开始运行。像这样切换每个 lambda 以使用自己的变量:

    if ((DateNow.Hour == TaskDateTime.Hour) && (DateNow.Minute == 20))
    {
        DateTime DateNow0 = DateNow; // DAteTime is a struct so this is a value copy
        thread0Running = true;
        Class myClass0 = new Class();
        new Thread(() => myClass0.DisplayValues(DateNow0, 0, cts0.Token)).Start();
    
    }
    

    在其他 2 个块中使用 DateNow1 和 DateNow2。通过这种更改,我认为您将获得您期望的输出。 DateNow 的当前值的副本现在发生在主执行线程中的可预测位置。但是,由于无法保证三个线程将按照它们创建的顺序运行,因此顺序可能仍然无法正确输出。

    【讨论】:

    • DateTime 是一个结构体,所以是按值复制的。在线程调用时,DateNow 的当前值被复制DisplayValues 方法的堆栈中。
    • @DavidHaney DateNow 不在堆栈中,因为它是闭包的一部分。只有在线程启动后(发生调用Thread对象的Start方法),在新线程中-DateNow的值被复制到堆栈以供调用DisplayValues - 但此时,新线程已经在与主线程(以及任何其他新创建的线程 - 但不会更改 DateNow 的值)竞争。
    • @ShlomiBorovitz 你是对的:stackoverflow.com/a/1982185/2420979 - 闭包捕获变量,而不是。最初我没有注意到关闭。我的休息日。
    • 好的,在这种情况下我的解释有点不对劲,我会修复它,在 lambda 执行开始时复制该值,这仍然是不可预测的。但是,我认为我的代码更改仍会产生所需的输出,因为值副本已移动到可预测的位置。
    猜你喜欢
    • 1970-01-01
    • 2013-08-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-03-08
    • 2011-07-27
    • 2020-11-10
    相关资源
    最近更新 更多