https://blog.csdn.net/weixin_43702146/article/details/125759979?utm_medium=distribute.pc_relevant.none-task-blog-2
https://www.jb51.net/article/237968.htm
https://www.cnblogs.com/yilezhu/p/12045018.html
https://www.bilibili.com/video/BV1vz4y1a7Vm/?spm_id_from=333.999.0.0&vd_source=3b08e97e50222fa2ec22737f6dcb2202
https://blog.csdn.net/S1lenceAAA/article/details/108386578
https://www.bilibili.com/video/BV16G4y1c72R/?spm_id_from=333.999.0.0&vd_source=3b08e97e50222fa2ec22737f6dcb2202
http://www.mobange.com/nav/net/99575.html
| using System; |
| using System.Threading.Tasks; |
| using System.Threading; |
| using System.Text; |
| using System.Linq; |
1. 前言
- Task是微软在.Net 4.0时代推出来的,Task看起来像一个Thread,实际上,它是在ThreadPool的基础上进行的封装,Task的控制和扩展性很强,在线程的延续、阻塞、取消、超时等方面远胜于Thread和ThreadPool,所以一经问世,基本ThreadPool就被取代了。
任务通常运行以异步方式在线程池线程上
- ThreadPool相比Thread来说具备了很多优势,但是ThreadPool却又存在一些使用上的不方便。比如:
ThreadPool不支持线程的取消、完成、失败通知等交互性操作;
ThreadPool不支持线程执行的先后次序;
以往,如果开发者要实现上述功能,需要完成很多额外的工作,现在,FCL中提供了一个功能更强大的概念:Task。Task在线程池的基础上进行了优化,并提供了更多的API。在FCL4.0中,如果我们要编写多线程程序,Task显然已经优于传统的方式。
- 执行工作 Task 对象通常以异步方式执行在线程池线程上而不是以同步方式在主应用程序线程,您可以使用 Status 属性,以及 IsCanceled, ,IsCompleted, ,和 IsFaulted 属性,以确定任务的状态。 大多数情况下,lambda 表达式用于指定的任务是执行的工作。
- 任务Task和线程Thread的区别:
任务是架构在线程之上的,也就是说任务最终还是要抛给线程去执行。
任务跟线程不是一对一的关系,比如开10个任务并不是说会开10个线程,这一点任务有点类似线程池,但是任务相比线程池有很小的开销和精确的控制。
- 综上,多线程:Thread->ThreadPool->delegate委托异步调用->Task。Task是微软建议使用的最新的多线程方法。是对ThreadPool的封装,增加了更多好用的功能。Task默认在线程池中创建线程,他会自行调度和回收不用的线程,减少每次重新创建线程的开销,而且创建的是背景线程。
2. 创建Task
(1)无返回值
| |
| |
| Task t1 = new Task(()=>{}); |
| t1.Start(); |
| |
| |
| var t2 = Task.Factory.StartNew(()=>{}); |
Task.Run 和 Task.Factory.StartNew 区别,所以我也就不需要展开告诉大家。
只需要知道 Task.Run 是在 dotnet framework 4.5 之后才可以使用,但是 Task.Factory.StartNew 可以使用比 Task.Run 更多的参数,可以做到更多的定制。
实际上 Task.Run(foo) 可以认为是对 Task.Factory.StartNew 封装,使用简单的默认的参数。如果需要自己定义很多参数,就请使用 Task.Factory.StartNew 定义参数。
可以认为 Task.Run 是简化的 Task.Factory.StartNew 的使用,除了需要指定一个线程是长时间占用的,否则就使用 Task.Run。
| |
| task = new Task(() => |
| { |
| Console.WriteLine("工作线程" + Thread.CurrentThread.ManagedThreadId); |
| }); |
| |
| task.RunSynchronously(); |
(2)有返回值
| Task<T> t = Task.Run(()=>{ |
| |
| return T; |
| }); |
(3)有参数
| void test(string str){...} |
| |
| Task tt = Task.Run(()=>test("123")); |
| |
| public Task StartNew( |
| Action<object> action, |
| object state |
| ) |
| |
| Task.Factory.StartNew(new Action(test), "123"); |
3.Task API
- Wait
就是等待任务执行(task1)完成,task1的状态变为Completed,才继续往下执行。
可用task1.Result获取执行结果(阻塞的,若task1未执行完会阻塞在Result)
| |
| task1.Wait(); |
| |
| task1.Wait(int millseconds, CancellationToken canceltoken); |
注意:Wait(int)若在timeout内未执行完,返回false后仍然会继续执行,线程不会结束。
| if(task1.Wait(3000)) |
| { |
| cancelSource.Cancel(); |
| } |
- WaitAll
等待task1,task2,task3都完成后在继续向下执行。
| Task.WaitAll(task1, task2, task3...); |
- WaitAny
等待task1,task2,task3任一完成后在继续向下执行。
| Task.WaitAny(task1, task2, task3...); |
- Task.ContinueWith
执行完task1后紧接着执行设定的任务
| |
| string result = Task.WhenAll(task1, task2, task3).ContinueWith<string>(()=>return "this is task result.."); |
| static void Main() |
| { |
| Console.WriteLine("准备洗衣服"); |
| |
| |
| Task<int> task = new Task<int>(() => |
| { |
| |
| int time = new Random().Next(2, 6); |
| Thread.Sleep(TimeSpan.FromSeconds(time)); |
| return time; |
| }); |
| |
| Console.WriteLine("开始洗衣服"); |
| |
| |
| task.Start(); |
| |
| Console.WriteLine("我去打王者,让洗衣机洗衣服"); |
| |
| Thread.Sleep(TimeSpan.FromSeconds(4)); |
| Console.WriteLine("打完王者了,衣服洗完了嘛?"); |
| |
| Console.WriteLine(task.IsCompleted); |
| if (task.IsCompleted) |
| Console.WriteLine("洗衣服花的时间:" + task.Result); |
| else |
| { |
| Console.WriteLine("在等洗衣机洗完衣服"); |
| task.Wait(); |
| Console.WriteLine("洗衣服花的时间:" + task.Result); |
| } |
| Console.WriteLine("洗完了,捞出衣服,晒衣服,继续打王者去"); |
| } |
task的暂停、继续、终止、延时执行
终止线程
| |
| CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); |
| |
| var token = cancellationTokenSource.Token; |
线程还未被调度执行,可通过token.Cancel()取消:
| |
| cancellationTokenSource.Cancel(); |
线程已经被调度正在运行过程中时,Cancel()已经无法取消了,这时就需要在线程中自己检测了:
| |
| if(token.IsCancellationRequested){ |
| Console.WriteLine("Cancel success.."); |
| } |
| |
| cancellationTokenSource.Register(()=>func)); |
Cancel()函数的重载
| |
| public void Cancel(); |
| |
| public void Cancel(bool throwOnFirstException); |
| |
| public void CancelAfter(int millisecondsDelay); |
| |
| public void CancelAfter(TimeSpan delay); |
方法-1
轮询token.IsCancellationRequested判断是否在线程外执行了token.Cancel()
| |
| class Program |
| { |
| static void Main(string[] args) |
| { |
| |
| CancellationTokenSource tokenSource = new CancellationTokenSource(); |
| |
| CancellationToken token = tokenSource.Token; |
| |
| |
| Task t = Task.Run(() => |
| { |
| |
| while(!tokenSource.IsCancellationRequested) |
| { |
| |
| Console.WriteLine("Task start!"); |
| |
| |
| Thread.Sleep(1000); |
| |
| |
| Console.WriteLine("Task finished!"); |
| } |
| }, tokenSource); |
| |
| Thread.Sleep(10); |
| |
| |
| while(true) |
| { |
| Console.Write("请切换到英文输入法"); |
| Console.WriteLine("取消任务请按Y"); |
| if((Console.ReadKey()).Key == ConsoleKey.Y) |
| { |
| |
| tokenSource.Cancel(); |
| tokenSource.Register(()=>Console.WriteLine("这是task被取消时自动执行的")); |
| } |
| } |
| } |
| } |
方法-2
在task内注册线程的Abort委托
| class Program |
| { |
| static void Main(string[] args) |
| { |
| |
| CancellationTokenSource tokenSource = new CancellationTokenSource(); |
| |
| CancellationToken token = tokenSource.Token; |
| |
| |
| Task t = Task.Run(() => |
| { |
| |
| using(tokenSource.Register((Thread.CurrentThread.Abort)) |
| { |
| while(true) |
| { |
| |
| Console.WriteLine("Task start!"); |
| |
| |
| Thread.Sleep(1000); |
| |
| |
| Console.WriteLine("Task finished!"); |
| } |
| }); |
| } |
| Thread.Sleep(10); |
| |
| |
| while(true) |
| { |
| Console.Write("请切换到英文输入法"); |
| Console.WriteLine("取消任务请按Y"); |
| if((Console.ReadKey()).Key == ConsoleKey.Y) |
| { |
| |
| tokenSource.Cancel(); |
| } |
| } |
| } |
| } |
方法3-TokenSource传入Task.Run作为参数
但这种方法只能在task.Start()但还未被调度执行前进行取消。若线程已开始运行直接Cancel()是取消不掉的。线程还是会执行完返回。
因此当线程中有while(true)循环时,用Cancel是不行的。
线程取消回调
通过Register方法注册的服务只会执行一次!
这里放线程被取消后要执行的代码。
| tokenSource.Token.Register(() => |
| { |
| IsStart = false; |
| }); |
暂停、继续
由ManualResetEvent的Set()和Reset()实现:
| public partial class Form1 : Form |
| { |
| |
| private ManualResetEvent ma; |
| private CancellationTokenSource tokenSource; |
| private CancellationToken token; |
| |
| static readonly object objLock = new object(); |
| |
| private bool IsStart = false; |
| private bool IsManualResetEvent = true; |
| public Form1() |
| { |
| ma = new ManualResetEvent(IsManualResetEvent); |
| tokenSource = new CancellationTokenSource(); |
| token = tokenSource.Token; |
| InitializeComponent(); |
| } |
| |
| |
| |
| |
| |
| |
| private void button1_Click(object sender, EventArgs e) |
| { |
| if (IsStart) |
| { |
| MessageBox.Show("程序已经启动!"); |
| return; |
| } |
| else |
| { |
| |
| tokenSource = new CancellationTokenSource(); |
| token = tokenSource.Token; |
| IsStart = true; |
| Task.Run(() => Start01(), tokenSource.Token); |
| Task.Run(() => Start02(), tokenSource.Token); |
| Task.Run(() => Start03(), tokenSource.Token); |
| Task.Run(() => Start04(), tokenSource.Token); |
| } |
| } |
| |
| public void Start01() |
| { |
| for (int i = 0; i < 9999; i++) |
| { |
| if (token.IsCancellationRequested) |
| { |
| return; |
| } |
| |
| string str = "计数器:" + i; |
| Invoke(new Action(() => { this.richTextBox1.Text = str; })); |
| ma.WaitOne(); |
| Thread.Sleep(100); |
| } |
| } |
| public void Start02() |
| { |
| for (int i = 0; i < 9999; i++) |
| { |
| if (token.IsCancellationRequested) |
| { |
| return; |
| } |
| |
| string str = "计数器:" + i; |
| Invoke(new Action(() => { this.richTextBox2.Text = str; })); |
| ma.WaitOne(); |
| Thread.Sleep(100); |
| } |
| } |
| public void Start03() |
| { |
| for (int i = 0; i < 9999; i++) |
| { |
| if (token.IsCancellationRequested) |
| { |
| return; |
| } |
| |
| string str = "计数器:" + i; |
| Invoke(new Action(() => { this.richTextBox3.Text = str; })); |
| ma.WaitOne(); |
| Thread.Sleep(100); |
| } |
| } |
| public void Start04() |
| { |
| for (int i = 0; i < 9999; i++) |
| { |
| if (token.IsCancellationRequested) |
| { |
| return; |
| } |
| |
| string str = "计数器:" + i; |
| Invoke(new Action(() => { this.richTextBox4.Text = str; })); |
| ma.WaitOne(); |
| Thread.Sleep(100); |
| } |
| } |
| |
| |
| |
| |
| |
| |
| |
| private void button3_Click(object sender, EventArgs e) |
| { |
| |
| if (IsStart) |
| { |
| ma.Reset(); |
| } |
| |
| } |
| |
| |
| |
| |
| |
| |
| private void button4_Click(object sender, EventArgs e) |
| { |
| if (IsStart) |
| { |
| ma.Set(); |
| } |
| |
| } |
| |
| |
| |
| |
| |
| |
| private void button2_Click(object sender, EventArgs e) |
| { |
| |
| tokenSource.Cancel(); |
| tokenSource.Token.Register(() => |
| { |
| IsStart = false; |
| }); |
| } |
| |
| |
| |
| |
| |
| |
| private void Form1_FormClosing(object sender, FormClosingEventArgs e) |
| { |
| |
| DialogResult result = MessageBox.Show("确定要关闭窗体吗?", "提示", MessageBoxButtons.YesNo, MessageBoxIcon.Information); |
| if (result == DialogResult.Yes) |
| { |
| |
| e.Cancel = false; |
| } |
| else |
| { |
| |
| e.Cancel = true; |
| } |
| } |
| |
| private void Form1_Load(object sender, EventArgs e) |
| { |
| |
| } |
| |
| |
| |
| |
| |
| |
| private void button5_Click(object sender, EventArgs e) |
| { |
| if (IsStart) |
| { |
| MessageBox.Show("程序已经启动!"); |
| return; |
| } |
| this.richTextBox1.Text = "计数器:0"; |
| this.richTextBox2.Text = "计数器:0"; |
| this.richTextBox3.Text = "计数器:0"; |
| this.richTextBox4.Text = "计数器:0"; |
| } |
| } |
| |
延时执行
可以在延迟 Xms 后执行一个 task 的需求,并要求能够实现在 xms 之前取消 task 的执行:
- .NET 4.5 以上或者 .netcore 的话,可以使用 Task.Delay() 来实现
| static void Main(string[] args) |
| { |
| var tokenSource = new CancellationTokenSource(); |
| var token = tokenSource.Token; |
| |
| tokenSource.Cancel(); |
| |
| |
| Task.Delay(1000, token).ContinueWith(t => |
| { |
| Console.WriteLine("hello world!"); |
| },token); |
| |
| Console.ReadLine(); |
| } |
- WaitOne(int milliseconds)
线程阻塞直到milliseconds毫秒或收到信号
| var tokenSource = new CancellationTokenSource(); |
| var token = tokenSource.Token; |
| |
| var task = Task.Factory.StartNew(() => |
| { |
| |
| token.WaitHandle.WaitOne(5000); |
| token.ThrowIfCancellationRequested(); |
| Console.WriteLine("Task started its work"); |
| }); |
4. 异步方法
使用 async 修饰符可将方法、lambda 表达式或匿名方法指定为异步。
使用 async 修饰的方法,称为异步方法。
await必须使用在异步方法中。async和await必须搭配使用。
说白了await/async就是一个语法糖,单纯使用await/async并不会创建一个线程,创建线程的这个操作由是线程池决定的。识别性更高:
以async声明异步方法,但仍然需要在方法内部创建线程或任务以异步执行想要的语句,如Task。
(1)异步方法无返回值
无返回值的异步方法不需要获得返回值,因此无需阻塞主线程获取返回值。
| CalculateSum3(1, 2); |
| public static async void CalculateSum3(int a, int b) |
| { |
| int c = 0; |
| await Task.Run(() => |
| { |
| Console.WriteLine(Thread.CurrentThread.ManagedThreadId + ":start"); |
| Thread.Sleep(1000 * 3); |
| Console.WriteLine(Thread.CurrentThread.ManagedThreadId + ":end"); |
| c = a + b; |
| }); |
| Console.WriteLine(c); |
| } |
| |
| |
| Task calculateSum2 = CalculateSum2(1, 2); |
| public static async Task CalculateSum2(int a, int b) |
| { |
| int c = 0; |
| await Task.Run(() => |
| { |
| Console.WriteLine(Thread.CurrentThread.ManagedThreadId + ":start"); |
| Thread.Sleep(1000 * 3); |
| Console.WriteLine(Thread.CurrentThread.ManagedThreadId + ":end"); |
| c = a + b; |
| }); |
| Console.WriteLine(c); |
| } |
(2)异步方法有返回值
异步方法返回值只能是void,Task,或者Task< T >,我们添加返回值Task< int >
| |
| Task<int> tt = CalculateSum(1, 2); |
| int res = tt.Result; |
| |
| public static async Task<int> CalculateSum(int a, int b) |
| { |
| int c = 0; |
| await Task.Run(() => |
| { |
| Console.WriteLine(Thread.CurrentThread.ManagedThreadId + ":start"); |
| Thread.Sleep(1000 * 3); |
| Console.WriteLine(Thread.CurrentThread.ManagedThreadId + ":end"); |
| c = a + b; |
| }); |
| Console.WriteLine("计算结果:" + c); |
| return c; |
| } |
(3)阻塞主线程获取异步方法返回值
- Task.Result
子线程本身不会被阻塞,主线程只有调用Task.Result处会被阻塞。
| Console.WriteLine("start"); |
| Task<int> task = CalculateSum(1, 2); |
| Task<int> task2 = CalculateSum(1, 2); |
| Task<int> task3 = CalculateSum(1, 2); |
| Task<int> task4 = CalculateSum(1, 2); |
| Console.WriteLine("end"); |
| Console.WriteLine("开始等待返回结果"); |
| Console.WriteLine(task.Result); |
| Console.WriteLine(task2.Result); |
| Console.WriteLine(task3.Result); |
| Console.WriteLine(task4.Result); |
| Console.WriteLine("等待返回结果完毕,程序结束"); |
- await
主线程被阻塞在await处,当调用的子线程返回值时才会继续向下执行。
| static void Main(string[] args) |
| { |
| test(); |
| } |
| |
| public static async void test() |
| { |
| int res = await CalculateSum(1, 2); |
| int res2 = await CalculateSum(1, 2); |
| int res3 = await CalculateSum(1, 2); |
| int res4 = await CalculateSum(1, 2); |
| } |
5. 坑
(1)在UI线程中调用async方法,依靠异步方法返回值task.Result修改控件值,会卡死。
https://www.cnblogs.com/guanglin/p/11677105.html
- 原因
Task.Result会直接阻塞当前调用线程,而aysnc、await机制是使用状态机切换上下文,
当调用.Result时,当前线程阻塞,async/await状态机无法切换回线程上下文,导致死锁。
- 解决
不用async/await异步方法。直接在普通函数里Task.Run
(2)CancellationTokenSource调用Cancel()后要重新new对象传入Task.Run否则Task不会启动
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了