文章目录
- 基础概念
- 进程
- 线程
- 句柄
- 多线程
- 同步/异步
- C#中的多线程
- Thread
- 如何开启新线程
- 线程的停止等待
- 后台线程,前台线程
- 跨线程操作主线程UI
- 线程的优先级
- 扩展封装
- 数据槽
- 内存栅栏
- 资源竞争与线程锁
- ThreadPool
- ThreadPool好处
- 线程池如何分配一个线程
- 线程等待
- 线程池如何控制线程数量
- 扩展一个定时器功能
- Task
- Task出现背景
- Task开启线程的三中方式
- 方式一
- 方式二
- 方式三
- Task阻塞的三种方式
- 方式一(不推荐)
- 方式二(推荐)
- 方式三(推荐)
- Task任务的延续
- 使用
- 延续任务的行为设置
- Task枚举TaskCreationOptions
- 父子任务
- 耗时任务
- Task中使用取消令牌
- Task任务的取消和判断
- Task任务延时取消
- Task任务取消回调
- Task跨线程访问控件
- 方式一
- 方式二
- Task的异常处理
- Parallel
- Parallel特点
- Parallel.For
- Parallel.ForEach
- 无返回值,有Async,无Await
- 无返回值,有Async,有Await
- 返回Task,有Async,有Await
- 返回Task int,有Async,有Await
- 返回Task,实现多个任务顺序执行不阻塞
- 在Winform中存在特殊处理
基础概念
一、进程
当一个程序开始运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源。 而一个进程又是由多个线程所组成的。
进程(Process)是操作系统分配资源的基本单元,它代表正在执行的程序实例。
1、进程的创建和控制
System.Diagnostics
命名空间提供了对进程进行管理的类,如Process
类用于创建、启动和控制进程。- 可以使用
Process.Start()
方法启动一个新进程,也可以获取已有进程的信息并与其进行交互。
2、进程的属性和信息
Process
类允许获取有关进程的各种信息,如进程 ID、进程名、启动时间、占用资源情况等。- 通过
StartInfo
属性可以设置启动进程的参数、工作目录、标准输入输出以及其他属性。
3、进程的控制和管理
- 可以使用
Kill()
方法结束指定的进程,或者通过Close()
方法关闭进程的主窗口。 - 进程还提供了一些其他方法和属性,允许读取和写入进程的标准输入输出,以及与进程进行交互。
4、多进程编程
- C# 中的多线程和异步编程允许创建和管理多个并行执行的进程。
Process
类可以用于创建和管理多个独立的进程。
5、进程间通信
- 可以使用多种方法实现不同进程之间的通信,如命名管道、共享内存、Socket 等机制,从而允许进程之间进行数据交换和通信。
6、权限和安全性
- 进程受操作系统权限和安全机制的限制,需要足够的权限才能启动、结束或管理其他进程。
7、调试和监控
System.Diagnostics
命名空间提供了一些工具和类,用于监视和调试正在运行的进程,如ProcessMonitor
、PerformanceCounter
等。
进程是操作系统资源分配的基本单位,C# 提供了一些类和方法来管理和控制进程,包括创建、启动、控制、监控和与进程间通信等功能。通过这些类,可以有效地管理应用程序中的进程,执行各种任务并实现进程间的协作。
二、线程
线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的,即不同的线程可以执行同样的函数。
线程(Thread)是程序执行的最小单位,它是操作系统调度的基本单元,负责执行程序的指令。在 C# 中,线程的管理和操作由 System.Threading
命名空间提供支持。
1、线程的创建和控制
- 可以使用
Thread
类创建和控制线程。通过ThreadStart
或ParameterizedThreadStart
委托指定线程执行的方法。 - 调用线程的
Start()
方法启动线程执行指定的方法。
2、线程的状态
- 线程可以处于多种状态,如运行、等待、挂起、终止等。可以通过
ThreadState
属性获取线程的状态信息。
3、线程同步和互斥
- 线程同步是多线程编程中重要的概念,可以使用锁(
lock
)、信号量(Semaphore
)、互斥锁(Mutex
)、事件(Event
)等机制控制线程的访问和执行顺序,防止多个线程对共享资源的冲突访问。
4、线程池
- .NET Framework 提供了线程池(
ThreadPool
)来管理线程,通过线程池可以避免频繁创建和销毁线程,提高了线程的重用性和效率。
5、后台线程和前台线程
- 可以通过设置
IsBackground
属性将线程设置为前台线程或后台线程。后台线程在主线程结束时会自动结束,而前台线程则会阻止程序的结束直到所有前台线程执行完毕。
6、异步编程和多线程
- C# 中的异步编程模型(如
async/await
)是基于线程的异步执行,能够提高程序的响应性和性能,但并不直接控制线程的创建和管理。
7、线程安全性
- 多线程编程需要考虑线程安全问题,确保对共享资源的访问是安全的,避免出现竞争条件和数据不一致的问题。
8、性能和资源消耗
- 创建过多的线程可能会增加系统的资源消耗和上下文切换的开销,因此需要根据实际需求合理控制线程的数量和使用方式。
线程是多任务并行执行的基本单位,在 C# 中,通过 Thread
类和其他同步机制可以创建、管理和控制线程的执行,但在进行多线程编程时需要注意线程安全性和资源消耗等问题。随着异步编程模型的不断完善,建议在合适的场景下使用异步编程来代替直接操作线程,以获得更好的性能和可维护性。
三、句柄
句柄(Handle)是一种引用或标识内部系统资源的值。这些句柄在底层操作系统中表示着各种不同类型的资源,如内存对象、文件、窗口、线程等。句柄提供了对这些资源的访问和操作。以下是句柄的一些重要概念:
1、句柄的类型
- 指针句柄(Pointer Handle):用于引用内存地址的指针。
- 对象句柄(Object Handle):用于引用 .NET 对象的引用(例如,
IntPtr
类型)。 - 操作系统句柄:用于引用操作系统资源,如文件句柄、窗口句柄、线程句柄等。
2、句柄的作用
- 资源引用:句柄允许程序访问和操作操作系统或内部资源,如文件、内存、线程等。
- 资源释放:在使用完毕后,释放句柄对应的资源是很重要的,否则可能导致资源泄漏和系统性能下降。
- 跨域访问:句柄也可用于跨应用程序域或进程之间共享资源的通信和访问。
3、句柄的管理
- 对于 .NET 管理的对象,CLR(Common Language Runtime)负责管理它们的生命周期,自动释放资源。
- 对于非托管资源或系统资源,程序员需要手动管理句柄,确保适当地释放资源,避免资源泄漏。
4、句柄在操作系统中的应用
- 在 Windows 操作系统中,几乎所有系统资源都被分配了一个唯一的句柄,如文件句柄、窗口句柄、进程句柄等。
- 句柄使得操作系统能够有效地管理和控制资源,允许程序以统一的方式访问和操作这些资源。
句柄是对内部系统资源的引用或标识,在 C# 中用于访问和操作操作系统资源。了解如何管理和使用句柄是进行系统级编程或与底层系统进行交互的重要一环。
四、多线程
1、多线程概念
多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。
2、多线程优点
可以提高CPU的利用率。在多线程程序中,一个线程必须等待的时候,CPU可以运行其它的线程而不是等待,这样就大大提高了程序的效率。(牺牲空间计算资源,来换取时间)
3、多线程缺点
线程也是程序,所以线程运行需要占用计算机资源,线程越多占用资源也越多。(占内存多)
多线程需要协调和管理,所以需要CPU跟踪线程,消耗CPU资源。(占cpu多)
线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题。多线程存在资源共享问题)
线程太多会导致控制太复杂,最终可能造成很多Bug。(管理麻烦,容易产生bug)
4、为什么计算机可以多线程
程序运行需要计算机资源,操作系统就会去申请CPU来处理,CPU在执行动作的时候是分片执行的。
分片:把CPU的处理能力进行切分,操作系统在调度的时候,按照切片去处理不同的计算需求,按照规则分配切片计算资源,对于同一个计算机核心来讲,所有的运行都是串行的,但是因为分片的存在,感觉几个程序同时在向前推进。
5、何时建议使用多线程
当主线程试图执行冗长的操作,但系统会卡界面,体验非常不好,这时候可以开辟一个新线程,来处理这项冗长的工作。
当请求别的数据库服务器、业务服务器等,可以开辟一个新线程,让主线程继续干别的事。
利用多线程拆分复杂运算,提高计算速度。
6、何时不建议使用多线程
当单线程能很好解决,就不要为了使用多线程而用多线程。
五、同步/异步
1、同步方法
线性执行,从上往下依次执行,同步方法执行慢,消耗的计算机资源少。
2、异步方法
线程和线程之间,不再线型执行,多个线程总的耗时少,执行快,消耗的计算机资源多,各线程执行是无序的。
六、C#中的多线程
Thread/ThreadPool/Task/异步编程(async/await) 都是C#语言在操作计算机的资源时封装的帮助类库。
1、Thread 类
System.Threading.Thread
类允许创建和操作线程。可以创建一个新的线程,并在其中执行指定的方法。- 使用
ThreadStart
或ParameterizedThreadStart
委托指定线程执行的方法。
Thread thread = new Thread(MyMethod); thread.Start();
2、ThreadPool 线程池
System.Threading.ThreadPool
类提供了线程池来管理线程,避免频繁创建和销毁线程,提高了线程的重用性和效率。
ThreadPool.QueueUserWorkItem(MyMethod);
3、Task 并行任务
System.Threading.Tasks.Task
类是 .NET 中更高级别的多线程抽象,提供了更方便的异步编程模型,可以利用Task.Run
方法创建并运行任务。
Task.Run(() => MyMethod());
4、异步编程(async/await)
async/await
是异步编程的一种模型,虽然并不直接控制线程,但是能够更简单地进行并发操作,而不需要显式操作线程。
public async Task MyAsyncMethod() { await SomeOperationAsync(); }
5、TODO:线程同步和互斥
多线程编程中需要考虑线程安全问题,可以使用锁(lock
)、信号量(Semaphore
)、互斥锁(Mutex
)、事件(Event
)等机制控制线程的访问和执行顺序,避免资源竞争和数据不一致的问题。
lock (lockObject) { // Critical section }
6、TODO:线程状态和控制
- 可以使用
ThreadState
属性获取线程的状态信息,例如运行、等待、挂起等。 - 可以使用
Thread.Sleep
来让线程暂停执行一段时间。
Thread.Sleep(1000); // 暂停执行 1 秒
多线程编程能够提高程序的性能和并发处理能力,但也需要注意线程安全性和资源消耗等问题。在合适的场景下选择合适的多线程实现方式是编写高效且可维护的程序的重要一环。
Thread
Thread是.Net最早的多线程处理方式,它出现在.Net1.0时代,虽然现在已逐渐被微软所抛弃,微软强烈推荐使用Task,但从多线程完整性的角度上来说,我们有必要了解下早期多线程的是怎么处理的,以便体会.Net体系中多线程处理方式的进化。
一、如何开启新线程
分析Thread类的源码,发现其构造函数参数有两类
- ThreadStart类:是无参无返回值的委托。
- ParameterizedThreadStart类:是有一个object类型参数但无返回值的委托。
开启了一个新的线程1
ParameterizedThreadStart parameterizedThreadStart = new ParameterizedThreadStart((oInstacnce) => { Debug.WriteLine($"ParameterizedThreadStart--{Thread.CurrentThread.ManagedThreadId.ToString("00")}--{DateTime.Now.ToString(" HH:mm:ss.fff")}"); }); Thread thread = new Thread(parameterizedThreadStart); thread.Start();
开启了一个新的线程2
ThreadStart threadStart = new ThreadStart(() => { this.DoSomething("张三"); }); Thread thread = new Thread(threadStart); thread.Start();
二、线程的停止等待
Thread thread = new Thread(() => { this.DoSomething("张三", 5000); }); thread.Start(); //thread.Suspend(); //表示线程暂停,现在已弃用,NetCore平台已经不支持 //thread.Resume(); //线程恢复执行,弃用,NetCore平台已经不支持 //thread.Abort(); //线程停止,子线程对外抛出了一个异常,线程是无法从外部去终止的 //Thread.ResetAbort();//停止的线程继续去执行 //thread.ThreadState //根据线程状态ThreadState判断实现线程间歇性休息 //while (thread.ThreadState != System.Threading.ThreadState.Stopped) //{ // Thread.Sleep(500); //当前休息500ms,不消耗计算机资源的 //} thread.Join();//主线程等待,直到当前线程执行完毕 //thread.Join(500);//主线程等待500毫秒,不管当前线程执行是否完毕,都继续往后执行 //thread.Join(new TimeSpan(500*10000));//主线程等待500毫秒,不管当前线程执行是否完毕,都继续往后执行 //TimeSpan 单位100纳秒 1毫秒=10000*100纳秒
三、后台线程,前台线程
- 后台线程,界面关闭,线程也就随之消失
- 前台线程,界面关闭,线程会等待执行完才结束
Thread thread = new Thread(() => { this.DoSomething("张三"); }); thread.Start(); thread.IsBackground = true;//后台线程,界面关闭,线程也就随之消失 thread.IsBackground = false;//前台线程,界面关闭,线程会等待执行完才结束 thread.Start();
四、跨线程操作主线程UI
Thread thread = new Thread(() => { for (int i = 0; i <= 5; i++) { Thread.Sleep(500); textBox1.Invoke(new Action(()=> textBox1.Text = i.ToString())); } }); thread.Start();
五、线程的优先级
设置优先级只是提高了他被优先执行的概率
Thread thread = new Thread(() => { this.DoSomething("张三"); }); // 线程的优先级最高 thread.Priority = ThreadPriority.Highest; Thread thread1 = new Thread(() => { this.DoSomething("张三"); }); // 线程的优先级最低 thread1.Priority = ThreadPriority.Lowest; thread.Start(); thread1.Start();//线程开启后,根据优先级,来执行
六、扩展封装
1、实现两个委托多线程顺序执行
方法封装:
private void CallBackThread(Action action1, Action action2) { Thread thread = new Thread(() => { action1.Invoke(); action2.Invoke(); }); thread.Start(); }
方法调用:
Action action1 = () => { this.DoSomething("张三"); }; Action action2 = () => { this.DoSomething("李四"); }; //不会阻塞线程 CallBackThread(action1, action2);
执行结果:
DoSomething--Start--张三--06--16:57:21.411 DoSomething--End--张三--06--16:57:23.412 DoSomething--Start--李四--06--16:57:23.417 DoSomething--End--李四--06--16:57:25.423
2、实现获取多线程执行委托的结果
方法封装:
private Func<T> CallBackFunc<T>(Func<T> func) { T t = default(T); ThreadStart threadStart = new ThreadStart(() => { t = func.Invoke(); }); Thread thread = new Thread(threadStart); thread.Start(); return new Func<T>(() => { thread.Join();//等待thread执行完成; return t; }); }
方法调用:
Func<int> func = () => { this.DoSomething("王五"); return DateTime.Now.Year; }; //这一步不会阻塞界面 Func<int> func1 = this.CallBackFunc<int>(func); Debug.WriteLine("线程开启后,计算结果出来前"); //这里会等待线程计算出结果才继续往后执行 int iResult = func1.Invoke(); Debug.WriteLine($"计算结果:{iResult}");
执行结果:
线程开启后,计算结果出来前 DoSomething--Start--王五--09--17:06:12.088 DoSomething--End--王五--09--17:06:14.090 计算结果:2021
七、数据槽
为了解决多线程竞用共享资源的问题,引入数据槽的概念,即将数据存放到线程的环境块中,使该数据只能单一线程访问。
1、AllocateNamedDataSlot命名槽位和AllocateDataSlot未命名槽位
在主线程上设置槽位,使该数据只能被主线程读取,其它线程无法访问
AllocateNamedDataSlot命名槽位:
var d = Thread.AllocateNamedDataSlot("userName"); Thread.SetData(d, "张三"); //声明一个子线程 var t1 = new Thread(() => { Debug.WriteLine($"子线程中读取数据:{Thread.GetData(d)}"); }); t1.Start(); //主线程中读取数据 Debug.WriteLine($"主线程中读取数据:{Thread.GetData(d)}");
AllocateDataSlot未命名槽位:
var d = Thread.AllocateDataSlot(); Thread.SetData(d, "李四"); //声明一个子线程 var t1 = new Thread(() => { Debug.WriteLine($"子线程中读取数据:{Thread.GetData(d)}"); }); t1.Start(); //主线程中读取数据 Debug.WriteLine($"主线程中读取数据:{Thread.GetData(d)}");
执行结果:
主线程中读取数据:张三 子线程中读取数据: 主线程中读取数据:李四 子线程中读取数据:
2、利用特性[ThreadStatic]
在主线程中给ThreadStatic特性标注的变量赋值,则只有主线程能访问该变量
变量标记特性:
[ThreadStatic] private static string Age = string.Empty;
线程访问变量:
Age = "小女子年方28"; //声明一个子线程 var t1 = new Thread(() => { Debug.WriteLine($"子线程中读取数据:{Age}"); }); t1.Start(); //主线程中读取数据 Debug.WriteLine($"主线程中读取数据:{Age}");
执行结果:
主线程中读取数据:小女子年方28 子线程中读取数据:
3、利用ThreadLocal线程的本地存储
在主线程中声明ThreadLocal变量,并对其赋值,则只有主线程能访问该变量
ThreadLocal<string> tlocalSex = new ThreadLocal<string>(); tlocalSex.Value = "女博士"; //声明一个子线程 var t1 = new Thread(() => { Debug.WriteLine($"子线程中读取数据:{tlocalSex.Value}"); }); t1.Start(); //主线程中读取数据 Debug.WriteLine($"主线程中读取数据:{tlocalSex.Value}");
执行结果:
主线程中读取数据:女博士 子线程中读取数据:
八、内存栅栏
当多个线程共享一个变量的时候,在Release模式的优化下,子线程会将共享变量加载到cup Cache中,导致主线程不能使用该变量而无法运行
1、默认情况(Release模式主线程不能正常运行)
var isStop = false; var t = new Thread(() => { var isSuccess = false; while (!isStop) { isSuccess = !isSuccess; } Trace.WriteLine("子线程执行成功"); }); t.Start(); Thread.Sleep(1000); isStop = true; t.Join(); Trace.WriteLine("主线程执行结束");
2、MemoryBarrier解决共享变量(Release模式下可以正常运行)
在此方法之前的内存写入都要及时从cpu cache中更新到memory;在此方法之后的内存读取都要从memory中读取,而不是cpu cache。
var isStop = false; var t = new Thread(() => { var isSuccess = false; while (!isStop) { Thread.MemoryBarrier(); isSuccess = !isSuccess; } Trace.WriteLine("子线程执行成功"); }); t.Start(); Thread.Sleep(1000); isStop = true; t.Join(); Trace.WriteLine("主线程执行结束");
3、VolatileRead解决共享变量(Release模式下可以正常运行)
var isStop = 0; var t = new Thread(() => { var isSuccess = false; while (isStop == 0) { Thread.VolatileRead(ref isStop); isSuccess = !isSuccess; } Trace.WriteLine("子线程执行成功"); }); t.Start(); Thread.Sleep(1000); isStop = 1; t.Join(); Trace.WriteLine("主线程执行结束");
九、资源竞争与线程锁
使用线程,我们可以并发或并行的在CPU核心中执行任务,最大化CPU的利用率,但是这样也可能导致产生各种各样奇怪的资源竞争问题。例如运行下列代码,程序报错。
static void Main(string[] args) { for(int i = 0; i< 10; i++) { var thread = new Thread(AddText); thread.Start(); } Console.WriteLine("文件下载完成"); Console.WriteLine("退出主程序"); } static void AddText() { File.AppendAllText(@"ThreadTest.txt", $"开始:{Thread.CurrentThread.ManagedThreadId}"); Thread.Sleep(1000); File.AppendAllText(@"ThreadTest.txt", $"结束:{Thread.CurrentThread.ManagedThreadId}"); }
发生这种错误的原因就是不同的线程之间发生了资源的争抢。在其中一个线程打开文件后,但还没完成操作时,CPU就切换到另外一个线程上,这个线程也要打开文件,由于上一个线程还没有将文件流关闭,就导致异常崩溃了。
这种现象就叫做资源竞争。资源竞争在多线程编程中是一个非常常见问题,最严重的情况就是现在这种系统直接崩溃。
那么,避免这种资源的恶性竞争的关键就在于文件必须每次是能由一个线程打开,当前线程操作文件的时候,其他线程需要排队等待。在C#中,有一个关键词叫做lock,可以帮我们锁定资源来排除资源竞争的现象。
lock块语法:
- 需要注意,传给lock块的参数不能是值类型和string类型,必须是除了string外的引用类型,而且这个引用类型对象必须是所有线程都能访问到的,否则锁不住。
- 其中作为锁的可以是任意的对象,一般情况下会创建一个object对象来作为指定的锁对象。
lock(lockObj) { ...... }
将上述例子中共享资源部分的代码加上锁,就可以避免资源竞争带来的问题。
static object lockObj = new object(); ...... static void AddText() { lock (lockObj) { File.AppendAllText(@"./ThreadTest.txt", $"开始:{Thread.CurrentThread.ManagedThreadId}\n"); Thread.Sleep(1000); File.AppendAllText(@"./ThreadTest.txt", $"结束:{Thread.CurrentThread.ManagedThreadId}\n"); } }
这里讲的是最简单的一种线程锁,lock
块是Monitor语法糖,本质是解决资源的争用问题。
ThreadPool
.NET Framework2.0时代,出现了一个线程池ThreadPool,是一种池化思想,如果需要使用线程,就可以直接到线程池中去获取直接使用,如果使用完毕,在自动的回放到线程池去;
一、ThreadPool好处
不需要程序员对线程的数量管控,提高性能,防止滥用,去掉了很多在Thread中没有必要的Api
二、线程池如何分配一个线程
QueueUserWorkItem方法,将方法排入队列以便开启异步线程,它有两个重载。
- QueueUserWorkItem(WaitCallback callBack),WaitCallback是一个有一个object类型参数且无返回值的委托。
- QueueUserWorkItem(WaitCallback callBack, object state),WaitCallback是一个有一个object类型参数且无返回值的委托,state即WaitCallback中需要的参数, 不推荐这么使用,存在拆箱装箱的转换问题,影响性能。
//无参数 ThreadPool.QueueUserWorkItem(o =>this.DoSomething("张三")); //一个参数 ThreadPool.QueueUserWorkItem(o => this.DoSomething("张三"), "12345");
三、线程等待
- 定义一个监听ManualResetEvent
- 通过ManualResetEvent.WaitOne等待
- 等到ManualResetEvent.Set方法执行了,主线程等待的这个WaitOne()就继续往后执行
ManualResetEvent resetEvent = new ManualResetEvent(false); ThreadPool.QueueUserWorkItem(o => { this.DoSomething(o.ToString()); resetEvent.Set(); }, "张三"); resetEvent.WaitOne();
四、线程池如何控制线程数量
如果通过SetMinThreads/SetMaxThreads来设置线程的数量,不建议大家去这样控制线程数量,这个数量访问是在当前进程中是全局的,错误配置可能影响程序的正常运行
{ //线程池中的工作线程数 int workerThreads = 4; //线程池中异步 I/O 线程的数目 int completionPortThreads = 4; //设置最小数量 ThreadPool.SetMinThreads(workerThreads, completionPortThreads); } { int workerThreads = 8; int completionPortThreads = 8; //设置最大数量 ThreadPool.SetMaxThreads(workerThreads, completionPortThreads); } { ThreadPool.GetMinThreads(out int workerThreads, out int completionPortThreads); Debug.WriteLine($"当前进程最小的工作线程数量:{workerThreads}"); Debug.WriteLine($"当前进程最小的IO线程数量:{completionPortThreads}"); } { ThreadPool.GetMaxThreads(out int workerThreads, out int completionPortThreads); Debug.WriteLine($"当前进程最大的工作线程数量:{workerThreads}"); Debug.WriteLine($"当前进程最大的IO线程数量:{completionPortThreads}"); }
五、扩展一个定时器功能
- RegisterWaitForSingleObject类,但是不常用.(涉及到定时任务,建议使用Quartz.Net)
- System.threading命名空间下的Thread类,通过查看源码,构造函数中有四个参数
- 第一个是object参数的委托
- 第二个是委托需要的值
- 第三个是调用 callback 之前延迟的时间量(以毫秒为单位)
- 第四个是 调用 callback 的时间间隔(以毫秒为单位)
//每隔3s开启一个线程执行业务逻辑 ThreadPool.RegisterWaitForSingleObject(new AutoResetEvent(true), new WaitOrTimerCallback((obj, b) => this.DoSomething("张三")), "hello world", 3000, false); //效果类似于Timer定时器:2秒后开启该线程,然后每隔3s调用一次 System.Threading.Timer timer = new System.Threading.Timer((n) => this.DoSomething("李四"), "1", 2000, 3000);
Task
一、Task出现背景
在前面的章节介绍过,Task出现之前,微软的多线程处理方式有:Thread→ThreadPool→委托的异步调用,虽然也可以基本业务需要的多线程场景,但它们在多个线程的等待处理方面、资源占用方面、线程延续和阻塞方面、线程的取消方面等都显得比较笨拙,在面对复杂的业务场景下,显得有点捉襟见肘了。正是在这种背景下,Task应运而生。
Task是微软在.Net 4.0时代推出来的,也是微软极力推荐的一种多线程的处理方式,Task看起来像一个Thread,实际上,它是在ThreadPool的基础上进行的封装,Task的控制和扩展性很强,在线程的延续、阻塞、取消、超时等方面远胜于Thread和ThreadPool。
二、Task开启线程的三中方式
1、方式一
通过创建Task
对象后调用其Start()
函数。
Task task = new Task(() => { Console.WriteLine("线程1"); }); task.Start();
2、方式二
调用Task
的静态方法Run()
Task task = Task.Run(()=>{Console.WriteLine("线程1"); });
3、方式三
通过Task
工厂,开新建一个线程。
Task task = Task.Factory.StartNew(()=>{Console.WriteLine("线程1"); });
三、Task阻塞的三种方式
Thread的阻塞是通过对象的Join()函数来完成,如果有多个线程需要按序执行,就会出现多个Join()的情况,且不说这样做效率上会怎么样,代码光看着就不舒服。Task对于阻塞的处理有着更好的方案。
1、方式1(不推荐)
使用Task对象的Wait()函数分别等待单个线程任务完成,这种方式跟Thread的Join没啥区别,不推荐使用。
Task task1 = Task.Run(()=>{...}); Task task2 = Task.Run(()=>{...}); task1.Wait(); task2.Wait();
2、方式2(推荐)
通过Task
的静态方法WaitAll()
来指定等待的一个或多个线程结束。
Task task1 = Task.Run(()=>{...}); Task task2 = Task.Run(()=>{...}); Task.WaitAll(task1, task2);
现在假设有三个方法Calculate1、Calculate2和Calculate3,其中Calculate3方法需要依赖Calculate1和Calculate2的计算结果,此时可以通过下列代码来实现。
- TaskAwaiter<TResult> GetAwaiter():Task类的实例方法,返回TaskAwaiter对象。
- TResult GetResult():TaskAwaiter类的实例方法,返回线程任务的返回结果。
Task<int> task1 = Task.Run(Calculator1); Task<int> task2 = Task.Run(Calculator2); TaskAwaiter<int> task1Awaiter = task1.GetAwaiter(); TaskAwaiter<int> task2Awaiter = task2.GetAwaiter(); Task.Run(()=> { Calculator3(task1Awaiter.GetResult(), task2Awaiter.GetResult()); }); Console.Read(); int Calculator1() { return 1*1; } int Calculator2() { return 1 * 2; } void Calculator3(int a, int b) { Console.WriteLine(a*b); }
3、方式3(推荐)
通过Task
的静态方法WaitAny()
来指定等待任意一个线程任务结束。
Task task1 = Task.Run(()=>{...}); Task task2 = Task.Run(()=>{...}); Task.WaitAny(task1, task2);
四、Task任务的延续
1、使用
除了上述例子中,使用Task.GetAwaiter().GetResult()的方式去实现Calculator1、Calculator2、Calculator3之间的关系外,还可以通过使用WhenAll().ContinueWith()来实现,其作用是当WhenAll()中指定的线程任务完成后再执行ContinueWith()中的任务,也就是线程任务的延续。而由于这个等待是异步的,因此不会给主线程造成阻塞。
- WhenAll(task1,task2,...):Task的静态方法,作用是异步等待指定任务完成后,返回结果。当线程任务有返回值时,返回Task<TResult[]>对象,否则返回Task对象。
- WhenAny()用法与WhenAll()是一样的,不同的是只要指定的任意一个线程任务完成则立即返回结果。
- ContinueWith():Task类的实例方法,异步创建当另一任务完成时可以执行的延续任务。也就是当调用对象的线程任务完成后,执行ContinueWith()中的任务。
var task1 = Task.Run(Calculator1); var task2 = Task.Run(Calculator2); Task.WhenAll(task1, task2).ContinueWith((data) => { Calculator3(data.Result[0], data.Result[1]); }); Console.WriteLine("主线程已经完成"); Console.Read(); int Calculator1() { Thread.Sleep(1000); return 1*1; } int Calculator2() { Thread.Sleep(1000); return 1 * 2; } void Calculator3(int a, int b) { Console.WriteLine(a*b); }
显然,ContinueWith()
作为Task
的实例函数,可以通过Task
对象直接调用,由于是异步创建的延续任务因此不会对主线程造成阻塞。
var task1 = Task.Run(Test); task1.ContinueWith((data) => { Console.WriteLine("线程1的延续"); }); Console.WriteLine("主线程已经完成"); Console.Read(); int Test() { Thread.Sleep(1000); }
2、延续任务的行为设置
ContinueWith()
函数有多个重载,可以通过传入TaskContinuationOptions
枚举参数来对延续任务的行为进行设置。
TaskContinuationOptions
说明
var task1 = Task.Run(() => { throw new Exception("抛出线程异常!"); }); task1.ContinueWith((data) => { if(data.IsFaulted) Console.WriteLine("处理异常:" + data.Exception.Message); }, TaskContinuationOptions.OnlyOnFaulted);
五、Task枚举TaskCreationOptions
TaskCreationOptions
枚举类型中多个成员,其中最常用的为AttachedToParent
和LongRunning
,前者用于将子线程依附到父线程中,后者用于声明耗时运行的线程任务。
1、父子任务
假设遇到如下需求,线程parentTask中开启了线程task1和task2,希望在开启parentTask线程任务的主线程中阻塞等待parentTask、task1和task2的任务完成。
此时可以将task1和task2线程依附到parentTask线程上作为parentTask的子线程,这样主线程在等待parentTask线程完成时,就必须同步等待task1和task2线程的任务完成。
子线程的依附操作很简单,在创建线程对象时,传入参数TaskCreationOptions.AttachedToParent即可。
Task parentTask = new Task(() => { Task task1 = new Task(() => { Console.WriteLine("task1任务。。。。。。"); }, TaskCreationOptions.AttachedToParent); Task task2 = new Task(() => { Console.WriteLine("task2任务。。。。。。"); }, TaskCreationOptions.AttachedToParent); task1.Start(); task2.Start(); }); parentTask.Start(); parentTask.Wait(); Console.WriteLine("这里是主线程"); Console.Read();
2、耗时任务
当要执行的线程任务比较耗时时,建议在创建线程对象时传入参数TaskCreationOptions.LongRunning,以此来声明为长时间运行的线程任务。
默认情况下,新建Task线程是从线程池ThreadPool中分配出来的,当使用TaskCreationOptions.LongRunning声明后则是直接新建一个线程。这样就可以避免耗时任务一直占用线程池资源的情况。当然了,也可以直接使用Thread,效果上是一样的。
Task task = new Task(()=>{...}, TaskCreationOptions.LongRunning); task.Start();
六、Task中使用取消令牌
Task中的取消功能使用的是CanclelationTokenSource,即取消令牌源对象,可用于解决多线程任务中协作取消和超时取消。
- CancellationToken Token:CanclelationTokenSource类的属性成员,返回CancellationToken对象,可以在开启或创建线程时作为参数传入。
- bool IsCancellationRequested:CanclelationTokenSource类的属性成员,表示当前任务是否已经请求取消。Token类中也有此属性成员,两者互相关联。
- void Cancel():CanclelationTokenSource类的实例方法,取消线程任务,同时将自身以及关联的Token对象中的IsCancellationRequested属性置为true。
- void CancelAfter(int millisecondsDelay):CanclelationTokenSource类的实例方法,用于延迟取消线程任务。
- CancellationTokenRegistration Register(Action callback):Token类的实例方法,用于注册取消任务后的回调任务。
1、Task任务的取消和判断
CancellationTokenSource cst = new CancellationTokenSource(); Task task = Task.Run(() => { while (!cst.IsCancellationRequested) { Console.WriteLine("持续时间:" + DateTime.Now); } }, cst.Token);//这里第二个参数传入取消令牌 Thread.Sleep(2000); cst.Cancel(); //两秒后结束
2、Task任务延时取消
任务的延时取消可以用于访问超时、执行超时等情况下的任务强制终止。
延时取消任务实现起来很简单,就是将CancellationTokenSource 的Cancel()方法换成CancelAfter(milliseconds)就好了。CancelAfter(milliseconds)也是异步的,这点请注意。
CancellationTokenSource cst = new CancellationTokenSource(); Task task = Task.Run(() => { while (!cst.IsCancellationRequested) { Console.WriteLine("持续时间:" + DateTime.Now); } }, cst.Token);//这里第二个参数传入取消令牌 cst.CancelAfter(2000); //两秒后结束
3、Task任务取消回调
如果取消任务后希望做一些处理工作。此时可以使用CancellationToken
类的Register()
函数来注册一个委托(回调函数),用于取消线程后调用。
CancellationTokenSource cst = new CancellationTokenSource(); Task task = Task.Run(() => { while (!cst.IsCancellationRequested) { Console.WriteLine("持续时间:" + DateTime.Now); Thread.Sleep(500); } }, cst.Token);//这里第二个参数传入取消令牌 cst.Token.Register(() => { Console.WriteLine("开始处理工作......"); Thread.Sleep(2000); Console.WriteLine("处理工作完成......"); }); Thread.Sleep(2000); cst.Cancel(); //两秒后结束
七、Task跨线程访问控件
在使用Winform或WPF编写程序时,经常会遇到跨线程访问控件的情况,除了使用Invoke
和委托等方法外,还可以有以下两种解决方法。
1、方式一
直接将TaskScheduler对象做为参数传给Start()函数(使用TaskScheduler.FromCurrentSynchronizationContext()可以获得TaskScheduler对象),以此来将线程任务传送到指定的调度程序中运行,结合WPF编程宝典多线程章节中的内容,应该是将线程任务丢给控件元素所在线程的调度程序中运行。这样做虽然可以跨线程访问控件,但是带来的弊端就是,如果线程任务耗时,就会让整个窗体卡住。
private void Button_Click(object sender, RoutedEventArgs e) { Task task = new Task(() => { Thread.Sleep(5000);//模拟耗时处理 txt_Info.Text = "test"; //此为文本控件 }); task.Start(TaskScheduler.FromCurrentSynchronizationContext()); }
2、方式二
针对线程耗时的情况,如果直接使用方式一,会导致整个UI界面都卡住,等到控件处理完成才恢复,这样显然是不可以的。因此要改变一下用法,利用线程延续,将耗时的任务与访问UI控件的任务分为两个线程,访问UI的线程放到延续的线程中。
private void Button_Click(object sender, RoutedEventArgs e) { txt_Info.Text = "数据正在处理中......"; txt_Info.Text = "数据正在处理中......"; Task.Run(() => { Thread.Sleep(5000); }).ContinueWith(t => { txt_Info.Text = "test"; }, TaskScheduler.FromCurrentSynchronizationContext()); }
八、Task的异常处理
Task线程的异常处理不能直接将线程对象相关代码try-catch来捕获,那样是捕获不到异常的,需要通过调用线程对象的wait()函数,通过wait()函数来进行线程的异常捕获。
此外,线程的异常会聚合到AggregateException异常对象中(AggregateException是专门用来收集线程异常的异常类),需要通过遍历该异常对象,获取正确的异常信息。
如果捕获到线程异常之后,还想继续往上抛出,就需要调用AggregateException对象的Handle函数,并返回false。(Handle函数遍历了一下AggregateException对象中的异常)
Task task1 = Task.Run(() => { throw new Exception("线程1的异常抛出"); }); Task task2 = Task.Run(() => { throw new Exception("线程2的异常抛出"); }); Task task3 = Task.Run(() => { throw new Exception("线程3的异常抛出"); }); try { task1.Wait(); task2.Wait(); task3.Wait(); } catch (AggregateException ex) { foreach (var item in ex.InnerExceptions) { Console.WriteLine(item.Message); } //如果希望再将异常往外抛出,可以调用AggregateException的Handle函数 //ex.Handle(p => false); } Console.Read();
Parallel
一、Parallel特点
- 可以传入多个委托,多个委托中的内容是会开启线程来执行,执行的线程可能是新的线程,也可能是主线程
- 会阻塞主线程,相当于是主线程等待子线程执行结束
Parallel.Invoke( () => this.DoSomething("张三"), () => this.DoSomething("李四"), () => this.DoSomething("王五"), () => this.DoSomething("赵六"));
执行结果:
DoSomething--Start--赵六--04--11:01:20.666 DoSomething--Start--王五--06--11:01:20.666 DoSomething--Start--张三--01--11:01:20.666 DoSomething--Start--李四--05--11:01:20.666 DoSomething--End--赵六--04--11:01:22.668 DoSomething--End--王五--06--11:01:22.696 DoSomething--End--张三--01--11:01:22.702 DoSomething--End--李四--05--11:01:22.703
- 可以传入options.MaxDegreeOfParallelism来限制开启的线程数量,可以做到不影响线程池的线程数量又能控制当前执行所用的线程数量
ParallelOptions options = new ParallelOptions(); options.MaxDegreeOfParallelism = 2; Parallel.Invoke(options, () => this.DoSomething("张三"), () => this.DoSomething("李四"), () => this.DoSomething("王五"), () => this.DoSomething("赵六"));
执行结果:
DoSomething--Start--张三--01--11:03:20.700 DoSomething--Start--李四--09--11:03:20.702 DoSomething--End--张三--01--11:03:22.704 DoSomething--Start--王五--01--11:03:22.706 DoSomething--End--李四--09--11:03:22.710 DoSomething--Start--赵六--09--11:03:22.711 DoSomething--End--王五--01--11:03:24.707 DoSomething--End--赵六--09--11:03:24.714
- 把Parallel包在一个Task里面实现不卡主线程
Task.Run(() => { Parallel.Invoke( () => this.DoSomething("张三"), () => this.DoSomething("李四"), () => this.DoSomething("王五"), () => this.DoSomething("赵六")); });
二、Parallel.For
实现循环开启线程执行动作,可以获取索引,可以控制开启的线程数量
ParallelOptions options = new ParallelOptions(); options.MaxDegreeOfParallelism = 3; Parallel.For(0, 10, options, index => { Debug.WriteLine($"index:{ index} 线程ID: {Thread.CurrentThread.ManagedThreadId.ToString("00")}"); });
执行结果:
index:3 线程ID: 05 index:0 线程ID: 01 index:6 线程ID: 04 index:1 线程ID: 01 index:7 线程ID: 04 index:8 线程ID: 04 index:4 线程ID: 05 index:5 线程ID: 05 index:2 线程ID: 01 index:9 线程ID: 04
三、Parallel.ForEach
实现循环遍历数组开启线程执行动作,可以获取数组值,可以控制开启的线程数量
List<int> intlist = new List<int>() { 1, 2, 3, 5, 7, 11, 13, 17 }; ParallelOptions options = new ParallelOptions(); options.MaxDegreeOfParallelism = 3; Parallel.ForEach(intlist, options, s => { Debug.WriteLine($"index:{ s} 线程ID: {Thread.CurrentThread.ManagedThreadId.ToString("00")}"); });
执行结果:
index:1 线程ID: 01 index:7 线程ID: 08 index:2 线程ID: 01 index:11 线程ID: 08 index:5 线程ID: 01 index:13 线程ID: 08 index:3 线程ID: 09 index:17 线程ID: 08
Await/Async
一、Aasync/Await是什么
(1)C#5 (.NET4.5) 引入的语法糖
(2)C#7.1,Main入口也可以
(3)C#8.0,可以使用异步流await,foreach可以释放对象await using
二、Await/Async用法
无返回值,有Async,无Awaitasync是用来修饰方法,如果单独出现,方法会警告,不会报错,和普通的多线程方法没有什么区别,不存在线程等待的问题
/// <summary> /// 无返回值,有Async,无Await /// </summary> private async void NoReturnAsyncNoAwait() { Console.WriteLine($"NoReturnAsyncNoAwait Start ThreadId={Thread.CurrentThread.ManagedThreadId}--{DateTime.Now.ToString("HH:mm:ss.fff")}"); Thread.Sleep(3000); Console.WriteLine($"NoReturnAsyncNoAwait End ThreadId={Thread.CurrentThread.ManagedThreadId}--{DateTime.Now.ToString("HH:mm:ss.fff")}"); }
1、无返回值,有Async,有Await
- await在方法体内部,只能放在async修饰的方法内,必须放在task前面
- 主线程到await这里就返回了,执行主线程任务
- task中的任务执行完毕以后,继续执行await后面的后续内容,有可能是子线程,也有可能是其他线程,甚至有可能是主线程来执行
- 类似ContinueWith回调await后面的后续内容
/// <summary> /// 无返回值,有Async,有Await /// </summary> private async void NoReturnAsyncAwait() { Debug.WriteLine($"NoReturnAsyncAwait--Start--{Thread.CurrentThread.ManagedThreadId.ToString("00")}--{DateTime.Now.ToString("HH:mm:ss.fff")}"); Task task = Task.Run(() => { Thread.Sleep(3000); Debug.WriteLine($"Task.Run--NoReturnAsyncAwait--{Thread.CurrentThread.ManagedThreadId.ToString("00")}--{DateTime.Now.ToString("HH:mm:ss.fff")}"); }); //主线程到await这里就返回了,执行主线程任务 //task中的任务执行完毕以后,继续执行await后面的后续内容,有可能是子线程,也有可能是其他线程,甚至有可能是主线程来执行 await task; Debug.WriteLine($"NoReturnAsyncAwait--End--{Thread.CurrentThread.ManagedThreadId.ToString("00")}--{DateTime.Now.ToString("HH:mm:ss.fff")}"); //类似ContinueWith回调await后面的后续内容 //task.ContinueWith(t => //{ // Debug.WriteLine($"NoReturnAsyncAwait--End--{Thread.CurrentThread.ManagedThreadId.ToString("00")}--{DateTime.Now.ToString("HH:mm:ss.fff")}"); //}); }
执行结果:
btnAwaitAsync_Click--Start--01--13:40:10.935 NoReturnAsyncAwait--Start--01--13:40:10.954 btnAwaitAsync_Click--End--01--13:40:10.987 Task.Run--NoReturnAsyncAwait--05--13:40:13.992 NoReturnAsyncAwait--End--01--13:40:14.034
2、返回Task,有Async,有Await
async Task == async void,Task和Task能够使用await, Task.WhenAny, Task.WhenAll等方式组合使用。Async Void 不行
方法:
/// <summary> /// 无返回值,返回Task,有Async,有Await /// </summary> /// <returns></returns> private async Task NoReturnTaskAsyncAwait() { Debug.WriteLine($"NoReturnTaskAsyncAwait--Start--{Thread.CurrentThread.ManagedThreadId.ToString("00")}--{DateTime.Now.ToString("HH:mm:ss.fff")}"); Task task = Task.Run(() => { Thread.Sleep(3000); Debug.WriteLine($"Task.Run--NoReturnTaskAsyncAwait--{Thread.CurrentThread.ManagedThreadId.ToString("00")}--{DateTime.Now.ToString("HH:mm:ss.fff")}"); }); await task; Debug.WriteLine($"NoReturnTaskAsyncAwait--End--{Thread.CurrentThread.ManagedThreadId.ToString("00")}--{DateTime.Now.ToString("HH:mm:ss.fff")}"); }
方法调用:
Task task = NoReturnTaskAsyncAwait(); Task.WhenAny(task).ContinueWith((a)=> { this.DoSomething("张三"); });
执行结果:
btnAwaitAsync_Click--Start--01--14:05:33.611 NoReturnTaskAsyncAwait--Start--01--14:05:33.624 btnAwaitAsync_Click--End--01--14:05:33.674 Task.Run--NoReturnTaskAsyncAwait--04--14:05:36.686 NoReturnTaskAsyncAwait--End--01--14:05:36.702 DoSomething--Start--张三--06--14:05:36.718 DoSomething--End--张三--06--14:05:38.733
3、返回Task int,有Async,有Await
方法:
/// <summary> /// 返回Task<int>,有Async,有Await /// </summary> /// <returns></returns> private Task<int> ReturnTaskIntAsyncAwait() { Debug.WriteLine($"ReturnTaskIntAsyncAwait--Start--{Thread.CurrentThread.ManagedThreadId.ToString("00")}--{DateTime.Now.ToString("HH:mm:ss.fff")}"); TaskFactory taskFactory = new TaskFactory(); Task<int> iResult = Task.Run(() => { Thread.Sleep(3000); Debug.WriteLine($"Task.Run--ReturnTaskIntAsyncAwait--{Thread.CurrentThread.ManagedThreadId.ToString("00")}--{DateTime.Now.ToString("HH:mm:ss.fff")}"); return 123; }); Debug.WriteLine($"ReturnTaskIntAsyncAwait--End--{Thread.CurrentThread.ManagedThreadId.ToString("00")}--{DateTime.Now.ToString("HH:mm:ss.fff")}"); return iResult; }
方法调用:
Task task = NoReturnTaskAsyncAwait(); Task.WhenAny(task).ContinueWith((a)=> { this.DoSomething("张三"); });
执行结果:
btnAwaitAsync_Click--Start--01--14:05:33.611 NoReturnTaskAsyncAwait--Start--01--14:05:33.624 btnAwaitAsync_Click--End--01--14:05:33.674 Task.Run--NoReturnTaskAsyncAwait--04--14:05:36.686 NoReturnTaskAsyncAwait--End--01--14:05:36.702 DoSomething--Start--张三--06--14:05:36.718 DoSomething--End--张三--06--14:05:38.733
4、返回Task int,有Async,有Await
要使用返回值就一定要等子线程计算完毕
方法:
/// <summary> /// 返回Task<int>,有Async,有Await /// </summary> /// <returns></returns> private Task<int> ReturnTaskIntAsyncAwait() { Debug.WriteLine($"ReturnTaskIntAsyncAwait--Start--{Thread.CurrentThread.ManagedThreadId.ToString("00")}--{DateTime.Now.ToString("HH:mm:ss.fff")}"); TaskFactory taskFactory = new TaskFactory(); Task<int> iResult = Task.Run(() => { Thread.Sleep(3000); Debug.WriteLine($"Task.Run--ReturnTaskIntAsyncAwait--{Thread.CurrentThread.ManagedThreadId.ToString("00")}--{DateTime.Now.ToString("HH:mm:ss.fff")}"); return 123; }); Debug.WriteLine($"ReturnTaskIntAsyncAwait--End--{Thread.CurrentThread.ManagedThreadId.ToString("00")}--{DateTime.Now.ToString("HH:mm:ss.fff")}"); return iResult; }
方法调用:
Task<int> result = ReturnTaskIntAsyncAwait(); Debug.WriteLine($"ReturnTaskIntAsyncAwait--Result--{result.Result}--{Thread.CurrentThread.ManagedThreadId.ToString("00")}--{DateTime.Now.ToString("HH:mm:ss.fff")}");
执行结果:
btnAwaitAsync_Click--Start--01--14:09:03.839 ReturnTaskIntAsyncAwait--Start--1--14:09:03.851 ReturnTaskIntAsyncAwait--End--1--14:09:03.874 Task.Run--ReturnTaskIntAsyncAwait--5--14:09:06.903 ReturnTaskIntAsyncAwait--Result--123--01--14:09:06.911 btnAwaitAsync_Click--End--01--14:09:06.917
5、返回Task,实现多个任务顺序执行不阻塞
/// <summary> /// 返回Task,实现多个任务顺序执行不阻塞 /// </summary> /// <returns>async 就只返回long</returns> private async Task ReturnTaskAsyncAwaits() { Debug.WriteLine($"ReturnTaskAsyncAwaits--Start--{Thread.CurrentThread.ManagedThreadId.ToString("00")}--{DateTime.Now.ToString("HH:mm:ss.fff")}"); await Task.Run(() => { this.DoSomething("task1"); }); await Task.Run(() => { this.DoSomething("task2"); }); await Task.Run(() => { this.DoSomething("task3"); }); Debug.WriteLine($"ReturnTaskAsyncAwaits--Start--{Thread.CurrentThread.ManagedThreadId.ToString("00")}--{DateTime.Now.ToString("HH:mm:ss.fff")}"); }
执行结果:
btnAwaitAsync_Click--Start--01--14:38:07.608 ReturnTaskAsyncAwaits--Start--01--14:38:07.627 DoSomething--Start--task1--04--14:38:07.703 btnAwaitAsync_Click--End--01--14:38:07.709 DoSomething--End--task1--04--14:38:09.719 DoSomething--Start--task2--04--14:38:09.753 DoSomething--End--task2--04--14:38:11.773 DoSomething--Start--task3--05--14:38:11.781 DoSomething--End--task3--05--14:38:13.799 ReturnTaskAsyncAwaits--Start--01--14:38:13.806
6、在Winform中存在特殊处理
更改控件的值,必须是(UI线程)主线程去执行,这跟Winform设计有关系,在Winform中,await后面的内容,都会让主线程来执行
方法:
private async Task<long> CalculationAsync(long total) { var task = await Task.Run(() => { Debug.WriteLine($"This is CalculationAsync Start,ThreadId={Thread.CurrentThread.ManagedThreadId}"); long lResult = 0; for (int i = 0; i < total; i++) { lResult += i; } Debug.WriteLine($"This is CalculationAsync End,ThreadId={Thread.CurrentThread.ManagedThreadId}"); return lResult; }); return task; //这句话必须由主线程来执行,线程在同一时刻只能做一件事儿 }
方法调用:
/// <summary> /// 在Winform中存在特殊处理 /// </summary> private async Task TextAsyncResultChange() { Debug.WriteLine($"TextAsyncResultChange--End--{Thread.CurrentThread.ManagedThreadId.ToString("00")}--{DateTime.Now.ToString("HH:mm:ss.fff")}"); long lResult = await this.CalculationAsync(1_000_000); //更改控件的值,必须是(UI线程)主线程去执行;这跟Winform设计有关系,在Winform中,await后面的内容,都会让主线程来执行 this.textAsyncResult.Text = lResult.ToString(); Debug.WriteLine($"TextAsyncResultChange--End--{Thread.CurrentThread.ManagedThreadId.ToString("00")}--{DateTime.Now.ToString("HH:mm:ss.fff")}"); }
执行结果:
btnAwaitAsync_Click--Start--01--15:31:16.624 TextAsyncResultChange--End--01--15:31:16.636 This is CalculationAsync Start,ThreadId=6 btnAwaitAsync_Click--End--01--15:31:16.735 This is CalculationAsync End,ThreadId=6 TextAsyncResultChange--End--01--15:31:16.787
三、Async/Await好处
- 既要有顺序,又要不阻塞,降低了编程难度
- 以同步编程的方式来写异步
- 提高性能和响应性: 异步操作可以在执行耗时的任务时释放主线程,使得程序能够在执行其他任务时继续响应用户输入,从而提高程序的响应速度和性能。
- 避免阻塞: 在进行网络请求、文件读写、数据库访问等 I/O 操作时,使用异步操作可以避免因等待这些操作完成而导致程序阻塞,使得系统资源得到更有效的利用。
- 简化并发编程: 异步编程模型可以简化并发编程,允许多个任务在不同线程上并发执行,而无需手动管理线程和同步操作。
- 提高代码清晰度:
async
和await
关键字使得异步代码的编写更加直观和易于理解。它们让代码看起来更像是同步代码,降低了编写异步代码的复杂性。 - 减少回调地狱(Callback Hell): 异步编程使用
await
可以避免深度嵌套的回调结构,使得代码更易读、更易维护。 - 异常处理: 异步方法可以更方便地使用
try-catch-finally
结构来捕获和处理异常,使得异步操作中的错误处理更加直观和有效。 - 支持并行操作: 异步方法可以与并行编程结合使用,允许多个异步任务同时执行,从而提高程序的并发性能。
- 与现有同步代码的兼容性: 异步方法可以与现有的同步代码进行良好的集成,允许在需要时将同步代码转换为异步代码,而不需要进行大规模的重构
四、Async/Await原理
如果给方法加上Async,在底层会生成一个状态机,一个对象在不同的状态可以执行的不同的行为
- 实例化状态机
- 把状态机实例交给一个build去执行
- 整理线程的上下文
- stateMachine.MoveNext();
- MoveNext如何执行,先获取一个状态,继续往后执行
- 如果有异常,抛出异常,把状态重置为-2
- 如果没有异常,把状态重置重置为-2
- SetResult();把结果包裹成一个Task
五、Async/Await优势场景
计算机的计算任务可以分成两类,计算密集型任务和IO密集型任务,async/await和Task相比,降低了线程使用数量,性能相当,不能提高计算速度,优势就是在同等硬件基础上系统的吞吐率更高,对计算密集型任务没有优势,IO密集型计算有优势,常见的IO密集型任务有:
- Web 开发:在 Web 开发中,异步操作对于处理大量并发请求非常重要。
async/await
可以用于处理 Web 请求、调用外部 API、数据库查询等任务,提高 Web 服务的吞吐量和性能。 - 文件读写(I/O 密集型操作):对于涉及网络请求、文件操作、数据库访问等 I/O 密集型操作,
async/await
能够显著提高性能。因为它允许系统在等待 I/O 操作完成时释放线程,而不会阻塞线程池中的线程,从而提高了系统的并发能力和资源利用率。 - UI 线程响应性:在桌面应用或移动应用中,使用
async/await
可以确保在进行耗时操作时不会阻塞 UI 线程,保持应用的响应性。比如,异步加载数据或处理后台任务,而不会阻塞用户界面的交互。 - 并发性和多任务处理:在处理多个并发任务时,
async/await
能够简化并发编程模型,使得代码更易读、更易维护。它可以与并行编程库(如Task.WhenAll
)结合使用,以便同时执行多个异步任务,并在所有任务完成后进行处理。 - 异步事件处理:对于处理事件驱动的异步操作,如异步响应用户输入、处理消息队列等,
async/await
可以让异步事件处理的代码更加简洁和可读。 - 资源的并发访问:在需要同时访问共享资源(如文件、数据库连接等)的情况下,
async/await
可以避免对资源的长时间占用,提高了资源的并发访问性能。
async/await
在处理涉及异步操作、I/O 密集型任务、并发性和响应性要求高的场景下具有明显的优势。它提供了一种清晰、简洁的编程模型,能够提高程序的性能、可维护性和可读性。