一、什么是异步

  同步和异步主要用于修饰方法。当一个方法被调用时,调用者需要等待该方法执行完毕并返回才能继续执行,我们称这个方法是同步方法;当一个方法被调用时立即返回,并获取一个线程执行该方法内部的业务,调用者不用等待该方法执行完毕,我们称这个方法为异步方法。

  异步的好处在于非阻塞(调用线程不会暂停执行去等待子线程完成),因此我们把一些不需要立即使用结果、较耗时的任务设为异步执行,可以提高程序的运行效率。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的创建和执行方式有如下三种:

        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();
}

执行结果如下:

  我们看到先打印"执行主线程",然后再打印各个任务,说明了Task不会阻塞主线程。上边的栗子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);
Console.WriteLine(task2.Result);
Console.WriteLine(task3.Result);
Console.ReadKey();
}

  注意task.Resut获取结果时会阻塞线程,即如果task没有执行完成,会等待task执行完成获取到Result,然后再执行后边的代码,程序运行结果如下:

  上边的所有栗子中Task的执行都是异步的,不会阻塞主线程。有些场景下我们想让Task同步执行怎么办呢?Task提供了  task.RunSynchronously()用于同步执行Task任务,代码如下:

        static void Main(string[] args)
{
Task task = new Task(() =>
{
Thread.Sleep(100);
Console.WriteLine("执行Task结束!");
});
//同步执行,task会阻塞主线程
task.RunSynchronously();
Console.WriteLine("执行主线程结束!");
Console.ReadKey();
}

执行结果如下:

2 Task的阻塞方法(Wait/WaitAll/WaitAny)

1 Thread阻塞线程的方法

  使用Thread时,我们知道用thread.Join()方法即可阻塞主线程。看一个例子:

        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();
//阻塞主线程
th1.Join();
th2.Join();
Console.WriteLine("主线程执行完毕!");
Console.ReadKey();
}

  如果注释掉两个Join,执行结果是:先打印【主线程执行完毕】,而添加两个Join方法后执行结果如下,实现了线程阻塞:

2 Task的Wait/WaitAny/WaitAll方法

  Thread的Join方法可以阻塞调用线程,但是有一些弊端:①如果我们要实现很多线程的阻塞时,每个线程都要调用一次Join方法;②如果我们想让所有的线程执行完毕(或者任一线程执行完毕)时,立即解除阻塞,使用Join方法不容易实现。Task提供了  Wait/WaitAny/WaitAll  方法,可以更方便地控制线程阻塞。

task.Wait()  表示等待task执行完毕,功能类似于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();
}

  执行结果如下:

  如果将栗子中的WaitAll换成WaitAny,那么任一task执行完毕就会解除线程阻塞,执行结果是:先打印【线程1执行完毕】,然后打印【主线程执行完毕】,最后打印【线程2执行完毕】

3 Task的延续操作(WhenAny/WhenAll/ContinueWith)

  上边的Wait/WaitAny/WaitAll方法返回值为void,这些方法单纯的实现阻塞线程。我们现在想让所有task执行完毕(或者任一task执行完毕)后,开始执行后续操作,怎么实现呢?这时就可以用到WhenAny/WhenAll方法了,这些方法执行完成返回一个task实例。  task.WhenAll(Task[] tasks)  表示所有的task都执行完毕后再去执行后续的操作, task.WhenAny(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执行完了后执行后续操作
Task.WhenAll(task1, task2).ContinueWith((t) => {
Thread.Sleep(100);
Console.WriteLine("执行后续操作完毕!");
}); Console.WriteLine("主线程执行完毕!");
Console.ReadKey();
}

  执行结果如下,我们看到WhenAll/WhenAny方法不会阻塞主线程,当使用WhenAll方法时所有的task都执行完毕才会执行后续操作;如果把栗子中的WhenAll替换成WhenAny,则只要有一个线程执行完毕就会开始执行后续操作,这里不再演示。

  上边的栗子也可以通过 Task.Factory.ContinueWhenAll(Task[] tasks, Action continuationAction)  和 Task.Factory.ContinueWhenAny(Task[] tasks, Action continuationAction) 来实现 ,修改上边栗子代码如下,执行结果不变。

       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();
