【问题标题】:Race condition during thread start?线程启动期间的竞争条件?
【发布时间】:2010-03-18 09:26:28
【问题描述】:

我正在运行以下代码来启动我的线程,但它们没有按预期启动。出于某种原因,一些线程以相同的对象开始(有些甚至不开始)。如果我尝试调试,它们就可以正常启动(我单击 F10 单步执行代码会增加额外的延迟)。

这些是我的表单应用程序中的功能:

private void startWorkerThreads()
{
    int numThreads = config.getAllItems().Count;
    int i = 0;

    foreach (ConfigurationItem tmpItem in config.getAllItems())
    {
        i++;
        var t = new Thread(() => WorkerThread(tmpItem, i));
        t.Start();
        //return t;
    }
}

private void WorkerThread(ConfigurationItem cfgItem, int mul) 
{
    for (int i = 0; i < 100; i++)
    {
        Thread.Sleep(10*mul);
    }
    this.Invoke((ThreadStart)delegate()
    {
        this.textBox1.Text += "Thread " + cfgItem.name + " Complete!\r\n";
        this.textBox1.SelectionStart = textBox1.Text.Length;
        this.textBox1.ScrollToCaret();
    });
}

谁能帮帮我?

【问题讨论】:

  • 与其自己创建所有线程,这是一项相当昂贵的操作,不如使用线程池。
  • 在多线程应用程序中...调试无济于事...它会给您带来与实时场景不同的结果...使用日志/打印语句...

标签: c# windows multithreading forms race-condition


【解决方案1】:

Starting 线程并没有真正启动线程。相反,它安排它执行。 IE。在某些时候,它会在预定时运行。调度线程是一个复杂的话题和操作系统的实现细节,所以你的代码不应该期望一定的调度。

您还在 lambda 中捕获变量。请参阅this post(有一个关于捕获变量的部分)以了解与这样做相关的问题。

【讨论】:

  • 不,这是给定的,但我确实希望能够发送一个对象并在线程中提供正确的对象...?...
【解决方案2】:

你只是遇到了(被称为)lambda 错误。

您直接从 foreach 循环中提供 ConfigurationItem。这导致了这样一个事实,即您的所有线程都获得相同的项目(最后一个)。

要使其工作,您必须为每个项目创建一个引用并将其应用于每个线程:

foreach (ConfigurationItem tmpItem in config.getAllItems())
{
        i++;
        var currentI = i;
        var currentItem = tmpItem;
        var t = new Thread(() => WorkerThread(currentItem, currentI));
        t.Start();
        //return t;
}

您还应该考虑使用线程池。

【讨论】:

  • 我同意这个诊断 ;-) 但是您必须对“i”变量应用相同的修复。看看我的答案并考虑定义一个“ThreadStartData”类或类似的东西
  • 请参阅“关闭被认为有害的循环变量”:blogs.msdn.com/ericlippert/archive/2009/11/12/…
  • @Seb:你是对的。我只是没有看到i。也为这个变量更新了我的答案。
  • @user296353:您最好将其重构为 Seb 提出的代码,而不是这个解决方案。
【解决方案3】:

问题似乎在那里:() =&gt; WorkerThread(tmpItem, i)

我不习惯Func&lt;&gt;,但它似乎在 .NET 2.0 中像匿名委托一样工作。因此,您可能引用了WorkerThread() 方法的参数。因此,它们的值稍后会被检索(当线程实际运行时)。

在这种情况下,您可能已经处于主线程的下一次迭代中...

试试这个:

var t = new Thread(new ParametrizedThreadStart(WorkerThread));
t.Start(new { ConfigurationItem = tmpItem, Index = i } );

[编辑] 其他实现。如果您将来需要将新参数传递给线程,则更加灵活。

