基于Task的异步编程模式(TAP)

async方法在开始时以同步方式执行,在async方法内部,await关键字对它参数执行一个异步等待,它首先检查操作是否已经完成,如果完成,就继续运行(同步方式),否则,会暂停async方法,并返回.留下一个未完成的task,一段时间后,操作完成,async方法就恢复执行.看到这句话应该就差不多能想到await为什么不会导致线程堵塞了,当碰到await时如果没有执行成功就先暂停这个方法的执行,执行方法外以下代码,等await操作完成后再执行这个方法await之后的代码;直白点的意思就是async/await就是一个让编译器把现有的代码直接生成一个带回调逻辑代码的语法糖。
Task 类表示通常以异步方式执行的单个操作, Task 对象是基于任务的异步模式的中心组件之一。 由于 Task 对象执行的工作通常在线程池线程上异步执行,而不是在主应用程序线程上同步执行,因此可以使用 Status 属性,还可以使用 IsCanceled、IsCompleted和 IsFaulted 属性,用于确定任务的状态。 通常,lambda 表达式用于指定任务要执行的工作。

Task.Run 和 Task.Factory.StartNew

Task.Run方法只是提供了一种使用Task.Factory.StartNew方法的更简洁的形式。

Task.Run(someAction);
//实际上等价于:
Task.Factory.StartNew(someAction, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);

CancellationToken

Task.Run的CancellationToken参数不是用于中断程序运行的,而是在开始执行前用于判断是否需要执行该任务。A cancellation token that can be used to cancel the work if it has not yet started. 任务内需要通过判断是否收到取消请求,决定是否调用ThrowIfCancellationRequested()方法抛出异常取消任务

Run(Action, CancellationToken)

CancellationTokenSource & CancellationToken

  • CancellationTokenSource:取消令牌源类,拥有Cancel()方法,可以给关联的令牌发送取消信号。
  • CancellationToken:取消令牌,结构体,拥有ThrowIfCancellationRequested()方法可以抛出异常,让任务状态变成Canceled。

异常

Task中的方法一般是不会将异常抛出。

async/await结构,异步方法名称以Async结尾

async主要作用是标记为异步,但不会创建异步。

  1. 调用方法:该方法调用异步方法,然后在异步方法执行其任务的时候继续执行;
  2. 异步方法:该方法异步执行工作,然后立刻返回到调用方法;
  3. await 表达式:用于异步方法内部,指出需要异步执行的任务。一个异步方法可以包含多个 await 表达式(不存在 await 表达式的话 IDE 会发出警告)。

异步方法返回类型

异步方法的返回类型只能是void、Task、Task,尽量不使用void作为返回类型,若希望异步方法返回void类型,请使用Task,异步方法名称以Async结尾。可以使用`Task.Result`获取返回值,也可以直接使用`var = await DemoAsync();`获取返回值。

异步方法

使用 async 关键字可将方法、lambda 表达式或匿名方法标记为异步。用async来修饰一个方法,表明这个方法是异步的,方法内部必须含有await修饰的方法,如果方法内部没有await关键字修饰的表达式,哪怕函数被async修饰也只能算作同步方法,执行的时候也是同步执行的。

await修饰的方法

被await修饰的只能是Task或者Task类型,通常情况下是一个返回类型是Task/Task的方法

暂停方法

  1. async方法中,当执行到await时,如果没有执行成功就先暂停这个方法的执行
  2. 外部调用方法如果需要获取Task.Result返回值时,如果异步方法没有执行完,则等待该异步方法执行完再继续执行程序。

Task创建

//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}");
});

async void应仅用于事件处理程序

async void 是允许异步事件处理程序工作的唯一方法,因为事件不具有返回类型(因此无法利用 Task 和 Task)。 其他任何对 async void 的使用都不遵循 TAP 模型,且可能存在一定使用难度,例如:

  • async void 方法中引发的异常无法在该方法外部被捕获。
  • async void 方法很难测试。
  • async void 方法可能会导致不良副作用(如果调用方不希望方法是异步的话)。

异步方法可以具有以下返回类型:

  1. Task(对于执行操作但不返回任何值的异步方法)。
  2. Task(对于返回值的异步方法)。
  3. void(对于事件处理程序)。
  4. 从 C# 7.0 开始,任何具有可访问的 GetAwaiter 方法的类型。 GetAwaiter 方法返回的对象必须实现 System.Runtime.CompilerServices.ICriticalNotifyCompletion 接口。
  5. 从 C# 8.0 开始,IAsyncEnumerable 返回异步流的异步方法 。

await

await的作用只是用于通知上层调用函数,函数在异步执行,你可以继续执行上层函数的程序。如下函数所示,当程序执行到Task<string> ResultFromTimeConsumingMethod = TimeConsumingMethod();这一行时,TimeConsumingMethod函数已经开始执行了,但是此时AsyncMethod不会返回给上层函数,上层函数需要继续等待,只有当AsyncMethod函数执行到string Result = await ResultFromTimeConsumingMethod + " + AsyncMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId;这一行时,AsyncMethod才会返回。即异步函数只有遇到await时,该异步函数才会返回通知上层函数,让上层函数开始继续执行下面的代码。

