ThreadPool.QueueUserWorkItem 的意外行为

发布于 2024-10-12 22:48:11 字数 1301 浏览 3 评论 0原文

请检查下面的代码示例:

public class Sample
{
    public int counter { get; set; }
    public string ID;
    public void RunCount()
    {
        for (int i = 0; i < counter; i++)
        {
            Thread.Sleep(1000);

            Console.WriteLine(this.ID + " : " + i.ToString());
        }
    }
}

class Test
{
    static void Main()
    {
        Sample[] arrSample = new Sample[4];

        for (int i = 0; i < arrSample.Length; i++)
        {
            arrSample[i] = new Sample();
            arrSample[i].ID = "Sample-" + i.ToString();
            arrSample[i].counter = 10;
        }

        foreach (Sample s in arrSample)
        {
            ThreadPool.QueueUserWorkItem(callback => s.RunCount());
        }

        Console.ReadKey();
    }

}

此示例的预期输出应该类似于:

Sample-0 : 0 
Sample-1 : 0 
Sample-2 : 0 
Sample-3 : 0 
Sample-0 : 1 
Sample-1 : 1 
Sample-2 : 1 
Sample-3 : 1
.
. 
.

但是,当您运行此代码时,它会显示类似这样的内容:

Sample-3 : 0 
Sample-3 : 0 
Sample-3 : 0 
Sample-3 : 1 
Sample-3 : 1 
Sample-3 : 0 
Sample-3 : 2 
Sample-3 : 2
Sample-3 : 1 
Sample-3 : 1
.
. 
.

我可以理解线程执行的顺序可能不同,因此计数并没有以循环方式增加。但是,我无法理解为什么所有 ID 都显示为 Sample-3,而执行显然是彼此独立的。

不同的对象不是被不同的线程使用吗?

Please check the code sample below:

public class Sample
{
    public int counter { get; set; }
    public string ID;
    public void RunCount()
    {
        for (int i = 0; i < counter; i++)
        {
            Thread.Sleep(1000);

            Console.WriteLine(this.ID + " : " + i.ToString());
        }
    }
}

class Test
{
    static void Main()
    {
        Sample[] arrSample = new Sample[4];

        for (int i = 0; i < arrSample.Length; i++)
        {
            arrSample[i] = new Sample();
            arrSample[i].ID = "Sample-" + i.ToString();
            arrSample[i].counter = 10;
        }

        foreach (Sample s in arrSample)
        {
            ThreadPool.QueueUserWorkItem(callback => s.RunCount());
        }

        Console.ReadKey();
    }

}

The expected output for this sample should be something like :

Sample-0 : 0 
Sample-1 : 0 
Sample-2 : 0 
Sample-3 : 0 
Sample-0 : 1 
Sample-1 : 1 
Sample-2 : 1 
Sample-3 : 1
.
. 
.

However, when you run this code, it would show something like this instead:

Sample-3 : 0 
Sample-3 : 0 
Sample-3 : 0 
Sample-3 : 1 
Sample-3 : 1 
Sample-3 : 0 
Sample-3 : 2 
Sample-3 : 2
Sample-3 : 1 
Sample-3 : 1
.
. 
.

I can understand that the order in which the threads are executing might differ and hence the count isnt increasing in round robin fashion. However, I fail to understand, why all the IDs are being displayed as Sample-3, while the execution is clearly happening independent of each other.

Arent different objects being used with different threads?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

随风而去 2024-10-19 22:48:11

这是旧的改进的闭包问题。您可能需要查看:线程池 - 可能的线程执行顺序问题 对于类似的问题,以及 Eric Lippert 的博客文章 关闭被认为有害的循环变量以了解该问题。

本质上,您获得的 lambda 表达式捕获的是声明 lambda 时的变量 s,而不是变量的 。因此,对变量值所做的后续更改对于委托来说是可见的。将运行 RunCount 方法的 Sample 实例将取决于变量 s 引用的实例(其值)在委托实际执行时

此外,由于委托(编译器实际上重用相同的委托实例)是异步执行的,因此不能保证每次执行时这些值是什么。您当前看到的是,foreach 循环在任何委托调用之前在主线程上完成(这是预料之中的 - 在主线程上安排任务需要时间)线程池)。因此,所有工作项最终都会看到循环变量的“最终”值。但这并不能以任何方式保证;尝试在循环内插入一个合理的持续时间Thread.Sleep,您将看到不同的输出。


通常的解决方法是:

  1. 在循环体内引入另一个变量。
  2. 将该变量分配给循环变量的当前值。
  3. 捕获“复制”变量而不是 lambda 内的循环变量。

    foreach(arrSample 中的示例)
    {
        样本 sCopy = s;
        ThreadPool.QueueUserWorkItem(callback => sCopy.RunCount());
    }
    

现在,每个工作项“拥有”循环变量的特定值。


在这种情况下,另一个选择是通过不捕获任何内容来完全避免该问题:

ThreadPool.QueueUserWorkItem(obj => ((Sample)obj).RunCount(), s);

This is the old modified closure problem. You might want to look at: Threadpools - possible thread execution order problem for a similar question, and Eric Lippert's blog post Closing over the loop variable considered harmful for an understanding of the issue.

Essentially, the lambda expression you've got there is capturing the variable s rather than the value of the variable at the point the lambda is declared. Consequently, subsequent changes made to the value of the variable are visible to the delegate. The instance of Sample on which the RunCount method will run will depend on the instance referred to by the variable s (its value) at the point the delegate actually executes.

Additionally, since the delegate(s) (the compiler actually reuses the same delegate instance) are being asynchronously executed, it isn't guaranteed what these values will be at the point of each execution. What you are currently seeing is that the foreach loop completes on the main-thread before any of the delegate-invocations (to be expected - it takes time to schedule tasks on the thread-pool). So all the work-items end up seing the 'final' value of the loop-variable. But this isn't guaranteed by any means; try inserting a reasonable-duration Thread.Sleep inside the loop, and you will see a different output.


The usual fix is to:

  1. Introduce another variable inside the loop-body.
  2. Assign that variable to the current value of the loop-variable.
  3. Capture the 'copy' variable instead of the loop-variable inside the lambda.

    foreach (Sample s in arrSample)
    {
        Sample sCopy = s;
        ThreadPool.QueueUserWorkItem(callback => sCopy.RunCount());
    }
    

Now each work-item "owns" a particular value of the loop variable.


Another option in this case is to dodge the issue completely by not capturing anything:

ThreadPool.QueueUserWorkItem(obj => ((Sample)obj).RunCount(), s);
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文