程序中经常用到的同步和异步方法
同步:当一个方法被调用以后,调用者需等待该方法执行完毕以后方能再次调用
异步:当调用者调用一个方法时,会分配一个线程去处理方法内部业务逻辑,当在有其他调用者调用时,调用者不必等该方法执行是否完毕,就可调用
同步异步调用的应用场景
同步: 在需要执行方法需要立即出结果的,或者被调用者调用等结果返回值才能进行下一步操作的,都需要同步执行
异步:在不需要立即响应结果的,被执行的程序中基本没有关联的场景下。好处是非阻塞,因此可以把一些不需要立即使用结果,较耗时的任务设计为异步执行
可以提升程序运行效率。net4.0在ThreadPool的基础上推出了Task类,微软也极力推荐使用Task来执行异步任务,现在C#类库中的异步方法基本都用到了Task;
net5.0推出了async/await,让异步编程更为方便。
本文主要介绍Task、asyncawait相关内容
Task简介
Task是在ThreadPool的基础上推出的,ThreadPool中有若干数量的线程,如果有任务需要处理时,会从线程池中获取一个空闲的线程来执行任务,任务执行完毕线程不会被销毁,而是被线程池回收供后续任务使用。当线程池中所有的线程都在忙碌时。又有新的任务需要处理时,线程池才会新建一个线程来处理该任务。如果线程数量达到设置的最大值,任务会排队,等待其他任务释放线程后在执行。线程池能减少线程的创建,节省开销,举例说明ThreadPool
static void Main(string[] args) { ThreadPoolTest(); Console.ReadKey(); } public static void ThreadPoolTest() { for (int i = 0; i < 10; i++) { ThreadPool.QueueUserWorkItem(new WaitCallback((obj) => { Console.WriteLine($"第{obj}个执行任务"); }), i); } }
调用以后执行结果
从结果可以看出ThreadPool相对于Thread来说可以减少线程的创建,有效减少系统开销;但是ThreadPool不能控制线程的执行顺序,我们也不能获取线程池
内线程取消/异常/完成的通知,即我们不能有效监控和控制线程池中的线程,基于此我们来研究一下Task
Task创建和运行
net4.0在ThreadPool的基础上推出了Task,Task拥有线程池的优点,同时也解决了使用线程池不易控制的弊端
创建并运行Task,有三种方式方法:
static void Main(string[] args) { CreatTaskMethod(); Console.ReadKey(); }public static void CreatTaskMethod() { //方式一: Task task = new Task(()=> { Thread.Sleep(100); Console.WriteLine($"Task第一种创建方式:通过实例化一个Task,然后通过Start方法启动,当前线程Id为{Thread.CurrentThread.ManagedThreadId}"); }); task.Start(); //方式二: Task task2 = Task.Factory.StartNew(() => { Thread.Sleep(100); Console.WriteLine($"Task第二种创建方式:直接创建并启动Task,当前线程Id为{Thread.CurrentThread.ManagedThreadId}"); }); //方式三: Task task3 = Task.Run(()=> { Thread.Sleep(100); Console.WriteLine($"Task第三种创建方式:将任务放在线程池队列,返回并启动一个Task,当前线程Id为{Thread.CurrentThread.ManagedThreadId}"); }); Console.WriteLine("主线程创建"); }
执行结果如下:
可以看到线程的创建 并不影响主线程的创建,说明Task任务不会阻塞主线程,我们也可以创建带有返回值的Task<TResult>,创建方法和没有返回值的基本一样
代码如下:
static void Main(string[] args) { CreatTaskMethod(); Console.ReadKey(); } /// <summary> /// 创建带有返回值的Task任务 /// </summary> public static void CreatReturnTask() { //方式一: Task<string> task = new Task<string>(() => { return ($"Task第一种创建方式:通过实例化一个Task,然后通过Start方法启动,当前线程Id为{Thread.CurrentThread.ManagedThreadId}"); }); task.Start(); //方式二: Task<string> task2 = Task<string>.Factory.StartNew(() => { return ($"Task第二种创建方式:直接创建并启动Task,当前线程Id为{Thread.CurrentThread.ManagedThreadId}"); }); //方式三: Task<string> task3 = Task<string>.Run(() => { return ($"Task第三种创建方式:将任务放在线程池队列,返回并启动一个Task,当前线程Id为{Thread.CurrentThread.ManagedThreadId}"); }); Console.WriteLine("主线程创建"); Console.WriteLine(task.Result); Console.WriteLine(task2.Result); Console.WriteLine(task3.Result); }
执行结果:
注意: task.Result 获取结果时会阻塞线程,即如果task没有执行完成,会等待task执行完成以后获取结果Result,然后在执行后面的代码
可以把 Console.WriteLine("主线程创建"); 放到现在获取Result结果后面会的到
这里会发现主线程被阻塞了。
Task也可以同步执行,不会阻塞主线程。Task提供了 RunSynchronously()用于同步执行Task任务,代码如下:
public static void SyncTask() { for (int i = 0; i < 10; i++) { Task task = new Task(() => { Thread.Sleep(100); Console.WriteLine($"执行结果Task结束,当前执行顺序为{i}"); }); task.RunSynchronously(); } Console.WriteLine("主线程执行结束"); }
执行结果如下:
这里可以看到 Task执行线程是有顺序的,在使用RunSynchronously以后,会阻塞主线程,达到同步执行。除了使用RunSynchronously方法阻塞线程外,我们还可以使用(wait/waitAll/waitAny)阻塞Task
这里先说明一下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(); }
Task的Wait/WaitAny/WaitAll方法
Thread的join 方法可以阻塞调用线程,但弊端是不利于调用,如果需要阻塞的线程比较多,需要每个线程都要调用一次 Thread.join() 方法;
另外如果我们想要某一线程或者全部线程执行完毕以后,立即解除线程阻塞,使用 join不容易实现,而Task提供了这种机制,下面我们逐一解释一下
Wait() 表示等待Task执行完毕,功能类似于thread,join();
WaitAll(Task[] tasks) 表示只有所有的task都执行完毕以后在接触阻塞;
WaitAny(Task[] tasks) 表示只要有一个task执行完毕就解除阻塞。列:
public static void TaskWaitAll() { Task task = Task.Factory.StartNew(() => { Console.WriteLine("线程1执行完毕"); }); Task task1 = Task.Run(() => { Console.WriteLine("线程2执行完毕"); }); Task.WaitAll(new Task[] { task,task1 }); Console.WriteLine("主线程执行完毕!"); }
执行结果如下:
可以看到线程是异步执行在我们指定了WaitAll线程之后 主线程是出于阻塞状态的,当WaitAll下所有线程执行完毕以后,主线程才解除阻塞,执行,如果使用了
WaitAny 执行得到的结果就是 当线程一或者线程二任意一个执行完毕的时候 主线程就会解除阻塞,继续进行
注意:Wait 、WaitAll、WaitAny方法返回值都是void 这些方法单纯的实现阻塞线程。
如果想要让所有Task执行完毕,或则让任意Task执行完毕后,开始解除阻塞执行后续操作,该如何实现呢?这时候需要用到WhenAny、WhenAll方法了这些方法执行完毕以后会返回一个task实力。
Task中的延续操作(WhenAny/WhenAll/ContinueWith)
task.WhenAll(Task[] tasks) 表示所有的task都执行完毕以后再去执行后续的操作。
task.WhenAny(Task[] tasks)表示任意一个task执行完毕以后就开始后续操作。列:
static void Main(string[] args) { TaskWhenAll(); Console.ReadKey(); } /// <summary> /// WhenAll WhenAny /// </summary> public static void TaskWhenAll() { Task task1 = Task.Run(() => { Console.WriteLine("线程1执行完毕"); }); Task task2 = Task.Factory.StartNew(() => { Console.WriteLine("线程2执行完毕"); }); //Task.WhenAll(task1, task2).ContinueWith((t) => { // Console.WriteLine("执行后续操作"); //}); Task.WhenAny(task1, task2).ContinueWith((t) => { Console.WriteLine("任意线程执行完毕后执行"); }); Console.WriteLine("主线程执行完毕"); }
执行结果如下:
可以看到使用WhenAll、WhenAny时方法不会阻塞线程
上边的例子也可以通过Task.Factory.ContinueWhenAll(Task[] tasks,Action continuationAction) 和 Task.Factory.ContinueWhenAny(Task[] task, Action continuationAction)实现 例:
public static void TaskWhenAll() { Task task1 = Task.Run(() => { Console.WriteLine("线程1执行完毕"); }); Task task2 = Task.Factory.StartNew(() => { Console.WriteLine("线程2执行完毕"); }); //Task.WhenAll(task1, task2).ContinueWith((t) => { // Console.WriteLine("执行后续操作"); //}); //Task.WhenAny(task1, task2).ContinueWith((t) => { // Console.WriteLine("任意线程执行完毕后执行"); //}); Task task3 = Task.Factory.ContinueWhenAll(new Task[] { task1,task2}, (t) => { Console.WriteLine("线程全部执行完毕以后,执行"); }); Task task4 = Task.Factory.ContinueWhenAny(new Task[] { task1,task2 },(t)=> { Console.WriteLine("任意线程执行完毕以后,执行"); }); Console.WriteLine("主线程执行完毕"); }
这两种写法不同 但是功效是相同的
Task的任务取消
先看一下 Thread是如何取消任务执行的
1 设置变量来控制任务是否停止,如var isStop=false; 如果值为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(); }
Task是如何取消任务执行的呢?Task中有一个专门的类CancellationTokenSource 来取消任务执行,还是上面的例子,修改后如下:
/// <summary> /// 取消任务 /// </summary> public static void CancelTask() { CancellationTokenSource source = new CancellationTokenSource(); int index = 0; Task task = Task.Factory.StartNew(() => { while (!source.IsCancellationRequested) { Console.WriteLine($"第{++index}次执行,线程运行中......"); } }); //0.1秒后取消任务执行 //取消方法1: Thread.Sleep(100); source.Cancel();//这个时候IsChancellationRequested会变成true 还可以用 //取消方法2: //source.CancelAfter(100); //这个是0.1秒后自动取消的,和上面方法是等价的 }
了解了线程创建,执行,销毁,那么跨线程操作也是很有必要了解一下的
跨线程执行
public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { Task.Run(() => { Action<int> setValue = (p) => { textBox1.Text = p.ToString(); }; for (int i = 0; i < 1000000; i++) { textBox1.Invoke(setValue,i); } }); } }
异步方法(async/await)
在C#5.0中出现的,让异步编程变得更简单。我们看一个例子:
static void Main(string[] args) { MethodOne(); MethodTwo(); Console.ReadKey(); } public static async Task MethodOne() { await Task.Run(() => { for (int i = 0; i < 50; i++) {
Thread.Sleep(100); Console.WriteLine("111"); } }); } public static void MethodTwo() { for (int i = 0; i <30; i++) {
Thread.Sleep(100); Console.WriteLine("222"); } }
执行结果
可以看到虽然先调用了方法一 但是方法二 由于是异步 也没影响方法1的执行