private void startWorkerThreads()
{
    int numThreads = config.getAllItems().Count;
    int i = 0;

    foreach (ConfigurationItem tmpItem in config.getAllItems())
    {
            i++;
            var wt = new WorkerThread(tmpItem, i);
            wt.Start();
            //return t;
    }
}
private class WorkerThread
{
    private ConfigurationItem _cfgItem;
    private int _mul;
    private Thread _thread;
    public WorkerThread(ConfigurationItem cfgItem, int mul) {
        _cfgItem = cfgItem;
        _mul = mul;
    }
    public void Start()
    {
        _thread = new Thread(Run);
        _thread.Start();
    }
    private void Run()
    {
        for (int i = 0; i < 100; i++)
        {
            Thread.Sleep(10 * _mul);
        }
        this.Invoke((ThreadStart)delegate()
        {
            this.textBox1.Text += "Thread " + _cfgItem.name + " Complete!\r\n";
            this.textBox1.SelectionStart = textBox1.Text.Length;
            this.textBox1.ScrollToCaret();
        });
    }
}

【讨论】:

  • 如果使用匿名类作为参数,WorkerThread的签名怎么写?
  • ParametrizedThreadStart 委托有一个对象作为唯一参数,抱歉。但是您可以定义一个包含线程数据和 Start() 方法的类。我将提供另一个代码示例。
  • @Seb:到目前为止一切顺利,但您遇到了同样的问题,因为在您的 foreach 循环中您没有使用 itmpItem 的副本。
  • @Oliver :否,因为通过避免使用 Func 和匿名方法,所有参数都在 WorkerThread 类中正确初始化。初始情况下的引用问题完全是由于匿名方法和 Func 的实现:生成的 MSIL 包含对循环变量的引用,而不是复制值和指针。
  • @Oliver:你应该看看#4 yoda.arachsys.com/csharp/teasers.html的段落,这是一个很好的解释:-)
【解决方案4】:

您真的需要手动生成线程(这是一项相当昂贵的任务)吗?您可以尝试切换到 ThreadPool。

【讨论】:

  • 嗯...我去看看 :) 谢谢!
【解决方案5】:

你不能假设线程会按照它们被调用的顺序运行,除非你强制它,并导致它们之间存在依赖关系。

所以真正的问题是 - 你的目标是什么?

【讨论】:

  • 感谢大家的快速回复!这个应用程序应该为用户保存几个配置,然后运行一些应用程序来为多媒体设备生成点唱机。我基本上想要做的是为每个配置组合启动一个线程(生成点唱机需要几个程序),其中一个 configItem 保存每个线程的必要信息。这是我从测试中得到的输出:线程 2 完成!线程 2 完成!线程 4 完成!线程温度完成!线程测试完成!线程测试完成! --- 如您所见,有些线程启动了两次
【解决方案6】:

我认为错误在其他地方。以下是一些帮助您调试的提示:

  1. 为每个线程命名,并显示线程名称而不是配置项名称:

    this.textBox1.Text += "Thread" + Thread.Current.Name +" Complete!\r\n";

  2. 显示config.getAllItems()的内容,可能是某些item同名(重复)

============

以下是有关使用 winform 进行多线程处理的一些附加信息:

  1. 不要直接创建新线程,而是使用线程池:

    ThreadPool.QueueUserWorkItem(state => { WorkerThread(tmpItem, i); });

  2. 如果您真的想创建线程,请使用 this.BeginInvoke 而不是 this.Invoke 您的工作线程将更快完成 => 更少的并发线程 => 更好的全局性能
  3. 不要在循环中调用 Thread.Sleep,只是做一个大睡眠:Thread.Sleep(10*mul*100);

希望对你有所帮助。

【讨论】:

    【解决方案7】:

    谢谢大家!

    我刚刚实现了线程池,它的作用就像一个魅力 - 额外的好处是不会一次产生太多线程。

    我也会看看其他解决方案,但这次围绕线程池将让我不必手动检查具有太多配置的 bozos ;)

    【讨论】:

    • 如果您发现一个(或多个)答案有用,您应该点赞。最后但并非最不重要的一点是,您应该将答案(对您帮助最大)标记为正确。这就是 SO 的工作原理。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-10-12
    • 2021-06-04
    • 2010-11-21
    相关资源
    最近更新 更多