static private async Task AsyncMethod()
{
    Console.WriteLine("1");
    Thread.Sleep(500);
    Console.WriteLine("2");
    Task<string> ResultFromTimeConsumingMethod = TimeConsumingMethod();
    string Result = await ResultFromTimeConsumingMethod + " + AsyncMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId;
    Console.WriteLine(Result);
    //返回值是Task的函数可以不用return
}

Task.Result

如果程序代码中需要使用Task.Result的值,那么该函数会挂起,直到该Task所指向的函数执行完,程序才会继续运行下去。

async void

如果异步函数为async void返回值,那么在调用时不需要使用await,虽然没有await,但是异步效果是一样的(不推荐使用,因为这个是用于异步事件的),下面程序当main函数调用AsyncMethod2时,当AsyncMethod2运行到await TimeConsumingMethod();时,因为有await,所以AsyncMethod2会直接返回,而不用等待TimeConsumingMethod运行完,但main函数接收到AsyncMethod2返回信号时,main函数会直接运行下面的代码,不会等到AsyncMethod2全部执行完才运行。

static void Main(string[] args)
{
    Console.WriteLine("111 balabala. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
    //Task t = AsyncMethod();
    AsyncMethod2();
    Console.WriteLine("222 balabala. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
    Console.ReadKey();
}

static private async void AsyncMethod2()
{
    Console.WriteLine("1");
    await TimeConsumingMethod();    
    Console.WriteLine("2");  
}

异步转同步

仔细体会下列代码,如果异步代码中没有await,则程序相当于同步代码。只会等所有代码执行完才返回。但是如果异步中又调用异步,虽然第一层异步代码中因为没有await指令而编程同步代码,但因为第二层异步代码中有await指令,所以第一层代码会全部执行完,但是第一层代码调用第二层异步代码时,遇到await指令,第二层代码会直接返回。这样的逻辑结果就是第一层代码在执行完所有代码返回后,可能第二层代码还没有执行完。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleAppTest.Async
{
    class TaskTest
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Main Start");
            Task task = TaskTest.TestWithOutawaitAsync();
            Console.WriteLine("Main End");
            Console.ReadKey();
            return;

        }

        public static async Task TestAsync()
        {
            //Task task1= Test1Async();
            //Task task2= Test2Async();
            //Task[] tasks = { task1, task2 };
            //Task.WaitAll(tasks);

            Console.WriteLine("TestAsync start");
            await Test1Async();
            Console.WriteLine("TestAsync middle");
            await Test2Async();
            Console.WriteLine("TestAsync end");

            //Output:
            //Main Start
            //TestAsync start
            //Test1Async.Start
            //Main End
            //Test1Async.End
            //TestAsync middle
            //Test2Async.Start
            //Test2Async.End
            //TestAsync end

        }

        public static async Task TestWithOutawaitAsync()
        {
            Console.WriteLine("TestAsync start");
            Test1Async();
            Console.WriteLine("TestAsync middle");
            Test2Async();
            Console.WriteLine("TestAsync end");

            //Output:
            //Main Start
            //TestAsync start
            //Test1Async.Start
            //TestAsync middle
            //Test2Async.Start
            //TestAsync end
            //Main End
            //Test1Async.End
            //Test2Async.End
        }

        public static async Task Test1Async()
        {
            Console.WriteLine("Test1Async.Start");
            await Task.Delay(2000);
            Console.WriteLine("Test1Async.End");
        }

        public static async Task Test2Async()
        {
            Console.WriteLine("Test2Async.Start");
            await Task.Delay(3000);
            Console.WriteLine("Test2Async.End");
        }

    }
}

使用await和不使用await区别(函数内部同步还是异步)

下面这三个代码,代码二和代码三等效。代码三是代码二的简写方式。代码一执行时,遇到第一个Task task = test01();在遇到test01内部的await时会直接返回,并且不会等待test01是否真的执行完,所以程序会一下子打印100行Hello World,代码二或代码三,因为Task在执行时使用了await,虽然程序会立即返回,导致Main函数会提前打印Main is end,但是Hello World并不会一下子打印出来,会1秒打印一行。通过这个测试,我们可以发现,异步函数内部,对于Task,如果使用了await,其实函数内部的代码时同步执行的,即必须等待await那一行真正的执行完才会执行下一行语句;而如果不适用await,函数内部只要接收到了内部task的return指令,就会接着向下执行,并不会等待task是否真正的执行完。

static async Task Main(string[] args)
{
	Console.WriteLine("Main is start");
	Task task = test02();
	Console.WriteLine("Main is end");
}

async static  public Task test01()
{
    await Task.Delay(1000);
}


//代码一
async static public Task test02()
{
    for (int i = 0; i < 100; i++)
    {
        Console.WriteLine("Hello world " + i);
        Task task = test01();
        //await task;
        //await test01();                
    }
}

//代码二
async static public Task test02()
{
    for (int i = 0; i < 100; i++)
    {
        Console.WriteLine("Hello world " + i);
        Task task = test01();
        await task;
        //await test01();                
    }
}

