Task和asyncawait详解
Task
本篇为Task进阶篇,Task的基础在专栏往期C#博客有具体讲解,可以关注追更。
传送门
link
什么是异步和同步
C#中同步
和异步
是两种不同的编程模式,用于控制代码的执行方式。
同步模式是指代码按照从上到下
的顺序依次执行,在执行一个任务时会一直阻塞在那里等待
其完成。
而异步模式则是指在调用一个任务后立即返回,不会等待
这个任务完成,而是通过回调机制在任务完成时通知调用者,可以同时执行
多个任务,提高程序的效率。
使用场景
:当你有一些可能会阻塞线程(如IO操作、网络请求、复杂计算等)的操作时,可以将它们封装为任务,并使用Task
类来异步执行它们。这样可以避免阻塞UI线程或其他重要线程,提高应用程序的响应性和性能。此外,通过使用上述的方法,你可以轻松地控制任务的执行情况。
Task的创建与运行
Task(任务)是在ThreadPool线程池
的基础上推出的,ThreadPool中有若干数量的线程,如果有任务需要处理时,会从线程池中获取一个空闲的线程来执行任务,任务执行完毕后线程不会销毁,而是被线程池回收
以供后续任务使用。当线程池中所有的线程都在忙碌
时,又有新任务要处理时,线程池才会新建一个线程来处理该任务,如果线程数量达到设置的最大值,任务会排队,等待其他任务释放线程后再执行。线程池能减少
线程的创建,节省开销
关键点提取
:
- 某种程度上说,一个task类似于一个thread(线程)或者
ThreadPool
(线程池)中的工作项(线程),但是Task处于一个更高
的抽象上(封装了线程的操作) - Task对象记录了该任务的
状态
,运行结果 任务的操作方法等信息
Status属性为任务的当前状态 - Task
四种
状态
Created 任务创建完成
WaitingToRun 已经启动 等待执行
Runing 正在执行
RunToComletion 执行完成
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using static System.Windows.Forms.VisualStyles.VisualStyleElement;
namespace _06_Task
{
public partial class Form1 : Form
{
//Task 任务
//某种程度上说,一个task类似于一个thread(线程)或者ThreadPool(线程池)中的工作项(线程),但是Task处于一个更高的抽象上(封装了线程的操作)
public Form1()
{
InitializeComponent();
}
Task tk;
private void button1_Click(object sender, EventArgs e)
{
//创建任务
tk = new Task(Fn);
//启动任务
tk.Start();
//Task对象记录了该任务的状态,运行结果 任务的操作方法等信息
//Status属性为任务的当前状态
Task tk1 = Task.Run(Fn);//创建并启动
Console.WriteLine(tk.Status);
}
void Fn()
{
Console.WriteLine(tk.Status);
Thread.Sleep(2000);
Invoke(new Action(() => {
label1.Text = "老坛酸菜踩好了";
}));
}
}
}
代码解释
:
在这个示例中,有一个方法被封装为任务(
Task
)并异步执行。
Fn()
:这个方法模拟了一个耗时操作。它让当前线程休眠2秒,然后更新UI元素label1
的文本。在
button1_Click
事件处理器中,首先创建了一个新的任务tk
,并异步执行Fn()
方法。然后,打印出任务的状态。注意,任务的状态可以通过Task.Status
属性获取,它返回一个TaskStatus
枚举值,表示任务的当前状态。
Task的控制
使用场景
:
关键点提取
:
Task.Wait()
:阻塞当前线程直到任务完成。Task.WaitAll(t1, t2)
:阻塞当前线程直到所有任务都完成。Task.WaitAny(t1, t2)
:阻塞当前线程直到任何一个任务完成。t1.ContinueWith(t => ...)
:注册一个回调函数,该函数会在t1
任务完成时执行。TaskFactory.ContinueWhenAll(new Task[] { t1, t2 }, t => ...)
:注册一个回调函数,该函数会在所有任务都完成时执行。TaskFactory.ContinueWhenAny(new Task[] { t1, t2 }, t => ...)
:注册一个回调函数,该函数会在任何一个任务完成时执行。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace _07_Task的控制
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
void Fn1()
{
Thread.Sleep(1000);
Console.WriteLine("Fn1方法执行完成");
} void Fn2()
{
Thread.Sleep(1000);
Console.WriteLine("Fn2方法执行完成");
}
private void button1_Click(object sender, EventArgs e)
{
//Task和线程相比的优势可以灵活控制
//1.task.Wait()等待任务完成(一般用于在任务执行中等待另一个任务)
//Task t1 = Task.Run(Fn1);
//t1.Wait();
将会卡住当前(主)线程,直到任务执行完毕
//Console.WriteLine("任务执行完毕");
//2.Task.WaitAll 等待所有任务执行完成,再执行(主)线程
//Task t1 = Task.Run(Fn1);
//Task t2 = Task.Run(Fn2);
//Task.WaitAll(t1,t2);
//Console.WriteLine("所有任务执行完毕,主线程执行");
//3..Task.WaitAny 等待某个任务执行完成,再执行(主)线程
Task t1 = Task.Run(Fn1);
Task t2 = Task.Run(Fn2);
Task.WaitAll(t1, t2);
Console.WriteLine("t1或t2其中一个任务执行完毕,主线程执行");
//4.不阻塞的等待(一个任务)
t1.ContinueWith((t) =>
{
Console.WriteLine("t1任务完成了,可以执行其他任务了");
});
//5.不阻塞的等待(多个任务)
//并且等待所有任务完成
TaskFactory factory = new TaskFactory();
factory.ContinueWhenAll(new Task[] { t1, t2 }, t =>
{
Console.WriteLine("所有任务执行完毕,主线程执行");
});
//并且等待其中一个任务完成
TaskFactory factory1 = new TaskFactory();
factory1.ContinueWhenAny(new Task[] { t1, t2 }, t =>
{
Console.WriteLine("t1或t2执行完毕,主线程执行");
});
}
}
}
代码解释
在这个示例中,有两个方法被封装为任务(
Task
)并异步执行。
Fn1()
和Fn2()
:这两个方法模拟了两个耗时操作。它们分别让当前线程休眠1秒,然后在控制台打印一条消息。在
button1_Click
事件处理器中,首先创建了两个新的任务t1
和t2
,分别异步执行Fn1()
和Fn2()
方法。然后,展示了如何使用Task.Wait()
,Task.WaitAll()
,Task.WaitAny()
,Task.ContinueWith()
,TaskFactory.ContinueWhenAll()
, 和TaskFactory.ContinueWhenAny()
方法来控制任务的执行。
带有返回值的Task
使用场景
:当Task运行的方法有返回值
时
关键点提取
:
-
当我们创建任务的时候,传递的是一个有返回值的方法,就会得到一个
泛型
任务,Task<方法的返回值类型> -
直接使用task.Result获取任务的结果时.会
自动
等待任务完成(会隐式调用wait方法)
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace _08带有返回值的Task
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
int Fn()
{
int sum = 0;
for (int i = 0; i < 100; i++)
{
Thread.Sleep(2);
sum += i;
}
return sum;
}
string MrWang()
{
Thread.Sleep(2000);
Console.WriteLine("王先生下班了");
return "王先生";
}
string MissLi()
{
Thread.Sleep(4000);
Console.WriteLine("李小姐下班了");
return "李小姐";
}
private void button1_Click(object sender, EventArgs e)
{
//启动任务并进行计算
//当我们创建任务的时候,传递的是一个有返回值的方法,就会得到一个泛型任务,Task<方法的返回值类型>
Task<int> task = Task.Run(Fn);
//task.Result获取任务的执行结果 当直接使用task.Result获取任务的结果时.会自动等待任务完成(会隐式调用wait方法)
Console.WriteLine(task.Result);
//当需要获取结果时不阻塞其他线程进行,可以使用ContinueWith
task.ContinueWith(t =>
{
Console.WriteLine(task.Result);
});
Task<string> t2=Task.Run(MrWang);
Task<string> t3=Task.Run(MissLi);
//要同时获取t2和t3的结果,可以使用上节TaskFactory 类的ContinueWhenAll
//比如王先生和李小姐都下班了,执行一起吃饭
TaskFactory tf= new TaskFactory();
tf.ContinueWhenAll(new Task<string>[] { t2, t3 }, t =>
{
Console.WriteLine("都下班了,一起吃饭");
});
//要只获取t2和t3的其中一个结果,可以使用上节TaskFactory 类的ContinueWhenAny方法
//比如王先生和李小姐谁先下班了,谁回家做饭
tf.ContinueWhenAny(new Task<string>[] { t2, t3 }, t =>
{
Console.WriteLine($"{t.Result}先下班了,回家做饭");
});
}
}
}
代码解释
在这个示例中,有三个方法被封装为任务(
Task<T>
)并异步执行。
Fn()
:这个方法计算0到99的和,并返回结果。它被封装在Task.Run(Fn)
中以在一个新的线程上运行,并返回一个Task<int>
对象。MrWang()
和MissLi()
:这两个方法模拟了两个人下班的情况。它们分别让当前线程休眠2秒和4秒,然后在控制台打印一条消息,并返回一个字符串。它们也被封装在Task.Run
中以在新的线程上运行,并返回Task<string>
对象。在
button1_Click
事件处理器中,首先使用Task<int> task = Task.Run(Fn)
来异步执行Fn()
方法,并返回一个Task<int>
对象。然后,使用Console.WriteLine(task.Result)
来获取并打印任务的结果。注意,直接访问task.Result
会阻塞当前线程直到任务完成。为了避免阻塞,可以使用task.ContinueWith(t => Console.WriteLine(task.Result))
来注册一个回调函数,该函数会在任务完成时执行,并打印出结果。接着,创建了两个新的任务
t2
和t3
,分别异步执行MrWang()
和MissLi()
方法。然后,使用TaskFactory.ContinueWhenAll
方法来注册一个回调函数,该函数会在t2
和t3
都完成时执行,并打印出一条消息。最后,使用TaskFactory.ContinueWhenAny
方法来注册另一个回调函数,该函数会在t2
和t3
中任何一个完成时执行,并打印出一条消息。
async/await
使用场景
:当你有一些可能会阻塞
线程(如IO操作、网络请求、复杂计算等)的操作时,可以将它们封装
为可等待的方法,然后使用async/await
关键字来异步执行它们。这样可以避免
阻塞UI线程或其他重要线程,提高
应用程序的响应性和性能。
关键点提取
- 使用await 关键字等待一个任务执行完成,如果该任务有返回值,则可以使用
变量
接收
注意:await 关键字 只能使用在async修饰的函数中
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace _09_async_await
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
string MrWang()
{
Thread.Sleep(1000);
return "王先生";
} void MissLi()
{
Thread.Sleep(5000);
Console.WriteLine("王小姐下班了");
}
private void button1_Click(object sender, EventArgs e)
{
Task<string> t1 = Task.Run(MrWang);
t1.ContinueWith(t =>
{
Console.WriteLine(t.Result + "下班了");
});
}
private async void button2_Click(object sender, EventArgs e)
{
//简写为:
//使用await 关键字等待一个任务执行完成,如果该任务有返回值,则可以使用变量接收
//注意:await 关键字 只能使用在async修饰的函数中
string s = await Task<string>.Run(MrWang);
await Console.Out.WriteLineAsync(s + "下班了");
//如果没有返回值 直接await即可
await Task<string>.Run(MissLi);
}
}
}
代码解释
在这个示例中,有两个方法被封装为任务(
Task
或Task<T>
)并异步执行。
MrWang()
:这个方法会让当前线程休眠1秒钟,然后返回字符串"王先生"。MissLi()
:这个方法会让当前线程休眠5秒钟,然后在控制台打印"王小姐下班了"。在
button1_Click
事件处理器中,首先使用Task<string> t1 = Task.Run(MrWang)
来异步执行MrWang()
方法,并返回一个Task<string>
对象。然后,使用t1.ContinueWith(t => Console.WriteLine(t.Result + "下班了"))
来注册一个回调函数,该函数会在t1
任务完成时执行,并打印出结果。在
button2_Click
事件处理器中,首先使用string s = await Task<string>.Run(MrWang)
来异步执行MrWang()
方法,并等待它完成并获取结果。然后,使用await Console.Out.WriteLineAsync(s + "下班了")
来异步打印结果。最后,使用await Task<string>.Run(MissLi)
来异步执行MissLi()
方法,并等待它完成。
封装可等待方法
使用场景
:当你有一些可能会阻塞线程(如IO操作、网络请求、复杂计算等)的操作时,可以将它们封装为可等待的方法,然后使用async/await
关键字来异步执行它们。这样可以避免阻塞UI线程或其他重要线程,提高应用程序的响应性和性能。
关键点提取
- await Console.Out.WriteLineAsync(item);
等同于Console.WriteLine(); - await 关键字 只能等待一个Task
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace _10_封装可等待的方法
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
void Fn()
{
Thread.Sleep(1000);
Console.WriteLine("结束");
}
//如果我们需要让我们的方法可以被等待,只需要让我们的方法返回一个Task即可
Task<string[]> ReadDir()
{
Task<string[]> t = Task.Run(() =>
{
Thread.Sleep(2000);
return new string[] { "目录1","目录2" };
});
return t;
}
private async void button1_Click(object sender, EventArgs e)
{
//await 关键字 只能等待一个Task
await Task.Run(Fn);
string[]s= await ReadDir();
foreach(var item in s)
{
await Console.Out.WriteLineAsync(item);
//等同于Console.WriteLine();
}
}
}
}
代码解释
这段代码是一个Windows Forms应用程序,其中使用了C#的
async/await
关键字来实现异步编程。在这个示例中,有两个方法被封装为可等待(返回Task
或Task<T>
)的方法。
Fn()
:这个方法会让当前线程休眠1秒钟,然后在控制台打印"结束"。它被封装在Task.Run(Fn)
中以在一个新的线程上运行,并返回一个Task
对象,这样就可以使用await
关键字等待它完成。ReadDir()
:这个方法模拟了读取目录的操作。它创建并返回一个Task<string[]>
对象,该任务会在新的线程上运行,休眠2秒钟(模拟耗时的IO操作),然后返回一个字符串数组。因为它返回了一个Task<string[]>
,所以可以使用await
关键字等待它完成并获取结果。在
button1_Click
事件处理器中,首先使用await Task.Run(Fn)
来异步执行Fn()
方法,并等待它完成。然后,使用string[] s = await ReadDir()
来异步执行ReadDir()
方法,并等待它完成并获取结果。最后,遍历结果数组,并使用await Console.Out.WriteLineAsync(item)
来异步打印每个元素
总结
Task和async/await是现代编程语言中用于简化异步编程
的两个关键概念。它们在处理长时间运行
的任务时非常有用,可以在等待结果的同时释放
线程以处理其他
工作,从而提高应用程序的响应性和性能。
Task和async/await协同
工作,提供了一种强大的异步编程模型
。Task负责执行后台操作,而async/await提供了一种优雅的编写异步代码的方式。使用async/await时,开发者可以编写出易于理解和维护
的异步代码,同时保持
程序的响应性和性能。
拓展(博客推荐)
传送门
link
传送门
link
传送门
link
如果觉得文章还不错,可以点赞、收藏和转发,以支持作者继续创作更多教程。 另外本专栏将会持续更新,作者专栏中有已经更新完毕的C#基础教程!!!
————————————————