异步编程

如果一个程序调用某个方法,等待其执行所有处理后才继续执行,我们就称这样的方法是同步的。相反,异步的方法在处理完成之前就返回到调用方法。在异步程序中,程序代码不需要按照编写时的顺序严格执行。有时需要在一个新的线程中运行一部分代码,有时无需创建新的线程,但为了更好地利用单个线程的能力,需要改变代码的执行顺序。
.NET Framework提供了执行异步操作的三种模式:

  • 异步编程模型(APM)模式(也称为IAsyncResult的模式),其中异步操作要求Begin和End方法(例如,BeginWrite和EndWrite异步写入操作)。这种模式不再被推荐用于新开发。
  • 基于事件的异步模式(EAP),它需要一个具有Async后缀的方法,并且还需要一个或多个事件,事件处理程序委托类型和被EventArg派生类型。EAP在 .NET Framework 2.0中引入。不再推荐新的开发。
  • 基于任务的异步模式(TAP),它使用单一方法来表示异步操作的启动和完成。TAP在 .NET Framework 4中引入,是 .NET Framework中推荐的异步编程方法。C#中的async和等待关键字,Visual Basic语言中的Async和Await运算符为TAP添加语言支持。

async/await

基于Task的异步编程模式(TAP)是Microsoft为.Net平台下使用Task进行编程所提供的一组建议,这种模式提供了可以被await调用方法的APIs。
任务跟线程不是一对一的关系,比如开10个任务并不是说会开10个线程,这一点任务有点类似线程池,但是任务相比线程池有很小的开销和精确的控制。

async/await该特性由三个部分组成:

  • 调用方法(calling method):该方法调用异步方法,然后在异步方法(可能在相同的线程,也可能在不同的线程)执行其任务的时候继续执行。
  • 异步(async)方法:该方法异步执行其工作,然后立即返回到调用方法。
  • await表达式:用于异步方法内部,指明需要异步执行的任务。一个异步方法可以包含任意多个await表达式,不过如果一个都不包含的话编译器会发出警告。

在语法上,异步方法具有如下特点:

  • 方法头中包含async方法修饰符;异步方法的名称应该以Async为后缀。
  • 包含一个或多个await表达式,表示可以异步完成的任务。
  • 必须具备以下三种返回类型。第二种(Task)和第三种(Task)的返回对象表示将在未来完成的工作,调用方法和异步方法可以继续执行。
    • void 调用方法不需要与异步方法做进一步交互,“调用即忘记”(fire and forget)。
    • Task 调用方法不需要从异步方法返回值,但需要检测异步方法的状态。
    • Task< T > 调用方法需要从调用中获取一个T类型的值;通过读取Result属性来获取这个T类型的值。
  • 异步方法的参数可以为任意类型任意数量,但不能为out或ref参数。
  • 除了方法以外,Lambda表达式和匿名方法也可以作为异步对像。

await表达式

await表达式指定了一个异步执行的任务,其语法由await关键字和一个空闲对象(任务)组成。这个任务是一个awaitable类型的实例,可能是一个Task类型的对象,也可能不是。awaitable类型是指包含GetAwaiter方法的类型,一般使用Task类即可。
可以使用Task.Run方法来创建一个Task,它是在不同的线程上运行你的方法。以下面一个签名为例,它以Func< TReturn >委托为参数,展示三种实现方式:

class MyClass
{
    public int Get10()
    {
        return 10;
    }

    public async Task DoWorkAsync()
    {
        //3种实现方式
        Func<int> ten = new Func<int>(Get10);
        int a = await Task.Run(ten);

        int b = await Task.Run(new Func<int>(Get10));//创建Func<int>委托

        int c = await Task.Run(() => { return 10; }); //Lambda表达式
        Console.WriteLine("{0} {1} {2}", a, b, c);

        //4种不同的委托类型所表示的方法
        await Task.Run(() => Console.WriteLine(5.ToString()));//Action
        Console.WriteLine((await Task.Run(() => 6)).ToString());//TResult Func()
        await Task.Run(() => Task.Run(() => Console.WriteLine(7.ToString())));//Task Func()
        Console.WriteLine((await Task.Run(() => Task.Run(() => 8))).ToString()); //Task<TRsult> Func()
    }
}

