如何跟踪循环中完成的异步任务数量?

时间:2013-04-01 14:43:13

标签: c# multithreading c#-4.0 task-parallel-library task

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

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

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

Exporting A
Exporting B
Exporting C
Exporting D
Exporting E
Finished 1 / 5
Finished 2 / 5
Finished 3 / 5
Finished 4 / 5
Finished 5 / 5

但我得到这样的输出

Exporting A
Exporting B
Exporting C
Exporting D
Exporting E
Finished 1 / 5
Finished 2 / 5
Finished 5 / 5
Finished 4 / 5
Finished 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输出中向后显示。

在循环中完成多少异步任务的计数的正确方法是什么,以便不会出现此问题?

2 个答案:

答案 0 :(得分: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());
        }
    })
);

答案 1 :(得分:5)

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

  1. 将计数器推到堆栈
  2. 将1推送到堆叠
  3. 在堆栈上添加值
  4. 存入计数器
  5. 由于多个线程正在执行此语句,因此可以通过完成上述序列来中断其他线程。这会导致不正确的值以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());
    }
    

    请注意,因为现在counter的增量发生在锁内,所以不再需要使用Interlocked.Increment