线程启动期间的竞态条件?

3
我正在运行以下代码来启动我的线程,但它们没有按预期启动。由于某些原因,一些线程使用相同的对象启动(有些甚至根本不启动)。如果我尝试调试,它们可以正常启动(我通过单击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();
    });
}

有人能帮我吗?


不要自己创建所有线程,这是一项相当昂贵的操作,您可能希望使用线程池。 - Ronald Wildenberg
1
在多线程应用程序中,调试无法帮助您解决问题。它会给出与实时场景不同的结果。请使用日志/打印语句。 - Mihir Mehta
7个回答

2
你遇到了所谓的lambda错误。
你直接从for循环中提供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;
}

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


2
我同意这个诊断;-) 但是你必须对“i”变量应用相同的修复。看看我的答案,考虑定义一个“ThreadStartData”类或类似的东西。 - Seb
2
请参阅“循环变量的闭包被认为是有害的”:http://blogs.msdn.com/ericlippert/archive/2009/11/12/closing-over-the-loop-variable-considered-harmful.aspx - LukeH
@Seb:你说得对。我刚刚没注意到 i。我也为这个变量更新了我的答案。 - Oliver
@user296353:与其使用这个解决方案,你最好将代码重构为Seb提出的代码。 - Oliver

2

启动一个线程并不会真正地启动该线程,而是将其安排为执行。也就是说,在某个时间点上,当它被调度时,它将会运行。调度线程是一个复杂的主题,是操作系统的实现细节,因此您的代码不应该期望有特定的调度。

你还在lambda中捕获了变量。请参见这篇文章(其中有关于捕获变量的部分)以了解相关问题。


不,这是理所当然的,但我确实希望能够发送一个对象并在线程中使用正确的对象...?... - user296353

1
问题似乎在这里:() => WorkerThread(tmpItem, i) 我不太熟悉Func<>,但它似乎像.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”的签名? - Oliver
一个ParametrizedThreadStart委托只有一个对象作为唯一参数,很抱歉。但是您可以定义一个包含线程数据和Start()方法的类。我会提供另一个代码示例。 - Seb
@Seb:到目前为止还不错,但是你遇到了同样的问题,因为在你的foreach循环中没有使用itmpItem的副本。 - Oliver
@Oliver:不是因为避免使用Func<>和匿名方法,所有参数都在WorkerThread类中正确初始化吗?初始情况下的引用问题完全是由于匿名方法和Func<>的实现造成的:生成的MSIL包含对循环变量的引用,而不是复制值和指针。 - Seb
@Oliver:你应该看一下第4段http://www.yoda.arachsys.com/csharp/teasers.html,这是一个很好的解释 :-) - Seb
@Seb:是的,你说得对。我也知道如何防止这种情况(或利用它们)。但在发表评论之前,我只是没有仔细阅读你的代码。 - Oliver

0
你真的需要手动创建线程吗(这是一项相当昂贵的任务)?你可以尝试切换到线程池。

嗯...我会看一下的 :) 谢谢! - user296353

0

除非你强制执行并在它们之间创建依赖关系,否则不能假设线程将按照被调用的相同顺序运行。

因此,真正的问题是 - 你的目标是什么?


谢谢大家的快速回复!这个应用程序应该为用户保存多个配置文件,然后运行一些应用程序来为多媒体设备生成点唱机。我想要做的基本上是为每个配置组合启动一个线程(生成点唱机需要几个程序),其中configItem保存每个线程所需的必要信息。

这是我从测试中得到的输出: Thread 2 Complete! Thread 2 Complete! Thread 4 Complete! Thread temp Complete! Thread test Complete! Thread test Complete!

正如您所看到的,有些线程会启动两次。
- user296353

0

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

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

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

  2. 显示config.getAllItems()的内容,可能有一些项目具有相同的名称(重复)

===========

以下是关于使用WinForms进行多线程编程的一些额外信息:

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

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

  2. 如果你真的想要创建自己的线程,请使用this.BeginInvoke而不是this.Invoke,这样你的工作线程将更快地完成,从而减少并发线程,提高全局性能。
  3. 不要在循环中调用Thread.Sleep,只需进行大的休眠:Thread.Sleep(10*mul*100);

希望这些信息能对你有所帮助。


0

感谢大家!

我刚刚实现了线程池,效果非常好 - 而且还有一个额外的好处,就是不会一次性生成太多线程。

我也会看看其他解决方案,但这次使用线程池可以避免我手动检查那些配置过多的笨蛋 ;)


1
如果你发现某个(或多个)答案有用,应该给它们点赞。最后但并非最不重要的是,你应该将一个(对你最有帮助的)答案标记为正确答案。这就是 SO 的工作方式。 - Oliver

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接