Task、await、async的使用
异步与同步》
异步:不阻塞线程
同步:阻塞线程
异步方法、同步方法》
即某一方法在执行时如果后续的代码不能执行只有等待该方法执行完成后才能继续执行(造成UI卡死现象)的方法称之为同步方法,反之如果该方法在执行阶段后续代码依旧可以执行,这个方法称之为异步方法。
多线程编写》
Thread》
之前使用最多的一种。
看一个例子【输出1到10每秒输出一个数字】
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
Thread th = new Thread(() =>
{
for (int index = 1; index <11; index++)
{
Console.WriteLine(index);
Thread.Sleep(1000);
}
});
th.Start();
Console.ReadLine();
}
上述是最基本的使用,关于Thread不多做介绍。
ThreadPool》
ThreadPool(线程池)是一种并发编程的概念,它是一组可重复使用的线程,用于执行多个任务。线程池管理着一个线程队列,其中包含多个线程,这些线程可以执行任务。
使用线程池的好处是可以避免频繁地创建和销毁线程,从而提高程序的性能和效率。线程池可以控制并发线程的数量,避免系统资源被过度占用。当有任务需要执行时,线程池中的线程会被分配给任务,并在任务完成后返回线程池,等待下一个任务的到来。
线程池通常由以下几个组件组成:
任务队列:用于存储待执行的任务。
线程池管理器:用于创建、销毁和管理线程池中的线程。
工作线程:线程池中的线程,用于执行任务。它们从任务队列中获取任务并执行,执行完任务后返回线程池等待下一个任务。
一个案例:
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
for (int i = 1; i <= 10; i++)
{
ThreadPool.QueueUserWorkItem(new WaitCallback((t) => {
Console.WriteLine($"这是第{t}个任务~");
}), i);
}
Console.ReadLine();
}
ThreadPool几乎没用过(惭愧啊)。
Task》
重点介绍一下Task
Task与Thread相比有哪些优缺点?
Task(任务)和Thread(线程)是并发编程中常用的两个概念,它们有各自的优缺点。
优点:
灵活性:Task比Thread更加灵活。Task通常是以异步的方式执行,可以在需要时启动、暂停、取消或等待任务的完成。而Thread是同步执行的,一旦启动就会一直执行直到结束。
资源消耗:Task比Thread消耗的资源更少。Task利用线程池来执行任务,可以重用线程,避免频繁创建和销毁线程的开销,从而减少系统资源的消耗。
异常处理:Task可以更好地处理异常。Task可以通过异常处理机制捕获和处理任务中的异常,而Thread需要开发者自行处理异常。
缺点:
复杂性:相比于Thread,Task的使用可能更加复杂
Task的创建》还是【输出1到10每秒输出一个数字】为例
方法1
如下创建方式
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
Task tk = new Task(()=>
{
for (int i = 1; i <= 10; i++)
{
Thread.Sleep(1000);
Console.WriteLine(i);
}
});
tk.Start();
Console.ReadLine();
}
方法二:
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
Task tk = Task.Factory.StartNew(() =>
{
for (int i = 1; i <= 10; i++)
{
Thread.Sleep(1000);
Console.WriteLine(i);
}
});
Console.ReadLine();
}
可以看到我并没有使用tk.Start();来启动,因为这种创建方式当创建好后就表示启动(窗机即启动)。
Task tk = new Task()
:这种方式只是创建了一个Task
对象,但并没有立即启动它。需要调用tk.Start()
方法来手动启动任务。这种方式适用于需要手动控制任务的启动时机,或者需要在任务启动前进行一些其他操作的情况。Task.Factory.StartNew()
:这种方式创建并立即启动一个Task
对象。它使用Task
类的工厂方法来创建任务,并自动启动任务。这种方式更为简洁,适用于直接启动任务且不需要手动控制启动时机的情况。
方法三:
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
Task tk = Task.Run(() =>
{
for (int i = 1; i <= 10; i++)
{
Thread.Sleep(1000);
Console.WriteLine(i);
}
});
Console.ReadLine();
}
Task tk = Task.Run():这种方式是.NET Framework 4.5及更高版本引入的简化创建和启动任务的方法。它会创建并立即启动一个 Task 对象,类似于 Task.Factory.StartNew(),但是更为简洁。它会自动使用默认的 TaskScheduler 来调度任务,并且返回一个已启动的 Task 对象。
关于await、async》
这两玩意是干啥的?
解释这个问题先看一下之前写的同步方法、与异步方法。
还是以【输出1到10每秒输出一个数字】为例说明。
同步方法最基本的写法来完成这个需求:
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
for (int i = 1; i <= 10; i++)
{
Thread.Sleep(1000);
Console.WriteLine(i);
}
Console.ReadLine();
}