//通过TaskFactroy实现
Task.Factory.ContinueWhenAll(new Task[] { task1, task2 }, (t) =>
{
Thread.Sleep(100);
Console.WriteLine("执行后续操作");
}); Console.WriteLine("主线程执行完毕!");
Console.ReadKey();
}

4 Task的任务取消(CancellationTokenSource)

1 Thread取消任务执行

  在Task前我们执行任务采用的是Thread,Thread怎么取消任务呢?一般流程是:设置一个变量来控制任务是否停止,如设置一个变量isStop,然后线程轮询查看isStop,如果isStop为true就停止,代码如下:

        static void Main(string[] args)
{
bool isStop = false;
int index = 0;
//开启一个线程执行任务
Thread th1 = new Thread(() =>
{
while (!isStop)
{
Thread.Sleep(1000);
Console.WriteLine($"第{++index}次执行,线程运行中...");
}
});
th1.Start();
//五秒后取消任务执行
Thread.Sleep(5000);
isStop = true;
Console.ReadKey();
}

2 Task取消任务执行

  Task中有一个专门的类 CancellationTokenSource  来取消任务执行,还是使用上边的例子,我们修改代码如下,程序运行的效果不变。

        static void Main(string[] args)
{
CancellationTokenSource source = new CancellationTokenSource();
int index = 0;
//开启一个task执行任务
Task task1 = new Task(() =>
{
while (!source.IsCancellationRequested)
{
Thread.Sleep(1000);
Console.WriteLine($"第{++index}次执行,线程运行中...");
}
});
task1.Start();
//五秒后取消任务执行
Thread.Sleep(5000);
//source.Cancel()方法请求取消任务,IsCancellationRequested会变成true
source.Cancel();
Console.ReadKey();
}

   CancellationTokenSource的功能不仅仅是取消任务执行,我们可以使用  source.CancelAfter(5000)  实现5秒后自动取消任务,也可以通过 source.Token.Register(Action action)  注册取消任务触发的回调函数,即任务被取消时注册的action会被执行。 看一个栗子:

        static void Main(string[] args)
{
CancellationTokenSource source = new CancellationTokenSource();
//注册任务取消的事件
source.Token.Register(() =>
{
Console.WriteLine("任务被取消后执行xx操作!");
}); int index = 0;
//开启一个task执行任务
Task task1 = new Task(() =>
{
while (!source.IsCancellationRequested)
{
Thread.Sleep(1000);
Console.WriteLine($"第{++index}次执行,线程运行中...");
}
});
task1.Start();
//延时取消,效果等同于Thread.Sleep(5000);source.Cancel();
source.CancelAfter(5000);
Console.ReadKey();
}

  执行结果如下,第5次执行在取消回调后打印,这是因为,执行取消的时候第5次任务已经通过了while()判断,任务已经执行中了:

   最后看上一篇跨线程的栗子,点击按钮启动一个任务,给tetxtbox赋值,我们把Thread改成Task,代码如下:

    public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
} private void mySetValueBtn_Click(object sender, EventArgs e)
{
Task.Run(() =>
{
Action<int> setValue = (i) => { myTxtbox.Text = i.ToString(); };
for (int i = 0; i < 1000000; i++)
{
myTxtbox.Invoke(setValue,i);
}
});
}
}

  运行界面如下,赋值的task不会阻塞UI线程:

三、异步方法(async/await)

  在C#5.0中出现的 async和await ,让异步编程变得更简单。我们看一个获取文件内容的栗子:

    class Program
{
static void Main(string[] args)
{
string content = GetContentAsync(Environment.CurrentDirectory + @"/test.txt").Result;
//调用同步方法
//string content = GetContent(Environment.CurrentDirectory + @"/test.txt");
Console.WriteLine(content);
Console.ReadKey();
}
//异步读取文件内容
async static Task<string> GetContentAsync(string filename)
{ FileStream fs = new FileStream(filename, FileMode.Open);
var bytes = new byte[fs.Length];
//ReadAync方法异步读取内容,不阻塞线程
Console.WriteLine("开始读取文件");
int len = await fs.ReadAsync(bytes, 0, bytes.Length);
string result = Encoding.UTF8.GetString(bytes);
return result;
}
//同步读取文件内容
static string GetContent(string filename)
{
FileStream fs = new FileStream(filename, FileMode.Open);
var bytes = new byte[fs.Length];
//Read方法同步读取内容,阻塞线程
int len = fs.Read(bytes, 0, bytes.Length);
string result = Encoding.UTF8.GetString(bytes);
return result;
}
}

  test.txt内容是【hello world!】执行结果为:

  上边的栗子也写出了同步读取的方式,将main函数中的注释去掉即可同步读取文件内容。我们可以看到异步读取代码和同步读取代码基本一致。async/await让异步编码变得更简单,我们可以像写同步代码一样去写异步代码。注意一个小问题:异步方法中方法签名返回值为Task<TResult>,代码中的返回值为TResult。上边栗子中GetContentAsync的签名返回值为Task<string>,而代码中返回值为string。牢记这一细节对我们分析异步代码很有帮助。通过上边的栗子我们知道aysnc/await的用法十分简单,那么aynsc标记的异步方法是怎样执行的呢?什么时候创建的任务线程?再看一个简单的栗子:

    class Program
{
static void Main(string[] args)
{
Console.WriteLine(">>>>>>>>>>>>>>>>主线程启动");
Task<string> task = GetStringAsync1();
Console.WriteLine("<<<<<<<<<<<<<<<<主线程结束");
Console.WriteLine($"GetStringAsync1执行结果:{task.Result}");
} static async Task<string> GetStringAsync1()
{
Console.WriteLine(">>>>>>>>GetStringAsync1方法启动");
string str = await GetStringAsync2();
Console.WriteLine("<<<<<<<<GetStringAsync1方法结束");
return str;
}
static async Task<string> GetStringAsync2()
{
Console.WriteLine(">>>>>>>>GetStringAsync2方法启动");
string str = await GetStringFromTask();
Console.WriteLine("<<<<<<<<GetStringAsync2方法结束");
return str;
} static Task<string> GetStringFromTask()
{
Console.WriteLine(">>>>GetStringFromTask方法启动");
Task<string> task = new Task<string>(() =>
{
Console.WriteLine(">>任务线程启动");
Thread.Sleep(1000);
Console.WriteLine("<<任务线程结束");
return "hello world";
});
task.Start();
Console.WriteLine("<<<<GetStringFromTask方法结束");
return task;
}
}

  使用async/await时,主线程和执行同步代码一样,是一路执行到底的,执行到await时也没有返回main函数,而是创建了一个空闲的任务后继续往下执行,当所有的调用的方法都执行完毕后再返回main函数。主线程执行到task.Start()才创建任务线程,真正开始执行异步任务。当异步方法async  Task<T>A()需要另一个异步方法async Task<T>B()的结果时,A()会等待B()完成异步任务再继续向下执行。程序执行结果如下:

  小结:到这里Task,async/await的简单使用已经基本结束了,一些高级特性等到工作遇到了再去研究。通过上边的介绍,我们知道async/await是基于Task的,而Task是对ThreadPool的封装改进,主要是为了更有效的控制线程池中的线程(ThreadPool中的线程,我们很难通过代码控制其执行顺序,任务延续和取消等等);ThreadPool基于Thread的,主要目的是减少Thread创建数量和管理Thread的成本。async/await Task是C#中更先进的,也是微软大力推广的特性,我们在开发中可以尝试使用Task来替代Thread/ThreadPool,处理本地IO和网络IO任务是尽量使用async/await来提高任务执行效率。

转载自:https://www.cnblogs.com/wyy1234/

转载请注明原始地址,感谢!

