阅读目录


一、什么是异步

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

  异步的好处在于非阻塞(调用线程不会暂停执行去等待子线程完成),因此我们把一些不需要立即使用结果、较耗时的任务设为异步执行,可以提高程序的运行效率。net4.0在ThreadPool的基础上推出了Task类,微软极力推荐使用Task来执行异步任务,现在C#类库中的异步方法基本都用到了Task;net5.0推出了async/await,让异步编程更为方便。本篇主要介绍Task、async/await相关的内容,其他异步操作的方式会在下一篇介绍。

二、Task介绍

2、Task的用法

  2.1、创建任务
  无返回值的方式
  方式1:
  var t1 = new Task(() => TaskMethod("Task 1"));
  t1.Start();
  Task.WaitAll(t1);//等待所有任务结束 
  注:
  任务的状态:
  Start之前为:Created
  Start之后为:WaitingToRun

  方式2:
  Task.Run(() => TaskMethod("Task 2"));

  方式3:
  Task.Factory.StartNew(() => TaskMethod("Task 3")); 直接异步的方法 
  或者
  var t3=Task.Factory.StartNew(() => TaskMethod("Task 3"));
  Task.WaitAll(t3);//等待所有任务结束
  注:
  任务的状态:
  Start之前为:Running
  Start之后为:Running

  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来提高任务执行效率。

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

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

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

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

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

  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. ISCC之msc_无法运行的exe

    打开hxd,里面老长一串base64 解码试了一下,解出来是png文件头,但是图片有错误 百度了一下,PNG文件头是89 50 4E 47 0D 0A 1A 0A 再回去看 改成0A了事, 出来一张二 ...

  2. WXS介绍

    WXS(WeiXin Script)是小程序的一套脚本语言,结合 WXML,可以构建出页面的结构. 1.WXS与JS的区别 1.1 WXS 的基础类库: WXS 只提供给开发者5个基础类库,分别是 c ...

  3. screen 调到后台使用

    yum -y install screen   screen -S  name     开启后台进程 screen -ls      显示进程 screen -r  ID号    进入 Ctrl+r再 ...

  4. Flume源码更改

    1.源码更改场景:如果使用 0.8 版本 Kafka 并配套 1.6 版本 Flume,由于 Flume 1.6 版本没有Taildir Source 组件,因此,需要将 Flume 1.7 中的 T ...

  5. 2019 icpc 徐州 解题报告

    A.Cat 题库链接 给定区间[l,r],求一个最长子区间,使得区间异或和小于等于s,(结论)偶数和偶数后三个数的异或和等于0 #include <bits/stdc++.h> using ...

  6. 洛谷 AT2434 JOI 公園 (JOI Park) 题解

    人生第一次AC黑题,我太感动了. 每日一题 day31 打卡 Analysis 先跑遍DJ,求出1到 i的最短路.得到每个点到 1号点的距离后,从小到大排序一遍,这时便可以枚举每个点到 1号点的距离修 ...

  7. .Net Core WebApi实现跨域

    .Net Core 需要引用一个包  Microsoft.AspNetCore.Cors 让接口实现跨域,需要配置两个地方. 一.Startup.cs 这里需要配置两个地方 public void C ...

  8. SVN “Previous operation has not finished”

    https://jingyan.baidu.com/article/cbcede0761334902f40b4d31.html 需要运行sqlite3打开.svn下的wc.db数据库文件, sqlit ...

  9. luogu P1058 立体图

    做了这个题后明确了自己的定位... 恩...普及- 题目大意估计都知道.. 给个传送门:   luogu 做了半上午 + 一整个下午的题... 占了我今天到的绝大多数时间. 其实此题不难, 核心代码我 ...

  10. 2D到3D视频转换 三维重建

    2D到3D视频转换(也称为2D到立体3D转换和立体转换)是将2D(“平面”)胶片转换为3D形式的过程,几乎在所有情况下都是立体声,因此它是创建图像的过程.每个眼睛来自一个2D图像. 内容 1概述 1. ...