C#异步

同步是调用一个方法,等待其返回结果。异步是发起一个操作,而不等待结果,后续通过轮询或回调得到结果。

异步一般要使用到多线程。

C#中的异步方式

回调

通过在参数中提供回调函数,可以做完成时通过回调来提供结果。

public static void Sum(int count,Action<int> callback)
{
    ThreadPool.QueueWordItem(_=>{
        int sum = 0;
        for(int i=1;i<=count;i++){
            sum +=i;
        }
        callback(sum);
    });
}

public static void Main(string args)
{
    Console.WriteLine("before sum");
    Sum(100,result =>{
        Console.WriteLine("result:"+result);
    });
    Console.WriteLine("after sum");
    Console.ReadLine();
}

上面的Sum方法就是一个异步方法,它在线程池中进行操作。 新线程执行完成后,通过回调通知调用者。 调用Sum后,Sum立刻返回,不会影响后续操作。

异步编程模型 (APM)

异步编程模型(APM)是通过一对Begin,End方法实现异步。 FCL中许多类型实现了APM,Stream,Socket,Dns,WebRequest等。

以下是Stream类的写数据的方法。

//同步
void Write(byte[] bytes,int offset,int count);
//异步
IAsyncResult BeginWrite(byte[] bytes,int offst,int count,AsyncCallback callback,object state);
void EndWrite(IAsyncResult ar)

异步相关类型的定义


public delegate void AsyncCallback(IAsyncResult ar);

public interface IAsyncResult
{
        //指示异步操作是否已完成。
        //在回调中,总是已完成的。
        Boolean IsCompleted { get; }
        //用于等待异步操作完成的 System.Threading.WaitHandle。
        //通过调用Wait方法等待异步操作完成。
        //一般不使用,等待失去了异步的意义。
        WaitHandle AsyncWaitHandle { get; }
        //包含有关异步操作的信息。
        //调用Begin方法时传递的state参数。
        //一般通过闭包获取参数,用不上这个属性。
        Object AsyncState { get; }
        //指示操作异步操作是否是同步完成。
        //对于一些数据量小的操作,为了效率,即使使用异步调用也可能是同步完成的。
        //一般不查询这个属性
        Boolean CompletedSynchronously { get; }
}

Begin方法前面的参数和同步方法一致,额外有以IAsyncResult接口作为参数的AsyncCallback委托作为完成的回调,以及object类型的附加参数,在回调中,可以查询附加参数。 Begin方法返回值的类型IAsyncResult。 在异步操作完成时,一个线程池线程会调用回调。 一般使用回调提供IAsyncResult,而不是用返回值的IAsyncResult。如果使用返回值,后续需要轮训或者等待操作完成,这样就失去了异步的优势。

End方法包含IAsyncResult类型的参数,需要传入。当传入的是Begin方法返回的值时,End方法会进行等待,直到操作完成;当传入的是AsyncCallback回调中的参数时,此时异步操作已经完成。 End方法的返回值和同步方法一致。 如果有异常,会在调用End方法时抛出。 必须调用End方法,以保证资源被释放。 Begin/End方法需要配对使用,并且需要在同一对象,同一IAsyncResult对象上。

public static void Main(string args)
{
    Stream stream = new MemoryStream();
    byte[] bytes = Encoding.UTF8.GetBytes("hello");

    stream.BeginWrite(bytes,0,bytes.Length,ar=>
    {
        //此时,操作已经完成,但可能出错
        //需要调用End方法获得返回值或错误,以及确保释放资源
        stream.EndWrite(ar);
        Console.WriteLine("write complete");
    },null);
}

APM和异常

当调用Begin方法时,如果参数不符合要求,会抛出异常。 当调用End方法时,如果异步操作有异常,会抛出异常。对于回调的情况,异常在线程池线程抛出。

委托和APM

定义委托时,委托会生成一对BeginInvoke,EndInvoke方法,它们的使用方式和上述一致。

基于事件的异步(EAP)

EAP通过注册事件来处理异步调用的结果。 异步方法名称一般为XXXAsync,并且没有返回值。 事件名称为XXXCompleted,事件数据继承自AsyncCompletedEventArgs

EAP的优点是有设计器支持。

AsyncCompletedEventArgs成员

    public class AsyncCompletedEventArgs : EventArgs
    {
        //是否已取消一个异步操作。
        public Boolean Cancelled { get; }
        //异步操作期间发生的错误。
        public Exception Error { get; }

        public Object UserState { get; }
    }

通过Cancelled属性检查是否取消,Error属性检查是否有异常。 对于有返回值的操作,异步事件参数类型会定义Result属性表示返回值。


WebClient wc = new WebClient();
wc.DownloadStringCompleted += (s, e) =>
{
    if (e.Error != null)
    {
        Console.WriteLine(e.Error.Message);
    }
    else
    {
        Console.WriteLine(e.Result);
    }
};
wc.DownloadStringAsync(new Uri("http:www.baidu.com"));