C#多线程和异步(二)——Task和async/await详解(转载)的更多相关文章

  1. C#多线程和异步(二)——Task和async/await详解

    一.什么是异步 同步和异步主要用于修饰方法.当一个方法被调用时,调用者需要等待该方法执行完毕并返回才能继续执行,我们称这个方法是同步方法:当一个方法被调用时立即返回,并获取一个线程执行该方法内部的业务 ...

  2. C#多线程和异步——Task和async/await详解

    阅读目录 一.什么是异步 二.Task介绍 1 Task创建和运行 2 Task的阻塞方法(Wait/WaitAll/WaitAny) 3 Task的延续操作(WhenAny/WhenAll/Cont ...

  3. Task和async/await详解

    一.什么是异步 同步和异步主要用于修饰方法.当一个方法被调用时,调用者需要等待该方法执行完毕并返回才能继续执行,我们称这个方法是同步方法:当一个方法被调用时立即返回,并获取一个线程执行该方法内部的业务 ...

  4. Promise和async await详解

    本文转载自Promise和async await详解 Promise 状态 pending: 初始状态, 非 fulfilled 或 rejected. fulfilled: 成功的操作. rejec ...

  5. async await详解

    async await本身就是promise + generator的语法糖. 本文主要讲述以下内容 async awiat 实质 async await 主要特性 async await 实质 下面 ...

  6. JavaScript中的async/await详解

    1.前言 ​ async函数,也就是我们常说的async/await,是在ES2017(ES8)引入的新特性,主要目的是为了简化使用基于Promise的API时所需的语法.async和await关键字 ...

  7. C#中 Thread,Task,Async/Await 异步编程

    什么是异步 同步和异步主要用于修饰方法.当一个方法被调用时,调用者需要等待该方法执行完毕并返回才能继续执行,我们称这个方法是同步方法:当一个方法被调用时立即返回,并获取一个线程执行该方法内部的业务,调 ...

  8. C#中 Thread,Task,Async/Await,IAsyncResult 的那些事儿!

    说起异步,Thread,Task,async/await,IAsyncResult 这些东西肯定是绕不开的,今天就来依次聊聊他们 1.线程(Thread) 多线程的意义在于一个应用程序中,有多个执行部 ...

  9. C#中 Thread,Task,Async/Await,IAsyncResult 的那些事儿![转载]

    说起异步,Thread,Task,async/await,IAsyncResult 这些东西肯定是绕不开的,今天就来依次聊聊他们 1.线程(Thread) 多线程的意义在于一个应用程序中,有多个执行部 ...

随机推荐

  1. Optimizing and caching browser bundles...

    扎心了,老铁!!! 他妈的,弄了几天都是卡在这里!!上网查了一下.说是我的内存不够,好吧.我慢慢等,然而让我等了好几天都没什么反应..两天了,一直卡在这里.今天在虚拟机也是发狠了. 我直接把虚拟机的内 ...

  2. git 各个区的区别

    Git有三大区(工作区.暂存区.版本库)以及几个状态(untracked.unstaged.uncommited) 把文件往Git版本库里添加的时候,是分两步执行的: 第一步是用 git add 把文 ...

  3. ECharts 雷达图怎么在类目值下面显示数值

    需要实现的效果: 官网里面的demo显示数值,都是在拐点处: [解决] 1.只显示类目 <div id="mychart" style="width:300px;h ...

  4. 认识HTML中文本、图片、链接标签和路径

    前端之HTML.CSS(一) 开发工具 编辑器 Dreamware.Sublime.Visual Studio Code.WebStorm 浏览器 Chrome.IE(Edge).Safari.Fir ...

  5. C++ GUI Qt4编程(03)-1.3layout

    1. 根据C++ GUI Qt4编程(第二版)整理2. 系统:centos7:  Qt版本:5.5.13. 程序:layout.cpp #include <QApplication> #i ...

  6. C++ GUI Qt4编程(01)-1.1Hello Qt

    1. 根据C++ GUI Qt4编程(第二版)整理2. 系统:centos7:  Qt版本:5.5.13. 程序:hello.cpp #include <QApplication> #in ...

  7. 2.rabbitmq 系列教程

    rabbitmq系列教程-文章[转] 视频分享: 链接:https://pan.baidu.com/s/1s_Qr2A1o0s8Ru0exK62jqg 提取码:eb68

  8. Oracle RAC集群搭建(三)--挂载磁盘

    一,磁盘配置 查看由上回配置的共享磁盘,一共三块-----以下所有内容均两台物理机都需要操作 查看磁盘id [root@rac2 ~]# /usr/lib/udev/scsi_id -g -u /de ...

  9. URL篇之相对URL

    URL有两种方式:绝对的和相对的. 绝对URL中包含有访问资源所需的全部信息,是访问网络资源必须的. 相对URL是不完整的,要从相对URL中获取访问资源所需的全部信息,就必须相对于另一个被称为其基础( ...

  10. 华为 p8上配置outllook,同步日历/邮件

    在网上找了半天,也没有找到合适的方法,在outlook官网上终于找到了配置方式 一种方式是使用outlook的手机应用.(此处省略) 另外一种方式是使用手机自带的邮件/日历应用(见下面操作方法) 如果 ...