取消一个异步操作

异步方法允许你请求终止执行。System.Threading.Tasks命名空间中有两个类是为此目的而设计的:CancellationToken和CancellationTokenSource。

  • CancellationToken对象包含一个任务是否应被取消的信息。
  • 拥有CancellationToken对象的任务需要定期检查其令牌(token)状态。如果CancellationToken对象的IsCancellationRequested属性为true,任务需停止其操作并返回。
  • CancellationToken是不可逆的,并且只能使用一次。也就是说,一旦IsCancellationRequested属性被设置为true,就不能更改了。

下面的代码展示了如何使用CancellationTokenSource和CancellationToken来实现取消操作

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

namespace tutorial_async
{
    class CancellationTest
    {
        static void cancell() {
            CancellationTokenSource cts = new CancellationTokenSource();
            CancellationToken token = cts.Token;
            var mc = new MyClass2();
            Task t = mc.RunAsync(token);

            //Thread.Sleep(3000)
            //cts.Cancel(); //取消操作

            t.Wait(); //等待Task对象完成
            Console.WriteLine("Was Cancelled:{o}", token.IsCancellationRequested);
        }
    }

    class MyClass2
    {
        public async Task RunAsync(CancellationToken ct)
        {
            if (ct.IsCancellationRequested) return;
            await Task.Run(() => CycleMethod(ct), ct);
        }

        void CycleMethod(CancellationToken ct)
        {
            Console.WriteLine("Starting CycleMethod");
            const int max = 5;
            for (int i = 0; i < max; i++)
            {
                if (ct.IsCancellationRequested) return;
                Thread.Sleep(1000);
                Console.WriteLine("{0} of {1} iterations completed", i + 1, max);
            }
        }
    }
}

等待任务

通过WaitAll和WaitAny方法可以同步地等待一组Task对象,另外,可以通过Task.WhenAll和Task.WhenAny方法实现在异步方法中异步地等待任务。Task.WhenAll和Task.WhenAny方法称为组合子(combinator)。

using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;

namespace book_tutorial_async
{
    class WhenTest
    {
        public void DoRun()
        {
            Task<int> t = CountCharactersAsync("http://www.microsoft.com","http://www.illustratedshape.com");
            Console.WriteLine("DoRun: Task{0} Finished", t.IsCompleted ? "" : "Not");
            Console.WriteLine("DoRun:Result={0}", t.Result);
        }

        private async Task<int> CountCharactersAsync(string site1, string site2)
        {
            WebClient wc1 = new WebClient();
            WebClient wc2 = new WebClient();
            Task<string> t1 = wc1.DownloadStringTaskAsync(new Uri(site1));
            Task<string> t2 = wc1.DownloadStringTaskAsync(new Uri(site2));

            List<Task<string>> tasks = new List<Task<string>>();
            tasks.Add(t1);
            tasks.Add(t2);

            await Task.WhenAll(tasks);
            Console.WriteLine("CCA:T1 {0} Finished",t1.IsCompleted?"":"Not");
            Console.WriteLine("CCA:T1 {0} Finished", t2.IsCompleted ? "" : "Not");

            return t1.IsCompleted ? t1.Result.Length : t2.Result.Length;
        }
    }
}

另外,Task.Delay方法可以将对象暂停在线程中的处理,并在一定时间之后完成。与Thread.Sleep阻塞线程不同的是,Task.Delay不会阻塞线程,线程可以继续处理其他工作。

ConfigureAwait