需要注意的是,需要先注册事件,再调用异步方法,否则可能无法获得结果。

任务 (TAP)

C#4.0后,在System.Thread.Threading.Tasks命名空间,提供了Task,Task<T>,用来表示异步操作。

Task的成员


//表示不返还值的异步操作。
public class Task
{
        //任务是否已完成。
        public Boolean IsCompleted { get; }

        //任务是否由于被取消而完成。
        public Boolean IsCanceled { get; }

        //获取此任务状态
        public TaskStatus Status { get; }

        //获取导致 System.AggregateException 提前结束的 System.Threading.Tasks.Task。 如果
        public AggregateException Exception { get; }

        //获取此任务的ID。
        public Int32 Id { get; }

        //获取用于创建此任务的 System.Threading.Tasks.TaskCreationOptions。
        public TaskCreationOptions CreationOptions { get; }

        //获取在创建 System.Threading.Tasks.Task 时提供的状态对象,如果未提供,则为 null。
        public Object AsyncState { get; }

        //任务是否由于未经处理异常的原因而完成。
        public Boolean IsFaulted { get; }
}

// 表示一个有返回值的异步操作。
public class Task<TResult> : Task
{
    // 获取任务的结果值。
    public TResult Result { get; }
}

创建任务

Task有一个TaskFactory类型的Factory静态属性,通过TaskFactoryStartNew方法,可以创建任务。 使用StartNew方法创建的任务是立即执行的。 如果要手动执行,可以通过Task的构造函数创建任务,调用Start方法执行。


        static void Main(string[] args)
        {
            Task<long> task = Task.Factory.StartNew(() => Sum(1_0000_0000));

            Console.WriteLine(task.Result);
        }

        public static long Sum(long value)
        {
            long sum = 0;
            for (long i = 0; i < value; i++)
            {
                sum += i;
            }
            return sum;
        }

任务默认在线程池执行。 通过Result属性读取结果时,如果任务没有完成,会等待完成。

延续任务

通过Result直接读取会导致程序等待,失去了异步的意义,可以通过ContinueWith方法创建延续任务。 默认情况下,延续任务在线程池执行。

 static void Main(string[] args)
{
    Task.Factory.StartNew(() => Sum(1_0000_0000))
                .ContinueWith(t =>
                {
                    //t是上一个任务,此时任务以及完成,可以检查错误和获取结果
                    Console.WriteLine(t.Result);
                });

    Console.ReadLine();
 }

设置任务调度器

默认情况,任务在线程池执行。

WinForm中,我们希望任务执行完成后在UI线程执行,可以使用TaskScheduler.FromCurrentSynchronizationContext()静态方法获取同步上下文调度器,让任务在UI线程执行。


public static void button1_Click(object sender, EventArgs e)
{
    Task.Factory.StartNew(() => Sum(1_0000_0000))
                .ContinueWith(t =>
                {
                    //这里是UI线程,可以安全访问控件
                    button1.Text = t.Result;
                },TaskScheduler.FromCurrentSynchronizationContext());
}

任务取消

一个任务要支持取消,需要调用者通知自己已经进行了取消,实现这自己进行取消。

这种互相合作的取消方式是协作式取消。

.NET使用CancellationToken表示取消操作的通知。

    public struct CancellationToken
    {
        //初始化
        public CancellationToken(Boolean canceled);

        //返回一个空标记,表示一个未取消而且不支持取消的实例。
        public static CancellationToken None { get; }

        //获取是否已请求取消。
        public Boolean IsCancellationRequested { get; }

        //获取是否可取消。
        //不可取消的标记不会被请求取消,通过实现判断此项可以提高效率。
        public Boolean CanBeCanceled { get; }

        // 如果已请求取消此标记,则引发 System.OperationCanceledException。
        public void ThrowIfCancellationRequested();
    }

