C#在同步上下文中运行异步任务
我没有太多C#异步经验。
任务 – 从互联网加载位图。 之前,我只是逐个加载它们,同步。 将它们加载到异步中可以更快地获得结果。 在下面,我做了两个例子,我如何获得单个图像 – GetImage
和GetImageAsync
。 对于图像列表,我将使用LoadImages
和LoadImages2
。
LoadImages
将在异步中运行同步函数(所有这些都在同一时间(?)), LoadImages2
将在异步中运行异步函数并产生相同的结果(?)。 我不完全理解的事情 – 在GetImageAsync
await request.GetResponseAsync()
。 我真的需要它吗? 做同样的事情是一种“更好”的方式吗? LoadImages
和LoadImages2
之间是否存在任何差异?
目前,我正在考虑选择LoadImages
和LoadImages
选项。 另外,我不想用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 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 LoadImages(List urls) { Dictionary images = new Dictionary(); Task.WaitAll(urls.Select(url => Task.Run(() => { images.Add(url, GetImage(url)); })).ToArray()); return images; } private Dictionary LoadImages2(List urls) { Dictionary images = new Dictionary(); Task.WhenAll(urls.Select(url => Task.Run(async () => { images.Add(url, await GetImageAsync(url)); }))); return images; }
这里的术语和技术选择存在一些混淆。
之前,我只是逐个加载它们,同步。 将它们加载到异步中可以更快地获得结果。
你的意思是串行与并发 ,而不是同步与异步 。 串行是一次一个,并发同时是多个事情。 同步代码可以是串行或并发的,异步代码可以是串行或并发的。
其次, 并发性与并行性 。 Task.Run
是一种并行forms,它是通过向问题添加线程来实现并发的一种方法。 异步是一种通过释放线程来实现并发的方法。
LoadImages
是使用具有同步代码的并行性的示例。 这种方法的优点是它使顶级方法保持同步,因此所有调用代码都不得更改。 缺点是它在资源使用方面是浪费的,并且不适合于下面发生的事情(I / O绑定代码更自然地由异步API表示)。
LoadImages2
是并行和异步代码的混合,有点令人困惑。 没有线程(即Task.Run
)更容易表示异步并发。 返回值更自然,而不是将集合更新为副作用。 所以,像这样:
private async Task> LoadImagesAsync(List urls) { Bitmap[] result = await Task.WhenAll(urls.Select(url => GetImageAsync(url))); return Enumerable.Range(0, urls.Length).ToDictionary(i => urls[i], i => result[i]); }
PS如果您决定使用(同步) LoadImages
,则需要修复竞争条件,其中各种并行线程都将尝试更新字典而不锁定它。
既然你坚持同步包装你的电话,你可以尝试一些方法
private Dictionary LoadImages(List urls) { var result = new Dictionary(); // 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
异步。 这是一个常见的观察结果,即异步代码往往会“感染”您的同步代码,并且您必须最终编写异步代码。