C#并发编程之异步编程(线程讨论)

简介: C#并发编程之异步编程(线程讨论)写在前面本篇是异步编程系列的第三篇,本来计划第三篇的内容是介绍异步编程中常用的几个方法,但是前两篇写出来后,身边的朋友总是会有其他问题,所以决定在续写一篇,作为异步编程(一)和异步编程(二)的补充。

C#并发编程之异步编程(线程讨论)
写在前面
本篇是异步编程系列的第三篇,本来计划第三篇的内容是介绍异步编程中常用的几个方法,但是前两篇写出来后,身边的朋友总是会有其他问题,所以决定在续写一篇,作为异步编程(一)和异步编程(二)的补充。

本篇内容主要讨论,在我们的异步代码里,运行的到底是哪个线程,在执行长时间运行操作时线程发生了什么。

Await之前
在一个被async修饰了的异步方法里,如果没有遇到await,你的代码将一直在调用线程上。在UI应用程序里,比如ASP.NET或者WinForm程序里,你的代码会在ASP.NET工作线程或WinForm工作线程上运行。

我们来看一下以下范例

1: public async Task GetResultAsync()
2: {
3: Console.WriteLine();
4:
5: User user = this.GetUserAsync();
6:
7: //call other code
8:
9: return Task.CompletedTask;
10: }
以上范例里,我们在一个异步方法里调用了另一个异步方法,但是我们并没有使用await,这段代码依然在原始调用线程上执行,此时这个方法只是扮演了一个传播异步的作用。

当我们在UI线程上如此编程的时候,代码在UI线程是执行,在没有执行结束之前,页面是没有响应的。所以如果页面长时间没有响应,未必是异步导致的,可能会有其他原因,需要综合考虑,可以借助性能分析器来查看影响系统的原因在哪里。

Await中
代码到达await后,到底是哪一个线程在执行异步操作呢。

我们以ASP.NET为例,对于网络请求之类的操作,此时没有线程在执行异步操作,他们都被阻塞了,正在等待操作完成。但是如果使用了Task.Run,那么执行该任务时就要用到线程池里的线程了。

那么问题来了,我们在编写异步方法的时候,确确实实可以看到这个方法被执行了,肯定有线程执行才行啊。

对的,确实需要线程来执行,这个线程我们把它称之为是IO完成端口线程。此线程等待网络请求完成,同时它在所有网络请求之间共享。当网络请求完成时,操作系统中的中断处理程序会以Job方式添加到IO完成端口的队列中。在请求发起后,响应返回前,它们需要依次由单个IO完成端口处理。

实际上,一般情况下只有少量IO完成端口线程,以充分利用多个CPU核心。需要注意的是,无论当前有多少个请求,我们的线程数量都是固定的。

参考以下运行图

IO

SynchronizationContext
我在异步编程(一)这边文章里,有讲到SynchronizationContext这个类,它是.NET框架提供的类,可以在特定类型的线程中运行代码。

.NET使用各种SynchronizationContext,常见的有ASP.NET、WinForms和WPF使用的UI线程上下文。SynchronizationContext的实例本身并没有特殊的地方,其实例指向的是其子类,具有静态成员,可以用于读取和控制当前的SynchronizationContext。

当前SynchronizationContext是当前线程的属性。在一个特定线程所运行到的任意的地方,都能够获取当前的SynchronizationContext并存储它,并且可以使用SynchronizationContext,在所启动的这个特定线程上运行代码。综上所述,我们并不需要知道代码在哪个线程上启动,只需要使用到SynchronizationContext,我们就可以返回到启动线程。

SynchronizationContext的重要方法是POST,它可以使委托在正确的上下文中运行。

某些SynchronizationContext封装单个线程,如UI线程。有些线程封装了特定类型的线程,例如线程池,但可以选择将委托发送到其中的任何一个线程。有些不会更改代码运行在哪个线程上,而只用于监视,如ASP.NET SynchronizationContext。

到这个地方,我们就需要了解一个问题了。在await之前,我们的代码是在调用线程上运行,那么await之后,恢复方法时到了哪个线程上了?

实际上,大多数情况下,await后的代码也由调用线程运行,尽管调用线程可能在等待期间做了其他事情。C#使用SynchronizationContext来完成此操作。当等待任务完成时,当前的同步上下文被存储为暂停方法的一部分。然后,当方法恢复时,await关键字的基础结构使用POST在捕获的同步上下文上恢复该方法。

既然有大多数情况,那么肯定也有小众情况吧,以下情况可以在不同的线程上运行

SynchronizationContext具有多个线程,如线程池
SynchronizationContext不是真正切换线程的上下文
到达等待时,没有当前的同步上下文,例如在控制台应用程序中。
将任务配置为不使用同步上下文来恢复
注意:

对于UI应用程序来说,在同一线程上恢复是最重要的,我们等待之后安全的操作UI。

解析异步操作
以WinForm为例,我们设计一个按钮,用于下载我们喜欢的小图标。用户点击按钮之后,UI线程启动,并会执行响应的操作,以下图片展示了一个异步操作的流程,以及期间UI线程与IO线程是如何切换的

image

1、用户单击该按钮,事件处理程序GetButton_OnClick开始排队等待运行。

2、用户界面线程执行GetButton_OnClick的前半部分,包括对GetFaviconAsync的调用。

3、UI线程继续进入GetFaviconAsync并执行其前半部分,包括对DownloadDataTaskAsync的调用。

4、UI线程继续进入DownloadDataTaskAsync,它启动下载并返回任务。

5、UI线程离开DownloadDataTaskAsync,并返回GgetFaviconAsync处的await。

6、当前的UI线程捕获到了SynchronizationContext。

7、GetFaviconAsyncy因为有await的标识,会等待,当DownloadDataTaskAsync完成后GetFaviconAsyncy便会使用捕获到的SynchronizationContext恢复。

8、用户线程离开GetFaviconAsync,并返回一个任务,并运行到GetButton_OnClick中的await。

9、类似地,GetButton_OnClick被等待暂停。

10、用户线程离开GetButton_OnClick,可能会用于处理其他操作。【此时,我们正在等待图标下载。可能需要几秒钟。注意,UI线程可以自由处理其他用户操作,而IO完成端口线程尚未涉及到。操作期间阻塞的线程总数为零。】

11、下载完成,因此IO完成端口在DownloadDataTaskAsync中对逻辑进行排队处理。

12、IO完成端口线程将把DownloadDataTaskAsync返回的任务设置为完成。

13、IO完成端口线程在任务内部运行代码并处理完成,并会调用捕获到的同步上下文(UI线程)上的POST以继续运行接下来的代码。

14、IO完成端口线程被释放并可能在其他IO上工作。

15、用户界面线程找到POST指令,并继续执行GetFaviconAsync的后半部分,直到结束。

16、当UI线程离开GetFaviconAsync时,它会将GetFaviconAsync返回的任务设置为完成。

17、在这个运行点里,当前的同步上下文与捕获的上下文相同,因而无需用到POST,UI线程也会继续同步进行。【此逻辑在WPF中是无效的,因为WPF经常创建新的SynchronizationContext对象。尽管它们是等效的,这使得TPL认为它需要重新POST。】

18、用户线程继续运行GetButton_OnClick的后半部分,直到结束。

总结
同步上下文的每个实现都是以不同的方式执行POST的,这是非常消耗性能的事情。为了避免这种开销,.NET内部也是有自己的优化机制的,它会在捕获的SynchronizationContext与任务完成时的当前上下文相同时,不使用POST。很有意思的是,如果你使用调试器查看这种情况,会发现调用堆栈是颠倒的。

但是,当同步上下文不同时,这就需要用到系统开销了。在性能关键的代码中或者某个代码库中,如果我们并不不关心使用到了哪个线程,这个时候我们也可以通过自己的手动操作来避开这种开销。

在等待任务之前调用ConfigureaWait来完成。这样就不会恢复到原始同步上下文。

1: byte[] bytes = await httpClient.PostAsJsonAsync(url,data).ConfigureAwait(false).ReadAsStreamAsync();
不过,ConfigureAwait并不是严格的指令,它是.NET设计的一个标识,用来告诉运行时我们不介意方法在哪个线程上运行。如果该线程不重要(线程池线程),它将会继续执行代码。如果是很重要的线程,.NET会通过自身机制将线程释放,让它来做其他事情,而方法也将在线程池中恢复。.NET使用线程的当前的SynchronizationContext来判断它是否重要。

前文有说过,本文再提一次,在同步代码中运行异步代码,可能有隐藏的问题。Task有一个Result属性,该属性阻止等待任务完成。如以下代码:

1: var result = GetUserAsync().Result;
但是如果在只有一个线程(如UI线程)的SynchronizationContext使用就会发生死锁现象。解决问题的方法就是,我们可以使用线程池线程来解决这个问题。如以下代码:

1: var result = Task.Run(() =>GetUserAsync()).Result;
以上为本篇文章的主要内容,希望大家多提意见,如果喜欢记得点个推荐哦

作者: 艾心

出处: https://www.cnblogs.com/edison0621/

