【问题标题】:C# running async tasks in sync contextC# 在同步上下文中运行异步任务
【发布时间】:2016-03-10 23:42:49
【问题描述】:

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

任务 - 从 Internet 加载位图。以前,我只是一个一个地同步加载它们。异步加载它们会更快地产生结果。在下面,我做了两个例子,我如何获得单个图像 - GetImageGetImageAsync。对于图像列表,我会使用LoadImagesLoadImages2

LoadImages 将以异步方式运行同步函数(所有同时 (?)),LoadImages2 将以异步方式运行异步函数并产生相同的结果 (?)。 那件事,我不完全理解 - 在GetImageAsyncawait 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;
}

【问题讨论】:

  • LoadImages2 看起来像是在填充图像之前返回,因为 Task.WhenAll 没有等待,而是返回一个等待的 Task
  • 好的,应该是await Task.WhenAll(...),对吧?但我不能在同步功能中使用await。那么Task.WhenAll(...).RunSynchronously()?这些东西在很多地方都让我的心弯曲。无论如何,我的主要问题是 - 每个图像加载都需要一项任务吗?我已经在LoadImages 中将它们作为任务运行。那么有区别吗?
  • 您不能在没有async 的情况下使用await。一般来说,如果你启动它,建议一直异步 -> 你异步加载图像?那么你的 LoadImages 方法应该是异步的,除非你达到了异步不再是一个选项的地步。

标签: c# async-await


【解决方案1】:

这里的术语和技术选择有些混乱。

以前,我只是一个一个地同步加载它们。以异步方式加载它们会更快地产生结果。

您的意思是 serialconcurrent,而不是 syncasync。串行是一次一件,并发是一次多件事情。同步代码可以串行也可以并发,异步代码可以串行也可以并发。

其次,并发并行Task.Run是并行的一种形式,是通过给问题增加线程来实现并发的一种方式。 异步是一种通过释放线程来实现并发的方法。

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]);
}

附:如果你决定使用(同步的)LoadImages,你需要修复一个竞争条件,即各种并行线程都将尝试更新字典而不锁定它。

【讨论】:

  • 是的!并发加载是正确的术语!你给出的例子,返回Task。这意味着调用它的函数需要是async 或以不同的方式处理任务。我不确定,什么是最好的。 LoadImagesAsync(urls).Result 会好吗?关于LoadImages 比赛问题 - 我应该使用ConcurrentDictionary 吗?
  • @Wish:不,您应该使用await 而不是ResultConcurrentDictionary 是一种方法;你也可以只lock 普通字典。
  • 好吧,那么awaitasync 应该冒泡到最顶端吗?这不方便,这不是我想做的:/
  • @Wish:是的,您的选择是要么改成async 并更改代码,要么使用并行方法并使用更多资源。
【解决方案2】:

由于您坚持同步包装您的通话,您可以尝试类似

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 设为异步会更好。这是一个常见的观察,异步代码往往会“感染”您的同步代码,并且您最终必须一直编写异步代码。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-19
    • 2021-05-07
    • 1970-01-01
    相关资源
    最近更新 更多