Func和Action
Func是一种委托,这是在3.5
里面新增的,2.0里面我们使用委托是用Delegate,Func位于System.Core命名空间下,使用委托可以提升效率,例如在反射中使用就可以弥补反射所损失的性能。
Action<T>
和Func<T,TResult>
的功能是一样的,
只是Action<T>
没有返回值,Func<T,TResult>
的最后一个参数为返回值。
Func<T,TResult>
的表现形式如下:
Func<T,TResult>
Func<T,T1,TResult>
Func
- Func只有带泛型的一种形式,Action有带泛型和不带的两种
- Func 委托必须要带有一个返回值
- 可以有0个或多达16个参数类型
- 最后一个泛型参数代表返回类型,前面的都是参数类型
Action
- Action是没有返回值的
- 参数也是0或者最多16个
1 | Func<TResult>; |
总结:
使用Func<T,TResult>和Action
,Action而不使用Delegate其实都是为了简化代码,使用更少的代码达到相同的效果,不需要我们显示的声明一个委托。 Func<T,TResult>的最后一个参数始终是返回类型,而Action
是没有返回类型的,而Action是没有返回类型和参数输入的。
简单示例
1 | using System; |
调用
1 | ZThreadUtil.threadRun( |
异步操作
异步操作可以使用多线程或者异步IO
- Thread(多线程)
- ThreadPool(多线程)
- Task(多线程)
- Task(多线程)+async/await(异步IO)
前三种方式中
Thread
的IsBackground
默认为false
,也就是该线程对调用它的线程不产生依赖,当调用线程退出时该线程也不会结束。
ThreadPool
和Task
的IsBackground
默认为true
来指明该线程是后台线程,这样当主线程退出时该线程也会结束。
不推荐代码中操作都用Thread来处理,推荐使用ThreadPool
和Task
,Task
本身也是基于线程池的。
Thread
和ThreadPool
在.NET Framework 1.1
之后都能使用
Task 对象是在.NET Framework 4
中首次引入的 基于任务的异步模式 的中心组件之一。
什么时候用多线程,什么时候用异步?
要讨论这个问题,首先要有任务类型的概念。
计算机执行的任务,大致分两类,计算型和I/O型,前者耗CPU,后者耗CPU以外的各种硬件(磁盘网卡打印机等)。
对于计算型任务,使用多线程可以并行处理提高效率,比如使用多个线程同时处理多张图片。
对于I/O型任务,使用异步可以腾出CPU资源,比如发起网络请求后,等待响应期间不用消耗CPU资源。
换句话,使用多线程的出发点应该是想要进行并行处理,使用异步的出发点应该是想要减轻CPU负担,如果不是那个目的而使用那个方法,就是用错了手段。
异步和多线程的联系和区别
相同点
- 异步和多线程两者都可以达到避免调用线程使程序发生阻塞的目的,来提高软件的响应性和流畅度。
不同点
- 多线程是多个Thread并发,需要开辟新的线程。
- 异步是硬件式的异步,没有开辟新的线程,简单来说就是当CPU发送完操作命令后,就不再等着命令的执行结束,而是可以去执行别的任务,当上述任务结束时,会触发一个回调,告诉CPU任务执行完毕了,可以接着执行了。
- 异步更强调等待完成,多线程更强调并行。
- 异步方法通常使用回调来进行处理。
线程
简单示例
1 | new Thread(o => { |
线程开启
1 | Thread t = new Thread(new ThreadStart(MyMethod)); |
线程关闭
1 | t.Abort(); |
注意:
一个线程abort以后,不能重启,只能重新new了
线程内执行完毕后线程会自动退出
需要注意的就是IsBackground默认为false,也就是该线程对调用它的线程不产生依赖,
当调用线程退出时该线程也不会结束。
因此需要将IsBackground设置为true以指明该线程是后台线程,这样当主线程退出时该线程也会结束。上面的线程关闭方法不太友善,如果我们的线程中是这样的
while(true){}
来保证线程的运行,那么我们可以定义一个变量,如果想退出时修改变量的值即可
线程池
1 | ThreadPool.QueueUserWorkItem(s => { |
有几个要点:
- 不要将长时间运行的操作放进线程池中;
- 不应该阻塞线程池中的线程;
- 线程池中的线程都是后台线程(又称工作者线程);
线程数
线程池中的 SetMinThreads()
和 SetMaxThreads()
可以设置线程池工作的最小和最大线程数。
其定义分别如下:
1 | // 设置线程池最小工作线程数线程 |
参数
workerThreads:要由线程池根据需要创建的新的工作程序线程数。
completionPortThreads:要由线程池根据需要创建的新的空闲异步 I/O 线程数。
返回值代表是否设置成功。
Task
Task 类的表示单个操作不返回一个值,通常以异步方式执行。
Task 对象是一个的中心思想 基于任务的异步模式 首次引入.NET Framework 4
中。 因为由执行工作 Task 对象通常以异步方式执行在线程池线程上而不是以同步方式在主应用程序线程,可以使用 Status 属性,以及 IsCanceled, ,IsCompleted, ,和 IsFaulted 属性,以确定任务的状态。
ThreadPool相比Thread来说具备了很多优势,但是ThreadPool却又存在一些使用上的不方便。
比如:
- ThreadPool不支持线程的取消、完成、失败通知等交互性操作;
- ThreadPool不支持线程执行的先后次序;
以往,如果开发者要实现上述功能,需要完成很多额外的工作,现在,FCL中提供了一个功能更强大的概念:Task。
Task在线程池的基础上进行了优化,并提供了更多的API。
在FCL4.0中,如果我们要编写多线程程序,Task显然已经优于传统的方式。
创建任务
1 | Task.Run(() => {Thread.Sleep(3000);}); |
示例
1 | Console.WriteLine("线程ID-1:" + Thread.CurrentThread.ManagedThreadId.ToString()); |
注意
1 | 线程ID-1:1 |
Task.Factory.StartNew()
和 Task.Run()
都可以用于启动任务并将其调度到线程池中可用的线程上执行,但它们在一些方面有所不同。
首先,Task.Run()
是使用起来更简单的方法,它的 API 设计更加简洁明了。它的返回值是一个 Task
对象,该对象表示异步操作的状态和结果。而 Task.Factory.StartNew()
需要传递一个 Func<object, TResult>
委托,并返回一个 Task<TResult>
对象。这增加了代码的复杂度,不够简洁。
其次,Task.Run()
默认情况下使用 TaskScheduler
中的 ThreadPoolTaskScheduler
来调度任务。它还针对常见情况进行了优化,例如长时间运行但不阻塞线程的操作,它会尝试在当前线程池中的线程上运行任务,以充分利用线程池中的线程资源,并避免产生额外的线程调度开销。而 Task.Factory.StartNew()
则没有提供这种优化。
最后,Task.Factory.StartNew()
还可以提供更丰富的选项来控制任务的行为,例如设置任务的调度优先级、取消标记、异常处理等。
而 Task.Run()
不提供这些选项,因为它的设计目的是提供一种简单且易于使用的方法来启动任务。
总体来说,如果你只需要启动一个简单的任务并将其调度到线程池中执行,Task.Run()
是一个不错的选择。
而如果你需要更多的控制和选项来管理任务的执行行为,或需要自定义调度器来控制任务的调度行为,那么 Task.Factory.StartNew()
可能更适合你的需求。
同步延时及异步延时
同步延时,卡界面
1 | Stopwatch stopwatch = new Stopwatch(); |
异步延时,不卡界面
1 | Stopwatch stopwatch2 = new Stopwatch(); |
同步+异步延时,不卡界面
1 | Stopwatch stopwatch3 = new Stopwatch(); |
Task+async/await
异步方法旨在成为非阻止操作。 异步方法中的 await
表达式在等待的任务正在运行时不会阻止当前线程。 相反,表达式在继续时注册方法的其余部分并将控件返回到异步方法的调用方。
async
和 await
关键字不会创建其他线程。 因为异步方法不会在其自身线程上运行,因此它不需要多线程(这里不是说不需要多线程,而是异步的方法本来就要用其他线程
)。 只有当方法处于活动状态时,该方法将在当前同步上下文中运行并使用线程上的时间。 可以使用 Task.Run 将占用大量 CPU 的工作移到后台线程,但是后台线程不会帮助正在等待结果的进程变为可用状态。
对于异步编程而言,该基于异步的方法优于几乎每个用例中的现有方法。 具体而言,此方法比 BackgroundWorker 类更适用于 I/O 绑定操作,因为此代码更简单且无需防止争用条件。 结合 Task.Run 方法使用时,异步编程比 BackgroundWorker 更适用于 CPU 绑定操作,因为异步编程将运行代码的协调细节与 Task.Run
传输至线程池的工作区分开来。
异步控制流的跟踪导航
异步对可能会被屏蔽的活动(如 Web 访问)至关重要。 对 Web 资源的访问有时很慢或会延迟。 如果此类活动在同步过程中被屏蔽,整个应用必须等待。 在异步过程中,应用程序可继续执行不依赖 Web 资源的其他工作,直至潜在阻止任务完成。
下表显示了异步编程提高响应能力的典型区域。 列出的 .NET 和 Windows 运行时 API 包含支持异步编程的方法。
应用程序区域 | 包含异步方法的 .NET 类型 | 包含异步方法的 Windows 运行时类型 |
---|---|---|
Web 访问 | HttpClient | Windows.Web.Http.HttpClient SyndicationClient |
使用文件 | JsonSerializer StreamReader StreamWriter XmlReader XmlWriter | StorageFile |
使用图像 | MediaCapture BitmapEncoder BitmapDecoder |
由于所有与用户界面相关的活动通常共享一个线程,因此,异步对访问 UI 线程的应用程序来说尤为重要。 如果任何进程在同步应用程序中受阻,则所有进程都将受阻。 你的应用程序停止响应,因此,你可能在其等待过程中认为它已经失败。
使用异步方法时,应用程序将继续响应 UI。 例如,你可以调整窗口的大小或最小化窗口;如果你不希望等待应用程序结束,则可以将其关闭。
当设计异步操作时,该基于异步的方法将自动传输的等效对象添加到可从中选择的选项列表中。 开发人员只需要投入较少的工作量即可使你获取传统异步编程的所有优点。
async异步方法的返回类型
异步函数的返回类型只能为: void、Task、Task
Task<TResult>
: 代表一个返回值T类型的操作。Task
: 代表一个无返回值的操作。void
: 为了和传统的事件处理程序兼容而设计。
await(等待)
await等待的是什么? 可以是一个异步操作(Task)、亦或者是具备返回值的异步操作(Task<TResult>
)的值 。
常见用法
1 | private async Task<string> GetStr() |
代码示例
元素显示3秒后消失
1 | private async void Button_Click_2(object sender, RoutedEventArgs e) |
就可以改为
1 | private async void Button_Click_2(object sender, RoutedEventArgs e) |
或者
1 | private async void Button_Click_2(object sender, RoutedEventArgs e) |
要使用await这个功能,必须是在.NET 4
以后的版本才可以用,如果是之前的版本是没法用的。
多个异步操作
下面的示例就是等多个异步都返回后再处理其他逻辑。
1 | private async Task MyTestAsync() |
帮助理解
1 | DateTime time0 = DateTime.Now; |
这里调用了3个Delay,总等待时间花了8秒。
1 | DateTime time0 = DateTime.Now; |
这里同样调用了3个Delay,但是总等待时间只花了5秒,因为拆分使用后任务做了统筹。
常见问题
Task.Run()是新线程中执行的吗?
Task.Run()
并不总是在新线程中执行。
Task.Run()
方法用于启动一个新的任务,并将该任务调度到线程池中可用的线程上执行。
如果当前线程池中没有可用的线程,它将等待一个线程变得可用,然后执行该任务。
此外,如果任务的体积很小,它也可能在当前线程上执行。
如果一定在新的线程中执行。可以将 TaskCreationOptions.LongRunning
选项,这将指示 Task
运行一个长时间运行、无法预测完成时间的操作,并且应该尽可能分配给一个新线程,以避免阻塞和影响线程池中其他任务的执行。
例如:
1 | Task.Factory.StartNew( |
需要注意的是,使用 TaskCreationOptions.LongRunning
选项会降低线程池的利用率,因为它需要为该任务分配一个新的线程,而不是重用线程池中的线程。因此,应该仅在必要时才使用该选项,以避免影响系统的整体性能。
为什么async方法里必须要有await?
C#编译器一旦遇到以async声明的方法(即异步方法)时会在这个方法中尝试寻找await关键字,如果找到以await关键字声明的方法,就会自动生成一些代码,这些代码负责尝试找到空闲的CPU内核运行以await声明的方法(即以异步方式运行),完成这一切后调用者线程从异步方法返回。后台线程在运行完以await声明的方法后会结束,此时await关键字生成的代码还负责提取方法的返回结果,所以干活的都是await,async实际上只是提醒编译器”你看到有async关键字的方法时进去方法内部是不是有个await关键字,有的话就干活”。
也就是说:
async只是起个标记这是一个异步方法的作用,真正起作用的是await。
所以,一个async的方法里面没有await的调用,那等于是脱了裤子放屁,本质上只是把return xxx
改成了retrurn Task.FromResult( xxx )
而已,没有任何变化。
如果认为一个方法加上了async他就自动成为了异步的调用,说明你连最根本的异步是什么都没搞清楚。你所理解的那种所谓的异步,直接用Task.Run
就可以了。
那为什么还要用async/await
呢?
假如方法在主线程执行,await方便在主线程中获取分线程返回的结果。
方便处理方法执行顺序的先后和并行。
async是多线程吗?
async/await的全称叫做 异步IO及等待 所以他跟线程没有任何关系。
Task才是线程的升级,async并不是。async只是异步IO的升级封装。
async本身不是开了一个线程,他本身只是等待CPU完成那个煮饭线程后,给你发起一个煮饭完成的IO事件,你实际等待只是这个IO事件,而不是煮饭线程。
1 | private async Task<string> myFun() |
输出
1-ThreadID:1
2-ThreadID:4
方法结果:123
3-ThreadID:1
结论
async不会创建新的线程,Task才会使用新的线程。
如果async的方法中想要添加延迟,要有
await Task.Delay(5000);
,千万不要用Thread.Sleep(3000);
,因为他会在调用的线程执行,如果在主进程就会阻塞页面的渲染。一般
async/await
都是和Task
结合使用,如果只用async
是没有意义的,相当于还是在方法所在线程执行,比如说接口请求。
延迟删除文件
假如我们一个线程产生文件,每个文件都保留1分钟,超过1分钟的删除
线程(会频繁创建和关闭线程)
1 | new Thread(o =>{ |
线程池
1 | ThreadPool.QueueUserWorkItem(s => { |
Task
1 | Task t = new Task(() => { |
ThreadPool.QueueUserWorkItem 和 Task.Run有什么区别
C# ThreadPool.QueueUserWorkItem和Task.Run是C#语言中异步编程的两种方式,它们的区别在于:
用法:
ThreadPool.QueueUserWorkItem是一种底层的API,需要手动创建线程池,将需要异步执行的方法放入队列中;
Task.Run是一种高层的API,在后台线程池上启动任务。
返回值:
ThreadPool.QueueUserWorkItem返回值为Boolean类型,表示方法是否成功加入队列;
Task.Run返回值为Task类型,可以通过该类型的实例来访问异步操作的结果。
异常处理:
ThreadPool.QueueUserWorkItem需要手动捕获异常,否则会导致程序崩溃;
Task.Run可以通过await关键字捕获异常,使得代码更加简洁。
性能:
ThreadPool.QueueUserWorkItem的性能较差,因为需要手动创建线程池;
Task.Run使用的是线程池中的线程,可以利用线程池中的可用资源,提高异步程序的性能。
综上所述,Task.Run比ThreadPool.QueueUserWorkItem更加方便、安全和性能更高。在平时编写异步操作的时候,建议使用Task.Run。
按钮异步事件导致重复触发
当我们添加按钮的点击事件的时候,如果事件本身是异步的(async修饰的),那么会导致事件重复执行,即使我们在异步方法的外面添加变量判断也会出问题。
这里建议
按钮的点击事件不要设置为异步的,如果执行的逻辑必须为异步,则新添加一个异步的方法类型是
async void
,然后再调用这个新添加的方法。这样代码的执行的结果才会和我们预想的一致。
Invoke和BeginInvoke
什么时候用到Invoke和BeginInvoke
当调度线程不是主线程的时候
简单示例
1 | this.Dispatcher.Invoke( |
我们一个方法里,调用100次Invoke或者BeginInvoke方法,每次加载一张图片:
使用方式 | 界面鼠标流畅与否 | 图像同步还是延迟 | 丢帧情况 |
---|---|---|---|
Invoke(action) | 卡 | 同步 | 丢帧 |
BeginInvoke(action) | 卡 | 不同步 | 不丢帧 |
Invoke(action,background) | 流畅 | 同步 | 丢帧 |
BeginInvoke(action,background) | 流畅 | 不同步 | 不丢帧 |
结论
在大量调用封送invoke的过程中,invoke会造成:丢帧,同步
在大量调用封送beginInvoke的过程中,beginInvoke会造成:不同步,不丢帧
在大量调用封送过程的时候,加DispatcherPriority会让界面流畅,不加会卡顿
因此每个人可以根据自己的情况来选择用哪一种封送方式及选择适当的参数。
- 在做视频项目的时候,打架关注的是视频的实时性,而略微的丢帧是没问题的。所以用invoke方法比较好。
- 在做数据处理的项目中,如果关注数据的完整性,即使不同步是能够接收的。这时候使用beginInvoke比较好。
- 当界面封送对象少的时候,或者需要同步数据的时候,即使等待也是可以接收的。不加background。
- 当界面封送对象多的时候,需要更加流畅的操作的时候,加background
BackgroundWorker
BackgroundWorker类的功能有这么一个描述:
BackgroundWorker类允许您在单独的线程上执行某个可能导致用户界面(UI)停止响应的耗时操作(比如文件下载数据库事务等),并且想要一个响应式的UI来反应当前耗时操作的进度。
可以看的出来,BackgroundWorker组件提供了一种执行异步操作(后台线程)的同时,并且还能妥妥的显示操作进度的解决方案。
属性
WorkerReportsProgress
bool类型,指示BackgroundWorker是否可以报告进度更新。当该属性值为True是,将可以成功调用ReportProgress方法,否则将引发InvalidOperationException异常。
用法:
1 | private BackgroundWorker bgWorker = new BackgroundWorker(); |
WorkerSupportsCancellation
bool类型,指示BackgroundWorker是否支持异步取消操作。当该属性值为True是,将可以成功调用CancelAsync方法,否则将引发InvalidOperationException异常。
用法:
1 | bgWorker.WorkerSupportsCancellation = true; |
CancellationPending
bool类型,指示应用程序是否已请求取消后台操作。此属性通常放在用户执行的异步操作内部,用来判断用户是否取消执行异步操作。当执行BackgroundWorker.CancelAsync()
方法时,该属性值将变为True。
用法:
1 | //在DoWork中键入如下代码 |
IsBusy
bool类型,指示BackgroundWorker是否正在执行一个异步操作。此属性通常放在BackgroundWorker.RunWorkerAsync()方法之前,避免多次调用RunWorkerAsync()方法引发异常。当执行BackgroundWorker.RunWorkerAsync()方法是,该属性值将变为True。
1 | //防止重复执行异步操作引发错误 |
方法
RunWorkerAsync()
开始执行一个后台操作。调用该方法后,将触发BackgroundWorker.DoWork事件,并以异步的方式执行DoWork事件中的代码。
该方法还有一个带参数的重载方法:RunWorkerAsync(Object)。该方法允许传递一个Object类型的参数到后台操作中,并且可以通过DoWork事件的DoWorkEventArgs.Argument属性将该参数提取出来。
注:当BackgroundWorker的IsBusy属性为True时,调用该方法将引发InvalidOperationException异常。
1 | //在启动异步操作的地方键入代码 |
ReportProgress(Int percentProgress)
报告操作进度。调用该方法后,将触发BackgroundWorker. ProgressChanged事件。另外,该方法包含了一个int类型的参数percentProgress,用来表示当前异步操作所执行的进度百分比。
该方法还有一个重载方法:ReportProgress(Int percentProgress, Object userState)。允许传递一个Object类型的状态对象到 ProgressChanged事件中,并且可以通过ProgressChanged事件的ProgressChangedEventArgs.UserState属性取得参数值。
注:调用该方法之前需确保WorkerReportsProgress属性值为True,否则将引发InvalidOperationException异常。
用法:
1 | for (int i = 0; i <= 100; i++) |
CancelAsync()
请求取消当前正在执行的异步操作。调用该方法将使BackgroundWorker.CancellationPending属性设置为True。
但需要注意的是,并非每次调用CancelAsync()都能确保异步操作,CancelAsync()通常不适用于取消一个紧密执行的操作,更适用于在循环体中执行。
用法:
1 | //在需要执行取消操作的地方键入以下代码 |
事件
DoWork
用于承载异步操作。当调用BackgroundWorker.RunWorkerAsync()
时触发。
需要注意的是,由于DoWork事件内部的代码运行在非UI线程之上,所以在DoWork事件内部应避免于用户界面交互,而于用户界面交互的操作应放置在ProgressChanged和RunWorkerCompleted事件中。ProgressChanged
当调用BackgroundWorker.ReportProgress(int percentProgress)
方式时触发该事件。
该事件的ProgressChangedEventArgs.ProgressPercentage属性可以接收来自ReportProgress方法传递的percentProgress参数值,ProgressChangedEventArgs.UserState属性可以接收来自ReportProgress方法传递的userState参数。RunWorkerCompleted
异步操作完成或取消时执行的操作,当调用DoWork事件执行完成时触发。
该事件的RunWorkerCompletedEventArgs
参数包含三个常用的属性Error,Cancelled,Result。其中,Error表示在执行异步操作期间发生的错误;Cancelled用于判断用户是否取消了异步操作;Result属性接收来自DoWork事件的DoWorkEventArgs参数的Result属性值,可用于传递异步操作的执行结果。
示例
1 | bgWorker.WorkerReportsProgress = true; |