Task.Factory.StartNew(()=> Parallel.ForEach完成事件处理程序

时间:2012-05-26 02:26:13

标签: c# .net task-parallel-library

我想知道某些并行任务何时完成。

我正在使用此代码在网站上制作1500到2000个小型WebClient.DownloadString并使用10秒HttpRequest超时:

Task.Factory.StartNew(() => 
    Parallel.ForEach<string>(myKeywords, new ParallelOptions 
    { MaxDegreeOfParallelism = 5 }, getKey));

有时,查询失败,因此存在异常并且函数永远不会完成,并且每个getKey函数内的UI刷新有时似乎被调用两次,因此我无法准确了解完成了多少任务。我正在计算:UI刷新调用次数/关键字总数,得到100%到250%之间的结果,我永远不知道任务何时完成。我在很多SO讨论中搜索,但没有一个是直接的方法或方法,以满足我的需要。所以我猜Framework 4.0没有提供任何Tasks.AllCompleted事件处理程序或类似的解决方法?

我应该在另一个线程而不是我的UI线程中运行Parallel.Foreach然后添加它吗?

myTasks.WaitAll

[编辑]

临时解决方案是在ArrayList中复制我的字符串列表,然后在每个查询开头的列表中逐个删除每个项目。只要功能运行良好,我就知道所有项目都已处理完毕。

1 个答案:

答案 0 :(得分:1)

Parallel.ForEach is no different than other loops when it comes to handling exceptions。如果抛出异常,那么它将停止处理循环。这可能是你看到百分比差异的原因(我假设你在处理循环时可能正在处理计数)。

此外,您真的不需要Parallel.ForEach因为您在WebClient class上进行的异步调用将阻止等待IO completion(网络响应),他们不受计算限制(Parallel.ForEach在计算限制时要好得多。)

也就是说,您应首先将来电转换为WebClient以使用Task<TResult>。使用event-based asynchronous patterntask-based asynchronous pattern翻译为TaskCompletionSource<TResult> class很简单。

假设您有一系列Uri个实例是由于您调用getKey而产生的,您可以创建一个函数来执行此操作:

static Task<String> DownloadStringAsync(Uri uri)
{
    // Create a WebClient
    var wc = new WebClient();

    // Set up your web client.

    // Create the TaskCompletionSource.
    var tcs = new TaskCompletionSource<string>();

    // Set the event handler on the web client.
    wc.DownloadStringCompleted += (s, e) => {
        // Dispose of the WebClient when done.
        using (wc)
        {
            // Set the task completion source based on the
            // event.
            if (e.Cancelled)
            {
                // Set cancellation.
                tcs.SetCancelled();
                return;
            }

            // Exception?
            if (e.Error != null)
            { 
                // Set exception.
                tcs.SetException(e.Error);
                return;
            }

            // Set result.
            tcs.SetResult(e.Result);
        };

    // Return the task.
    return tcs.Task;
};

注意,上面的内容可以优化使用 one WebClient,这是留给你的练习(假设你的测试表明你需要它)。

从那里,您可以获得一系列Task<string>

// Gotten from myKeywords
IEnumerable<Uri> uris = ...;

// The tasks.
Task<string>[] tasks = uris.Select(DownloadStringAsync).ToArray();

请注意,必须调用ToArray extension method才能开始运行任务。这是为了解决deferred execution。您不必调用ToArray,但必须调用将枚举整个列表并使任务开始运行的内容。

拥有这些Task<string>个实例后,您可以通过调用ContinueWhenAll<TAntecedentResult> method上的TaskFactory class来完成所有这些操作,如下所示:

Task.Factory.ContinueWhenAll(tasks, a => { }).Wait();

完成此操作后,您可以遍历tasks数组并查看Exception和/或Result属性,以查看异常或结果是什么。

如果您要更新用户界面,那么您应该查看拦截对Enumerable.Select的呼叫,即您应该调用Task<TResult>上的ContinueWith<TNewResult> method来执行操作下载完成时,如下:

// The tasks.
Task<string>[] tasks = uris.
    Select(DownloadStringAsync).
    // Select receives a Task<T> here, continue that.
    Select(t => t.ContinueWith(t2 => {
        // Do something here: 
        //   - increment a count
        //   - fire an event
        //   - update the UI
        // Note that you have to take care of synchronization here, so
        // make sure to synchronize access to a count, or serialize calls
        // to the UI thread appropriately with a SynchronizationContext.
        ...

        // Return the result, this ensures that you'll have a Task<string>
        // waiting.
        return t2;
    })).
    ToArray();

这将允许您在事情发生时进行更新。请注意,在上述情况下,如果再次调用Select,您可能需要检查t2的状态并触发其他一些事件,具体取决于您希望错误处理机制的用途。