C#多线程和异步编程——Task和async/await详解
目录
2 Task的阻塞方法(Wait/WaitAll/WaitAny)
一、什么是异步
同步和异步主要用于修饰方法。当一个方法被调用时,调用者需要等待该方法执行完毕并返回才能继续执行,我们称这个方法是同步方法;当一个方法被调用时立即返回,并获取一个线程执行该方法内部的业务,调用者不用等待该方法执行完毕,我们称这个方法为异步方法。
异步的好处在于非阻塞(调用线程不会暂停执行去等待子线程完成),因此我们把一些不需要立即使用结果、较耗时的任务设为异步执行,可以提高程序的运行效率。net4.0在ThreadPool的基础上推出了Task类,微软极力推荐使用Task来执行异步任务,现在C#类库中的异步方法基本都用到了Task;net5.0推出了async/await,让异步编程更为方便。本篇主要介绍Task、async/await相关的内容,其他异步操作的方式会在下一篇介绍。
二、Task介绍
Task是在ThreadPool的基础上推出的,我们简单了解下ThreadPool。ThreadPool中有若干数量的线程,如果有任务需要处理时,会从线程池中获取一个空闲的线程来执行任务,任务执行完毕后线程不会销毁,而是被线程池回收以供后续任务使用。当线程池中所有的线程都在忙碌时,又有新任务要处理时,线程池才会新建一个线程来处理该任务,如果线程数量达到设置的最大值,任务会排队,等待其他任务释放线程后再执行。线程池能减少线程的创建,节省开销,看一个ThreadPool的栗子吧
static void Main(string[] args)
{
for (int i = 1; i <=10; i++)
{
//ThreadPool执行任务
ThreadPool.QueueUserWorkItem(new WaitCallback((obj) => {
Console.WriteLine($"第{obj}个执行任务");
}),i);
}
Console.ReadKey();
}
上边的代码通过ThreadPool执行了10个任务
ThreadPool相对于Thread来说可以减少线程的创建,有效减小系统开销;但是ThreadPool不能控制线程的执行顺序,我们也不能获取线程池内线程取消/异常/完成的通知,即我们不能有效监控和控制线程池中的线程。
1 Task创建和运行
我们知道了ThreadPool的弊端:我们不能控制线程池中线程的执行顺序,也不能获取线程池内线程取消/异常/完成的通知。net4.0在ThreadPool的基础上推出了Task,Task拥有线程池的优点,同时也解决了使用线程池不易控制的弊端。
首先看一下怎么去创建并运行一个Task,Task的创建和执行方式有如下5种:
static void Main(string[] args)
{
//1. new方式实例化一个Task,需要通过Start方法启动
Task task = new Task(() =>
{
Thread.Sleep(100);
Console.WriteLine($"hello, task1的线程ID为{Thread.CurrentThread.ManagedThreadId}");
});
task.Start();
//2. Task.Factory.StartNew(Action action)创建和启动一个Task
Task task2 = Task.Factory.StartNew(() =>
{
Thread.Sleep(100);
Console.WriteLine($"hello, task2的线程ID为{ Thread.CurrentThread.ManagedThreadId}");
});
//3. Task.Run(Action action)将任务放在线程池队列,返回并启动一个Task
Task task3 = Task.Run(() =>
{
Thread.Sleep(100);
Console.WriteLine($"hello, task3的线程ID为{ Thread.CurrentThread.ManagedThreadId}");
});
Console.WriteLine("执行主线程!");
Console.ReadKey();
//4. 开启一个新线程,指定work方法
Task.Factory.StartNew(Work);
static void Work()
{
_running = true;
while (_running)
{
//SendOut();
//DealWith();
Thread.Sleep(50);
}
}
//5. 这种方法也可以跨线程调用UI控件
Action action = () =>
{
UiManager.SurfaceForm.SetSurplusPhotoNumber(count1.ToString());
};
Task.Factory.StartNew(action);
}
我们看到先打印"执行主线程",然后再打印各个任务,说明了Task不会阻塞UI主线程。上边的栗子Task都没有返回值,我们也可以创建有返回值的Task<TResult>,用法和没有返回值的基本一致,我们简单修改一下上边的栗子,代码如下:
static void Main(string[] args)
{
1.new方式实例化一个Task,需要通过Start方法启动
Task<string> task = new Task<string>(() =>
{
return $"hello, task1的ID为{Thread.CurrentThread.ManagedThreadId}";
});
task.Start();
2.Task.Factory.StartNew(Func func)创建和启动一个Task
Task<string> task2 =Task.Factory.StartNew<string>(() =>
{
return $"hello, task2的ID为{ Thread.CurrentThread.ManagedThreadId}";
});
3.Task.Run(Func func)将任务放在线程池队列,返回并启动一个Task
Task<string> task3= Task.Run<string>(() =>
{
return $"hello, task3的ID为{ Thread.CurrentThread.ManagedThreadId}";
});
Console.WriteLine("执行主线程!");
Console.WriteLine(task.Result);//注意task.Result获取结果时会阻塞UI主线程
Console.WriteLine(task2.Result);
Console.WriteLine(task3.Result);
Console.ReadKey();
}
注意task.Resut获取结果时会阻塞UI主线程,即如果task没有执行完成,会等待task执行完成获取到Result,然后再执行后边的代码
2 Task的阻塞方法(Wait/WaitAll/WaitAny)
1 Thread阻塞线程的方法
使用Thread时,我们知道用thread.Join()方法即可阻塞UI主线程。看一个例子:
static void Main(string[] args)
{
Thread th1 = new Thread(() => {
Thread.Sleep(500);
Console.WriteLine("线程1执行完毕!");
});
th1.Start();
Thread th2 = new Thread(() => {
Thread.Sleep(1000);
Console.WriteLine("线程2执行完毕!");
});
th2.Start();
//阻塞UI主线程
th1.Join();
th2.Join();
Console.WriteLine("主线程执行完毕!");
Console.ReadKey();
}
2 Task的Wait/WaitAny/WaitAll方法
Thread的Join方法可以阻塞调用线程,但是有一些弊端:①如果我们要实现很多线程的阻塞时,每个线程都要调用一次Join方法;②如果我们想让所有的线程执行完毕(或者任一线程执行完毕)时,立即解除阻塞,使用Join方法不容易实现。Task提供了 Wait/WaitAny/WaitAll 方法,可以更方便地控制线程阻塞。
task.Wait() 表示等待task执行完毕(会阻塞UI主线程),功能类似于thead.Join(); Task.WaitAll(Task[] tasks) 表示只有所有的task都执行完成了再解除阻塞; Task.WaitAny(Task[] tasks) 表示只要有一个task执行完毕就解除阻塞,看一个栗子:
static void Main(string[] args)
{
Task task1 = new Task(() => {
Thread.Sleep(500);
Console.WriteLine("线程1执行完毕!");
});
task1.Start();
Task task2 = new Task(() => {
Thread.Sleep(1000);
Console.WriteLine("线程2执行完毕!");
});
task2.Start();
//阻塞主线程。task1,task2都执行完毕再执行主线程
//执行【task1.Wait();task2.Wait();】可以实现相同功能
Task.WaitAll(new Task[]{ task1,task2});
Console.WriteLine("主线程执行完毕!");
Console.ReadKey();
}
三、异步方法(async/await)
C# 5 引入了一种简便方法,即异步编程。此方法利用了 .NET Framework 4.5 及更高版本、.NET Core 5.0 和 Windows 运行时中的异步支持.
测试环境:vs2019,.Net Framework4.6.1。
我们看一个栗子:
private void button1_Click(object sender, EventArgs e)
{
Console.WriteLine("主线程ManagedThreadId:" + Thread.CurrentThread.ManagedThreadId);
AsyncTest();
}
static async void AsyncTest()
{
Console.WriteLine("*******Start************ManagedThreadId:" + Thread.CurrentThread.ManagedThreadId);
Task<int> taskA = Print();
Console.WriteLine("*********Middle**********ManagedThreadId:" + Thread.CurrentThread.ManagedThreadId);
int a = await taskA;
Console.WriteLine("return a=" + a);
Console.WriteLine("*********End**********ManagedThreadId:" + Thread.CurrentThread.ManagedThreadId);
}
static Task<int> Print()
{
//Console.WriteLine("Print方法开始执行")
return Task<int>.Run(() =>
{
Thread.Sleep(5000);
return 98;
});
}
参考网址:
https://www.cnblogs.com/wyy1234/p/9172467.html#_label1_3