0

我有一个简单的控制台应用程序,我想在其中循环调用许多 Url 并将结果放入数据库表中。我正在使用 .Net 4.5 并使用异步 i/o 来获取 URL 数据。这是我正在做的事情的简化版本。除数据库操作外,所有方法都是异步的。大家看到这个有什么问题吗?有没有更好的优化方法?

   private async Task Run(){
        var items = repo.GetItems(); // sync method to get list from database
        var tasks = new List<Task>();

        // add each call to task list and process result as it becomes available 
        // rather than waiting for all downloads
        foreach(Item item in items){
            tasks.Add(GetFromWeb(item.url).ContinueWith(response => { AddToDatabase(response.Result);}));
        }
        await Task.WhenAll(tasks); // wait for all tasks to complete.
    }

    private async Task<string> GetFromWeb(url) {
       HttpResponseMessage response = await GetAsync(url);
       return await response.Content.ReadAsStringAsync();
    }

    private void AddToDatabase(string item){
        // add data to database.
    }
4

3 回答 3

1

您的解决方案非常正确,只有两个小错误(都导致编译器错误)。首先,您不调用ContinueWith的结果List.Add,您需要在任务上调用 continue ,然后将延续添加到您的列表中,只需移动括号即可解决。您还需要调用Result.reponse Task

这是有两个小改动的部分:

tasks.Add(GetFromWeb(item.url)
    .ContinueWith(response => { AddToDatabase(response.Result);}));

另一种选择是利用一种方法,该方法采用一系列任务并按照完成的顺序对它们进行排序。这是我对这种方法的实现:

public static IEnumerable<Task<T>> Order<T>(this IEnumerable<Task<T>> tasks)
{
    var taskList = tasks.ToList();

    var taskSources = new BlockingCollection<TaskCompletionSource<T>>();

    var taskSourceList = new List<TaskCompletionSource<T>>(taskList.Count);
    foreach (var task in taskList)
    {
        var newSource = new TaskCompletionSource<T>();
        taskSources.Add(newSource);
        taskSourceList.Add(newSource);

        task.ContinueWith(t =>
        {
            var source = taskSources.Take();

            if (t.IsCanceled)
                source.TrySetCanceled();
            else if (t.IsFaulted)
                source.TrySetException(t.Exception.InnerExceptions);
            else if (t.IsCompleted)
                source.TrySetResult(t.Result);
        }, CancellationToken.None, TaskContinuationOptions.PreferFairness, TaskScheduler.Default);
    }

    return taskSourceList.Select(tcs => tcs.Task);
}

使用这个你的代码可以变成:

private async Task Run()
{
    IEnumerable<Item> items = repo.GetItems(); // sync method to get list from database

    foreach (var task in items.Select(item => GetFromWeb(item.url))
        .Order())
    {
        await task.ConfigureAwait(false);
        AddToDatabase(task.Result);
    }
}
于 2013-03-06T18:19:57.870 回答
1

您的解决方案是可以接受的。但是您应该查看TPL Dataflow,它允许您设置数据流“网格”(或“管道”),然后将数据推入其中。

对于这么简单的问题,Dataflow 除了摆脱ContinueWith(我总是觉得手动延续很尴尬)之外不会真正添加太多。但是,如果您计划在将来添加更多步骤或更改数据流,那么您应该考虑使用 Dataflow。

于 2013-03-06T18:08:39.217 回答
0

尽管我也会用 Rx 解决方案戴上帽子

using System.Reactive;
using System.Reactive.Linq;
private Task Run()
{
    var fromWebObservable = from item in repo.GetItems.ToObservable(Scheduler.Default)
                            select GetFromWeb(item.url);

    fromWebObservable
                    .Select(async x => await x)
        .Do(AddToDatabase)
        .ToTask();

}
于 2013-03-06T18:51:51.893 回答