5

我有一些代码循环遍历记录列表,为每个记录启动一个导出任务,并在每次任务完成时将进度计数器增加 1,以便用户知道该过程有多远。

但是根据我的循环时间,我经常看到输出在一个较低的数字之前显示一个较高的数字。

例如,我希望看到这样的输出:

出口 A
出口 B
导出 C
出口 D
出口电子
完成 1 / 5
完成 2 / 5
完成 3 / 5
完成 4 / 5
完成 5 / 5

但相反,我得到这样的输出

出口 A
出口 B
导出 C
出口 D
出口电子
完成 1 / 5
完成 2 / 5
完成 5 / 5
完成 4 / 5
完成 3 / 5

我不希望输出是准确的,因为当我更新/使用它时我没有锁定该值(有时它输出相同的数字两次,或者跳过一个数字),但是我不希望它倒退

我的测试数据集是72个值,相关代码如下:

var tasks = new List<Task>();
int counter = 0;

StatusMessage = string.Format("Exporting 0 / {0}", count);

foreach (var value in myValues)
{
    var valueParam = value;

    // Create async task, start it, and store the task in a list
    // so we can wait for all tasks to finish at the end
    tasks.Add(
        Task.Factory.StartNew(() =>
        {
            Debug.WriteLine("Exporting " + valueParam );

            System.Threading.Thread.Sleep(500);
            counter++;
            StatusMessage = string.Format("Exporting {0} / {1}", counter, count);

            Debug.WriteLine("Finished " + counter.ToString());
        })
    );
}

// Begin async task to wait for all tasks to finish and update output
Task.Factory.StartNew(() =>
{
    Task.WaitAll(tasks.ToArray());
    StatusMessage = "Finished";
});

输出可以在调试语句和StatusMessage输出中向后出现。

计算一个循环中有多少异步任务已完成以便不会发生此问题的正确方法是什么?

4

2 回答 2

7

您会得到混合输出,因为计数器的递增顺序与 Debug.WriteLine(...)方法执行的顺序不同。

要获得一致的进度报告,您可以在任务中引入报告锁

tasks.Add(
    Task.Factory.StartNew(() =>
    {
        Debug.WriteLine("Exporting " + valueParam );

        System.Threading.Thread.Sleep(500);
        lock(progressReportLock)
        {
           counter++;
           StatusMessage = string.Format("Exporting {0} / {1}", counter, count);
           Debug.WriteLine("Finished " + counter.ToString());
        }
    })
);
于 2013-04-01T15:04:31.390 回答
5

在此示例中,counter变量表示多个线程之间的共享状态。在共享状态上使用++运算符是不安全的,并且会给你错误的结果。它基本上归结为以下说明

  1. 将计数器推入堆栈
  2. 将 1 推入堆栈
  3. 在堆栈上添加值
  4. 存入柜台

由于多个线程正在执行此语句,因此一个线程可能会在完成上述序列的过程中中断另一个线程。这将导致不正确的值以counter.

而不是++使用以下语句

Interlocked.Increment(ref counter);

此操作专门设计用于更新可能在多个线程之间共享的状态。联锁将自动发生,不会受到我概述的竞争条件的影响

即使在我建议的修复之后,值的实际无序显示也会遇到类似的问题。增量和显示操作不是原子的,因此一个线程可以在增量和显示之间中断另一个线程。如果您希望操作不会被其他线程中断,那么您将需要使用锁。

object lockTarget = new object();
int counter = 0; 

...

lock (lockTarget) {
  counter++;
  StatusMessage = string.Format("Exporting {0} / {1}", counter, count);
  Debug.WriteLine("Finished " + counter.ToString());
}

请注意,由于counternow 的增量发生在锁内部,因此不再需要使用Interlocked.Increment

于 2013-04-01T15:02:32.157 回答