public static long Factorial(long count, CancellationToken token)
        {
            long value = 1;
            for (long i = 1; i <= count; i++)
            {
                //当通知取消时抛出异常
                token.ThrowIfCancellationRequested();
                value *= i;
                Thread.Sleep(100);
            }

            return value;
        }

        public static Task<long> FactorialAsync(long value, CancellationToken token)
        {
            return Task.Factory.StartNew(() => Factorial(value, token), token);
        }

        private static async void TaskCancel(CancellationTokenSource tcs)
        {
            try
            {
                long value = await FactorialAsync(10, tcs.Token);
                Console.WriteLine(value);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }

        static void Main(string[] args)
        {
            CancellationTokenSource tcs = new CancellationTokenSource();

            TaskCancel(tcs);

            Console.WriteLine("按任意键取消");
            Console.ReadLine();
            tcs.Cancel();
            Console.ReadLine();
        }

通过CancellationTokenSource类,可以控制取消操作。

将APM转换成Task

TaskFactory类型提供了FromAsync静态方法,可以将APM转换成Task

    public Task WriteAsync(Stream stream, byte[] bytes, int offset, int count)
    {
        return Task.Factory.FromAsync(stream.BeginWrite, stream.EndWrite,
            bytes, offset, count, null);
    }

将EAP转化成Task

EAP转换成Task,需要编码实现。 由于EAP异步方法名称以Async结尾,Task异步方法也以Async结尾,为了作区分,在同时存在两种模式时,Task异步方法使用TaskAsync结尾。


public Task<string> DownloadStringTaskAsync(WebClient wc, Uri url)
{
    TaskCompletionSource<string> tcs = new TaskCompletionSource<string>();

    DownloadStringCompletedEventHandler handler = null;

    handler = (s, e) =>
    {
        wc.DownloadStringCompleted -= handler;

        if (e.Cancelled)
        {
            tcs.TrySetCanceled();
        }
        else if (e.Error != null)
        {
            tcs.TrySetException(e.Error);
        }
        else
        {
            tcs.TrySetResult(e.Result);
        }
    };

    wc.DownloadStringCompleted += handler;

    try
    {
        wc.DownloadStringAsync(url);
    }
    catch (Exception)
    {
        wc.DownloadStringCompleted -= handler;
        throw;
    }

    return tcs.Task;
}

async/await

C#5.0开始,可以使用async/await关键字简化任务的使用。 使用async/await需要.NET Framework4.5

async用来修饰方法,说明方法是一个异步方法。异步方法需要返回Task,Task<T>或者void。 支持void是为了兼容事件处理。

await可以用于async修饰的异步方法内部,用来挂起一个方法,等待方法完成后才继续执行后续代码。

异步方法


public static long Sum(long value)
{
    long sum = 0;
    for (long i = 0; i < value; i++)
    {
        sum += i;
    }
    return sum;
}

public static Task<long> SumAsync(long value)
{
    return Task.Factory.StartNew(()=>Sum(value));
}

public static async Task F()
{
    long sum = await SumAsync(1_0000_0000);
    Console.WriteLine(sum);

    long sum2 = await SumAsync(5000_0000);
    Console.WriteLine(sum2);
}

public static void Main(string[] args)
{
    F().Wait();
}

await使得调用异步方法可以像调用同步方法一样,在其背后是编译器为我们生产了代码。

执行到await时,会开始执行任务,任务完成后,再执行await后面的代码,表面一起的代码实际是分散在多个位置,被多个线程执行的。

public Task F()
{
   return SumAsync(1_0000_0000).ContinueWith(t=>{
       long sum = t.Result;
       Console.WriteLine(sum);

       return SumAsync(5000_0000).ContinueWith(t2=>{
           long sum2 = t2.Result;
           Console.WriteLine(sum2);
       });
   })
}

异步方法类代码和上面代码的作用类似,但是更加简洁,避免了逻辑分布在多个回调里面。

异常处理

在使用async/await时,还可以使用try..catch进行异常处理,这是ContinueWith难以实现的。


try
{
    long sum = await SumAsync(1_0000_0000);
    Console.WriteLine(sum);

    long sum2 = await SumAsync(5000_0000);
    Console.WriteLine(sum2);
}
catch(OverFlowException)
{
    Console.WriteLine("溢出");
}

配置await

默认情况下,await后面的代码是在线程池线程执行的,通过对Task调用ConfigureAwait(true),可以使用原始线程。

对于WinForm,就是让后续代码在UI线程执行。

catch..和finally语句中的await

C#6.0开始,await语句可以放在catchfinally语句块中,至此await功能完全。

支持await的类型

不仅仅Task类型支持await,符合要求的类型都可以,这些类型是awaitable。

awaitable类型要求: 有可访问的方法GetAwaiter,返回类型是awaiter。 GetAwaiter可以是扩展方法。

awaiter要求: 实现ICriticalNotifyCompletionINotifyCompletion接口。 拥有bool IsCompleted{get;}属性。 拥有TResult GetResult()void GetResult()方法。

awaiter的目的使用实现await后续任务的在当前任务完成后的调度。

通用异步方法返回类型

从C#7.0开始,异步方法可返回任何具有可访问的GetAwaiter方法的类型。 支持通用返回类型意味着可返回轻量值类型,从而避免额外的内存分配。 .NET提供System.Threading.Tasks.ValueTask<TResult>结构作为返回任务的通用值的轻量实现。 可以通过NuGet包System.Threading.Tasks.Extensions引用。

异步Main方法

C#7.1开始,Main方法也支持异步了。

static async Task Main(string[] args)
{
    await Task.Factory.StartNew(()=>Console.WriteLine("hello"));
}

异步Main方法是编译器的花招,实际重新定义了一个新的Main函数等待我们的Main函数完成。


//标记为入口点
static void _Main(string[] args)
{
    Main(args).GetAwaiter().GetResult();
}

static async Task Main(string[] args)
{
    await Task.Factory.StartNew(()=>Console.WriteLine("hello"));
}