在Task里中有ConfigureAwait这么一个方法。一个async方法是由多个同步执行的程序块组成.每个同步程序块之间由await语句分隔.用await语句等待一个任务完成.当该方法在await处暂停时,就可以捕捉上下文(context).如果当前SynchronizationContext不为空,这个上下文就是当前SynchronizationContext.如果为空,则这个上下文为当前TaskScheduler.该方法会在这个上下文中继续运行.一般来说,运行UI线程时采用UI上下文,处理ASP.NET请求时采用ASP.NET请求上下文,其它很多情况则采用线程池上下文。" 这句话已经基本讲明了其实后续代码会下上文中执行。这个上下文一般时UI上下文(运行在UI上)或请求上下文(ASP. NET) 这两个可以说时原始上下文,而其它情况采用线程池上下文,也就是开辟一个新线程。这么说也就是ConfigureAwait方法是将后续代码是送到原始上下文还是线程池上下文中。

BackgroundWorker类

前面介绍了如何使用async/await特性来异步地处理任务。这里将学习另一种实现异步工作的方式——即后台线程。async/await特性更适合那些需要在后台完成的不相关的小任务。但有时候,你可能需要另建一个线程,在后台持续运行以完成某项工作,并不时地与主线程进行通信。Backgroundworker类就是为此而生。
下图展示了此类的主要成员。

Backgroundworker类

图中一开始的两个属性用于设置后台任务是否可以把它的进度汇报给主线程以及是否支持从主线程取消。可以用第三个属性来检查后台任务是否正在运行。
类有三个事件,用于发送不同的程序事件和状态。你需要为自己的程序写这些事件的事件处理方法来执行适合程序的行为。

  • 在后台线程开始的时候触发DoWork。
  • 在后台任务汇报状态的时候触发ProgressChanged事件。
  • 后台工作线程退出的时候触发RunMorkercompleted事件。

三个方法用于初始化行为或改变状态。

  • 调用RunMorkerAsync方法获取后台线程并且执行DoWork事件处理程序。
  • 调用CancelAsync方法把CancellationPending属性设置为true。Dowork事件处理程序需要检查这个属性来决定是否应该停止处理。
  • Dolork事件处理程序(在后台线程)在希望向主线程汇报进度的时候,调用Report-Progress方法。

并行循环

这里将简要介绍任务并行库(Task ParellelLibrary)。它是BCL中的一个类库,极大地简化了并行编程。最简单的两个结构是Parallel.For循环和Parallel.ForEach循环。这两个结构位于System.Threading.Tasks命名空间中。

至此,我相信你应该很熟悉C#的标准for和foreach循环了。这两个结构非常普遍,且极其强大。许多时候我们的循环结构的每一次迭代依赖于之前那一次迭代的计算或行为。但有的时候又不是这样。如果迭代之间彼此独立,并且程序运行在多核处理器的机器上,若能将不同的选代放在不同的处理器上并行处理的话,将会获益匪浅。Parallel.For和Parallel.ForEach结构就是这样做的。

如下代码是使用Parallel.For和Foreach结构的例子。第一个例子从0到14迭代(记住实际的参数15超出了最大迭代索引)并且打印出迭代索引和索引的平方。该应用程序满足各个选代之间是相互独立的条件。另外的两个例子是以并行方式填充一个整数数组,和输出每个string的字符数。
还要注意,必须使用System.Threading.Tasks命名空间。

using System;
using System.Net.Http.Headers;
using System.Threading.Tasks;

namespace book_tutorial_async
{
    class ParallelTest
    {
        /// <summary>
        /// echo number
        /// </summary>
        static void Ex1()
        {
            Parallel.For(0, 15, i => Console.WriteLine("The square of{0} is {1}", i, i * i));
        }

        /// <summary>
        /// fill in list
        /// </summary>
        static void Ex2()
        {
            const int maxValues = 50;
            int[] squares = new int[maxValues];

            Parallel.For(0, maxValues, i => squares[i] = i * i);
        }

        /// <summary>
        /// echo the count of string
        /// </summary>
        static void Ex3()
        {
            const int maxValues = 50;
            string[] squares = { "We", "hold", "these", "truths", "to", "be", "self-evident", "that", "all", "men" };

            Parallel.ForEach(squares,i=>Console.WriteLine(string.Format("{0} has {1} letters",i,i.Length)));
        }
    }
}

异步编程对性能的影响