//代码三
async static public Task test02()
{
    for (int i = 0; i < 100; i++)
    {
        Console.WriteLine("Hello world " + i);
        //Task task = test01();
        //await task;
        await test01();                
    }
}

同步事件调用异步Task

会造成主界面死住,卡死的问题。

创建和启动Task的方法

一共由三个方法可以创建和启动task,这些方法都是异步的,如下代码所示,因为每个方法等待的时间不一样,所以会先执行第三个,再执行第二个,最后才执行第一个。但是如果直接获取Task.Result返回值时会阻塞线程。

//无返回值
class Program
{
    static void Main(string[] args)
    {
        // 实例化一个Task,通过Start方法启动
        Task task = new Task(
            () =>
            {
                Thread.Sleep(1000);
                Console.WriteLine($"NEW实例化一个task,线程ID为{Thread.CurrentThread.ManagedThreadId}");
            }
            );
 
        task.Start();
 
        // Task.Factory.StartNew(Action action)创建和启动一个Task
        Task task2 = Task.Factory.StartNew(
            () =>
            {
                Thread.Sleep(500);
                Console.WriteLine($"Task.Factory.StartNew方式创建一个task,线程ID为{Thread.CurrentThread.ManagedThreadId}");
            });
        
        // Task.Run(Action action)将任务放在线程池队列,返回并启动一个Task
        Task task3 = Task.Run(
            () =>
            {
                Thread.Sleep(200);
                Console.WriteLine($"Task.Run方式创建一个task,线程ID为{Thread.CurrentThread.ManagedThreadId}");
            });
 
        Console.WriteLine("执行主线程");
        Console.Read();
    }
}

//有返回值,Task.Result获取返回值时会阻塞线程。
class Program
{
    static void Main(string[] args)
    {
 
        // 有返回值的启动task
        Task<string> task = new Task<string>(
            () =>
            {
                Thread.Sleep(1000);
                return $"NEW实例化一个task,线程ID为{Thread.CurrentThread.ManagedThreadId}";
            }
            );
 
        task.Start();
 
        // Task.Factory.StartNew(Action action)创建和启动一个Task
 
        Task<string> task2 = Task.Factory.StartNew(
            () =>
            {
                Thread.Sleep(3000);
                return $"Task.Factory.StartNew方式创建一个task,线程ID为{Thread.CurrentThread.ManagedThreadId}";
            });
 
        // Task.Run(Action action)将任务放在线程池队列,返回并启动一个Task
 
        Task<string> task3 = Task.Run(
            () =>
            {
                Thread.Sleep(2000);
                return $"Task.Run方式创建一个task,线程ID为{Thread.CurrentThread.ManagedThreadId}";
            });
 
        Console.WriteLine("执行主线程");
        Console.WriteLine(task.Result);
        Console.WriteLine(task2.Result);
        Console.WriteLine(task3.Result);
        Console.Read();
    }
}

TaskCompletionSource

TaskCompletionSource可以作为一个变量传给子函数,然后在子函数中设置它的值,子函数可以返回TaskCompletionSource.Task。通过在调用函数中获取TaskCompletionSource.Task.Result这个值,实现异步的调用。

TaskCompletionSource<int> tcs1 = new TaskCompletionSource<int>();
Task<int> t1 = tcs1.Task;

// Start a background task that will complete tcs1.Task
Task.Factory.StartNew(() =>
{
    Thread.Sleep(1000);
    tcs1.SetResult(15);
});

int result = t1.Result;

Task与线程

当调用异步函数时,并不意味着,该异步函数就是一个子线程,因为有可能是该异步函数又调用了另一个异步函数,第一层函数之所以是异步的,是因为异步级联的原因导致的。如果UI线程调用异步函数时,其中有一个异步函数使用Wait()同步调用,那么就会造成死锁。如下示例代码。可以使用wait2Async().ConfigureAwait(false);代替wait2Async().Wait();解决该问题,当然最好不要有异步函数同步调用的用法。

    private async void Button_Click(object sender, RoutedEventArgs e)
    {
        Console.WriteLine("Main Start: " + Thread.CurrentThread.ManagedThreadId);
        await wait1Async();
        Console.WriteLine("Main End: " + Thread.CurrentThread.ManagedThreadId);
    }
    
    private async Task wait1Async()
    {
        Console.WriteLine("1 Start: " + Thread.CurrentThread.ManagedThreadId);
        wait1(); //造成死锁
        await wait2Async();
        Console.WriteLine("1 End: " + Thread.CurrentThread.ManagedThreadId);
    }

    private void wait1()
    {
        Console.WriteLine("1 Start: " + Thread.CurrentThread.ManagedThreadId);
        wait2Async().Wait(); //异步函数同步调用可能会造成死锁,可以使用wait2Async().ConfigureAwait(false);代替
        Console.WriteLine("1 End: " + Thread.CurrentThread.ManagedThreadId);
    }

    private async Task wait2Async()
    {
        Console.WriteLine("2 Start: " + Thread.CurrentThread.ManagedThreadId);
        await Task.Delay(10000);
        Console.WriteLine("2 End: " + Thread.CurrentThread.ManagedThreadId);
    }