相关文章
|
2月前
|
缓存 Java 开发者
Java多线程并发编程:同步机制与实践应用
本文深入探讨Java多线程中的同步机制,分析了多线程并发带来的数据不一致等问题,详细介绍了`synchronized`关键字、`ReentrantLock`显式锁及`ReentrantReadWriteLock`读写锁的应用,结合代码示例展示了如何有效解决竞态条件,提升程序性能与稳定性。
202 6
|
2月前
|
开发框架 Java .NET
.net core 非阻塞的异步编程 及 线程调度过程
【11月更文挑战第12天】本文介绍了.NET Core中的非阻塞异步编程,包括其基本概念、实现方式及应用示例。通过`async`和`await`关键字,程序可在等待I/O操作时保持线程不被阻塞,提高性能。文章还详细说明了异步方法的基础示例、线程调度过程、延续任务机制、同步上下文的作用以及如何使用`Task.WhenAll`和`Task.WhenAny`处理多个异步任务的并发执行。
|
2月前
|
并行计算 数据处理 调度
Python中的并发编程:探索多线程与多进程的奥秘####
本文深入探讨了Python中并发编程的两种主要方式——多线程与多进程,通过对比分析它们的工作原理、适用场景及性能差异,揭示了在不同应用需求下如何合理选择并发模型。文章首先简述了并发编程的基本概念,随后详细阐述了Python中多线程与多进程的实现机制,包括GIL(全局解释器锁)对多线程的影响以及多进程的独立内存空间特性。最后,通过实例演示了如何在Python项目中有效利用多线程和多进程提升程序性能。 ####
|
2月前
|
设计模式 安全 Java
Java 多线程并发编程
Java多线程并发编程是指在Java程序中使用多个线程同时执行,以提高程序的运行效率和响应速度。通过合理管理和调度线程,可以充分利用多核处理器资源,实现高效的任务处理。本内容将介绍Java多线程的基础概念、实现方式及常见问题解决方法。
122 0
|
3月前
|
数据挖掘 程序员 调度
探索Python的并发编程:线程与进程的实战应用
【10月更文挑战第4天】 本文深入探讨了Python中实现并发编程的两种主要方式——线程和进程,通过对比分析它们的特点、适用场景以及在实际编程中的应用,为读者提供清晰的指导。同时,文章还介绍了一些高级并发模型如协程,并给出了性能优化的建议。
46 3
|
4月前
|
负载均衡 Java 调度
探索Python的并发编程:线程与进程的比较与应用
本文旨在深入探讨Python中的并发编程,重点比较线程与进程的异同、适用场景及实现方法。通过分析GIL对线程并发的影响,以及进程间通信的成本,我们将揭示何时选择线程或进程更为合理。同时,文章将提供实用的代码示例,帮助读者更好地理解并运用这些概念,以提升多任务处理的效率和性能。
74 3
|
4月前
|
Java Android开发 UED
🧠Android多线程与异步编程实战!告别卡顿,让应用响应如丝般顺滑!🧵
在Android开发中,为应对复杂应用场景和繁重计算任务,多线程与异步编程成为保证UI流畅性的关键。本文将介绍Android中的多线程基础,包括Thread、Handler、Looper、AsyncTask及ExecutorService等,并通过示例代码展示其实用性。AsyncTask适用于简单后台操作,而ExecutorService则能更好地管理复杂并发任务。合理运用这些技术,可显著提升应用性能和用户体验,避免内存泄漏和线程安全问题,确保UI更新顺畅。
153 5
|
4月前
|
缓存 监控 Java
Java中的并发编程:理解并应用线程池
在Java的并发编程中,线程池是提高应用程序性能的关键工具。本文将深入探讨如何有效利用线程池来管理资源、提升效率和简化代码结构。我们将从基础概念出发,逐步介绍线程池的配置、使用场景以及最佳实践,帮助开发者更好地掌握并发编程的核心技巧。
|
4月前
|
C# UED
C#一分钟浅谈:异步编程基础 (async/await)
在现代软件开发中,异步编程对于提升应用性能和响应性至关重要,尤其是在处理网络请求和文件读 异步编程允许程序在等待操作完成时继续执行其他任务,从而提高用户体验、高效利用资源,并增强并发性。在 C# 中,`async` 用于标记可能包含异步操作的方法,而 `await` 则用于等待异步操作完成。 示例代码展示了如何使用 `async` 和 `await` 下载文件而不阻塞调用线程。此外,本文还讨论了常见问题及解决方案,如不在 UI 线程上阻塞、避免同步上下文捕获以及正确处理异常。
60 0
|
4月前
|
并行计算 API 调度
探索Python中的并发编程:线程与进程的对比分析
【9月更文挑战第21天】本文深入探讨了Python中并发编程的核心概念,通过直观的代码示例和清晰的逻辑推理,引导读者理解线程与进程在解决并发问题时的不同应用场景。我们将从基础理论出发,逐步过渡到实际案例分析,旨在揭示Python并发模型的内在机制,并比较它们在执行效率、资源占用和适用场景方面的差异。文章不仅适合初学者构建并发编程的基础认识,同时也为有经验的开发者提供深度思考的视角。