在.NET异步编程中,async和await不会创建其他线程,同时异步方法不会在其自身线程上运行,因此它不需要多线程。只有当方法处于活动状态时,该方法将在当前同步上下文中运行并使用线程上的时间。可以使用Task.Run将占用大量CPU的工作移到后台线程,但是后台线程不会帮助正在等待结果的进程变为可用状态。

对于异步编程而言,基于异步的方法优于几乎每个用例中的现有方法。具体而言,这种方法优于BackgroundWorker的I/O绑定操作因为代码更简单且无需防止争用条件。结合Task.Run使用时,异步编程比BackgroundWorker更适用于CPU绑定的操作,因为异步编程将运行代码的协调细节与Task.Run传输至线程池的工作区分开来。
那么异步编程对线程的影响又是什么呢,相比大家应该都知道,ASP.NET中有两类线程,工作线程,和IO线程。其中工作线程处理普通请求的线程,也是我们用得最多的线程。这个线程是有限的,是根CPU的个数相关的。IO线程,比如与文件读写,网络操作等是可以异步实现并且使性能提升的地方。I/O线程通常情况下是空闲的。所以可以使用IO线程来代替工作线程,一方面充分运用了系统资源,另一方面也节省了工作线程调度及切换所带来的损耗。由此我们需要明白,在I/O密集型处理时,使用异步可以带来很大的提升,比如数据库操作以及网络操作。

即便异步编程带来性能的提升,但是运用不慎,也会对系统性能产生反作用,比如直接使用Task.Run或者Task.Factory.StartNew所带来的异步编程,这些方式会占用工作线程以及工作线程之间的切换。

异步和多线程的区别

异步是相对同步而言的,我们知道异步是开启了新线程,但是和多线程不是一个概念,异步相当于一个人的“大脑”能够做试卷,又能够看电影,同时处理两件以上不同的事情。多线程好比多个人做不同的事情。
因为异步操作无须额外的线程负担,并且使用回调的方式进行处理,在设计良好的情况下,处理函数可以不必使用共享变量(即使无法完全不用,最起码可以减少 共享变量的数量),减少了死锁的可能。当然异步操作也并非完美无暇。编写异步操作的复杂程度较高,程序主要使用回调方式进行处理,与普通人的思维方式有些出入,而且难以调试。多线程的优点很明显,线程中的处理程序依然是顺序执行,符合普通人的思维习惯,所以编程简单。但是多线程的缺点也同样明显,线程的使用(滥用)会给系统带来上下文切换的额外负担。并且线程间的共享变量可能造成死锁的出现。

异步和多线程的适用环境

当需要执行I/O操作时,使用异步操作比使用线程+同步 I/O操作更合适。I/O操作不仅包括了直接的文件、网络的读写,还包括数据库操作、Web Service、HttpRequest以及 .net Remoting等跨进程的调用。
而线程的适用范围则是那种需要长时间CPU运算的场合,例如耗时较长的图形处理和算法执行。但是往往由于使用线程编程的简单和符合习惯,所以很多朋友往往会使用线程来执行耗时较长的I/O操作。这样在只有少数几个并发操作的时候还无伤大雅,如果需要处理大量的并发操作时就不合适了。

异步方法是一个功能强大的高效工具,使您能够更轻松编写可伸缩和响应更快的库和应用程序。 请牢记一点,异步不是对单个操作的性能优化。 采用同步操作并使其异步化必然会降低该操作的性能,因为它仍然需要完成同步操作的所有工作,只不过现在会有额外的限制和注意事项。 关注异步的一个原因是其总体性能:如果采用异步方法编写所有内容,整个系统的执行效果如何。这样仅消耗执行需要的有价值的资源,重叠 I/O 并实现更好的系统利用率。从现在开始,无论何时准备在 .NET Framework 中开发异步代码,异步方法都是首选的工具。

参考:
MSDN(Asynchronous programming patterns)
使用 Async 和 Await 的异步编程
C#并发编程之异步编程

posted @ 2020-10-07 23:11  Jamest  阅读(98)  评论(0编辑  收藏  举报