C#在同步上下文中运行异步任务

时间:2016-03-10 10:38:13

标签: c# async-await

我没有太多C#异步经验。

任务 - 从互联网加载位图。之前,我只是逐个加载它们,同步。将它们加载到异步中可以更快地获得结果。在下面,我做了两个例子,我如何获得单张图片 - GetImageGetImageAsync。对于图片列表,我会使用LoadImagesLoadImages2

LoadImages将在异步中运行同步函数(所有同时(?)),LoadImages2将在异步中运行异步函数并生成相同的结果(?)。 我不完全理解的事情 - GetImageAsync await request.GetResponseAsync()。我真的需要它吗?做同样的事情是一种“更好”的方式吗? LoadImagesLoadImages2之间是否存在任何差异?

目前,我正在考虑选择GetImageLoadImages选项。另外,我不想用async Task装饰每个函数,我只需要在异步中加载这些图像。

public Bitmap GetImage(string url)
{
    HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;
    using (WebResponse response = request.GetResponse())
    using (Stream responseStream = response.GetResponseStream())
        return new Bitmap(responseStream);
}

public async Task<Bitmap> GetImageAsync(string url)
{
    HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;
    using (WebResponse response = await request.GetResponseAsync())
    using (Stream responseStream = response.GetResponseStream())
        return new Bitmap(responseStream);
}

private Dictionary<string, Bitmap> LoadImages(List<string> urls)
{
    Dictionary<string, Bitmap> images = new Dictionary<string, Bitmap>();
    Task.WaitAll(urls.Select(url => 
        Task.Run(() =>
        {
            images.Add(url, GetImage(url));
        })).ToArray());
    return images;
}

private Dictionary<string, Bitmap> LoadImages2(List<string> urls)
{
    Dictionary<string, Bitmap> images = new Dictionary<string, Bitmap>();
    Task.WhenAll(urls.Select(url =>
        Task.Run(async () =>
        {
            images.Add(url, await GetImageAsync(url));
        })));
    return images;
}

2 个答案:

答案 0 :(得分:4)

这里的术语和技术选择存在一些混淆。

  

之前,我只是逐个加载它们,同步。将它们加载到异步中可以更快地获得结果。

您的意思是串行并发,而不是同步 async 。串行是一次一个,并发同时是多个事情。同步代码可以是串行或并发的,异步代码可以是串行或并发的。

其次,并发并行Task.Run是一种并行形式,它是通过向问题添加线程来实现并发的一种方法。 Asynchrony 是一种通过释放线程来实现并发的方法。

LoadImages是使用并行与同步代码的示例。这种方法的优点是它使顶级方法保持同步,因此所有调用代码都不得更改。缺点是它在资源使用方面浪费,而且不适合下面发生的事情(I / O绑定代码更自然地由异步API表示)。

LoadImages2是并行和异步代码的混合,有点令人困惑。没有线程(即Task.Run)更容易表示异步并发。返回值也更自然,而不是将集合更新为副作用。所以,像这样:

private async Task<Dictionary<string, Bitmap>> LoadImagesAsync(List<string> urls)
{
  Bitmap[] result = await Task.WhenAll(urls.Select(url => GetImageAsync(url)));

  return Enumerable.Range(0, urls.Length).ToDictionary(i => urls[i], i => result[i]);
}

P.S。如果您决定使用(同步)LoadImages,则需要修复竞争条件,其中各种并行线程都将尝试更新字典而不锁定它。

答案 1 :(得分:0)

由于你坚持同步包装你的电话,你可以尝试一下

private Dictionary<string, Bitmap> LoadImages(List<string> urls)
{
    var result = new Dictionary<string, Bitmap>();
    // start tasks, associate each task with its url
    var tasks = urls.Select(x => new { url = x, imgTask = GetImageAsync(x) });
    // wait for all tasks to complete
    Task.WhenAll(tasks.Select(x => x.imgTask)).Wait();
    // transform the task results into your desired result format
    foreach (var item in tasks)
    {
        result.Add(item.url, item.imgTask.Result);
    }
    return result;
}

但是,我并非100%确信Task.WhenAll(...).Wait()构造在所有情况下都完全没有死锁。避免死锁是同步和异步代码之间切换的棘手问题。如Stephan Cleary建议的那样,最好使LoadImages异步。这是一个常见的观察,即异步代码往往会“感染”您的同步代码,并且您必须最终编码异步。