执行结果没毛病,是每秒输出一个。但是我代码后面有一句[Console.ReadLine();]读取用户输入的操作,在代码输出1-10期间我是不能输入的因为这是同步方法,我的UI是卡死的(控制台程序还不太能看出来卡死在大多数窗体应用中非常明显,这样用户体验会非常差),所以来看看异步方法。
确保画面不卡死的情况下完成这个需求:
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
Task tk = Task.Factory.StartNew(() =>
{
for (int i = 1; i <= 10; i++)
{
Thread.Sleep(1000);
Console.WriteLine(i);
}
});
Console.ReadLine();
}

当数字在输出阶段我还是可以手动输入内容的,这样就不会造成UI卡死的现象。
但是这并不是一个异步方法来完成的。
Task.Factory.StartNew
方法用于创建并启动一个新的 Task
对象。然而,代码中使用的是 Thread.Sleep
方法而不是 Task.Delay
方法来进行延迟,这意味着任务是通过线程阻塞来实现延迟的。
由于使用了 Thread.Sleep
方法,这个任务实际上是同步执行的,而不是异步的。
使用异步方法来完成这个需求:
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
Task tk = Task.Factory.StartNew(async () =>//async 来修饰异步方法
{
for (int i = 1; i <= 10; i++)
{
await Task.Delay(1000);//await 表示等待异步执行完成
Console.WriteLine(i);
}
});
Console.ReadLine();
}
上面是代码(注释部分先看一眼,后面解释),下面是一些截图

解释一下异步方法中的awiat、async(重点)》
将上述方法小小的修改一下,先去掉await、async,如何用一个方法来实现一些:
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
TaskSleep();
Console.ReadLine();
}
public static void TaskSleep()
{
for (int i = 1; i <= 10; i++)
{
Task.Delay(1000);
Console.WriteLine(i);
}
}
我调用写的TaskSleep来完成输出,但是效果并不是一秒一个而是瞬间全部输出,

这是因为Task.Delay()方法是一个异步方法,既然是异步方法,那么这个执行就不会阻塞后面的输出了,我要如何让后面的输出等待这个异步方法完成之后再去输出?
await来了》
为了让异步方法也可以实现类似于”阻塞“的效果就让这个await来完成吧

再改一下上述代码:
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
TaskSleep();
Console.ReadLine();
}
public static void TaskSleep()
{
for (int i = 1; i <= 10; i++)
{
await Task.Delay(1000);
Console.WriteLine(i);
}
}

报错啊?
这时就要看async了

可以看到await只能用在异步方法中,所以要使用async来修饰方法位异步方法,再改代码如下:
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
TaskSleepAsync();
Console.ReadLine();
}
public static async Task TaskSleepAsync()
{
for (int i = 1; i <= 10; i++)
{
await Task.Delay(1000);
Console.WriteLine(i);
}
}
看一下效果是可以完成需求的标准,一秒输出一个的。

为什么TaskSleepAsync下面有个绿波浪线?

这是因为我的TaskSleepAsync是一个异步方法,但是却没有实现等待(await),也就是说并不会等待我的异步方法执行完成再执行后续代码而是直接执行后续代码,这也就是为什么在输出进行中时我还能输入。
修改一些代码让这个问题看起来明显一点:

如果后续的代码要等待我异步方法执行完后再执行,但是我异步方法还没开始输出就输出【异步方法执行完成!】是不是很奇怪?
给这个异步方法加上await来试试

这下就没问题了。
综上所述,awiat、async就是原来修饰一个方法位异步方法的,await就是来等待这个异步方法完成的,使用await时被修改的方法必须是异步的,而且调用者本身也是由async所修饰的。
异步方法的返回值》
异步方法也可以有返回值如下:

如果不适应await修饰时,返回类型就是一个Task<int>了!

因为没有使用await修饰所以不会等待异步完成后执行后续代码,但是异步方法依旧是执行的,这点可以从执行结果看出来。

我还可以如下这样,这时就可以看到等待的效果,而且a执行的结果使用int接收也没问题。

输出1到10的内容是在TaskSleepAsync
方法中完成的。在TaskSleepAsync
方法中,使用了Console.WriteLine(i)
语句,在每次循环中输出当前的计数值。这意味着无论是否等待异步方法的完成,都会输出10次计数。
int b = await a;
语句只是等待异步方法的完成,并将返回的结果赋值给变量b
。它并不会触发Console.WriteLine(i)
语句的执行。
另一种:

a.Result表示等待异步方法TaskSleepAsync的完成,并获取其返回的结果。通过使用a.Result,我们可以阻塞当前线程,直到异步方法完成并返回结果。
a.Result和await都可以用于等待异步方法的完成并获取其返回的结果,但它们有一些重要的区别。 阻塞 vs 非阻塞:使用a.Result会阻塞当前线程,直到异步方法完成并返回结果。这意味着在等待期间,线程会被阻塞,无法执行其他任务。而使用await关键字时,当前线程会被释放,可以执行其他任务,直到异步方法完成后再继续执行。
也就是说使用a.Result等待的话画面会卡死,但是await不会