1.前言 2.开宗明义 3.开发原则和要点 (1)并发编程概述 (2)异步编程基础 (3)并行开发的基础 (4)测试技巧 (5)集合 (6)函数式OOP (7)同步 1.前言最近趁着项目的一段平稳期研读了不少书籍,其中《C#并发编程经典实例》给我的印象还是比较深刻的。当然,这可能是由于近段日子看的书大多嘴炮大于实际,如《HeadFirst设计模式》《Crackingthecodinginterview》等,所以陡然见到一本打着“实例”旗号的书籍,还是挺让我觉得耳目一新。本着分享和加深理解的目的,我特地整理了一些笔记(主要是Web开发中容易涉及的内容,所以部分章节如数据流,RX等我看了看就直接跳过了),以供审阅学习。语言和技术的魅力,真是不可捉摸 2.开宗明义一直以来都有一种观点是实现底层架构,编写驱动和引擎,或者是框架和工具开发的才是高级开发人员,做上层应用的人仅仅是“码农”,其实能够利用好平台提供的相关类库,而不是全部采用底层技术自己实现,开发出高质量,稳定的应用程序,对技术能力的考验并不低于开发底层库,如TPL,async,await等。 3.开发原则和要点(1)并发编程概述并发:同时做多件事情 多线程:并发的一种形式,它采用多个线程来执行程序 并行处理:把正在执行的大量的任务分割成小块,分配给多个同时运行的线程 并行处理是多线程的一种,而多线程是并发的一种处理形式 异步编程:并发的一种形式,它采用future模式或者callback机制,以避免产生不必要的线程 异步编程的核心理念是异步操作:启动了的操作会在一段时间后完成。这个操作正在执行时,不会阻塞原来的线程。启动了这个操作的线程,可以继续执行其他任务。当操作完成后,会通知它的future,或者调用回调函数,以便让程序知道操作已经结束 await关键字的作用:启动一个将会被执行的Task(该Task将在新线程中运行),并立即返回,所以await所在的函数不会被阻塞。当Task完成后,继续执行await后面的代码 响应式编程:并发的一种基于声明的编程方式,程序在该模式中对事件作出反应 不要用void作为async方法的返回类型!async方法可以返回void,但是这仅限于编写事件处理程序。一个普通的async方法如果没有返回值,要返回Task,而不是void async方法在开始时以同步方式执行。在async方法内部,await关键字对它的参数执行一个异步等待。它首先检查操作是否已经完成,如果完成了,就继续运行(同步方式)。否则,它会暂停async方法,并返回,留下一个未完成的task。一段时间后,操作完成,async方法就恢复运行。 await代码中抛出异常后,异常会沿着Task方向前进到引用处 你一旦在代码中使用了异步,最好一直使用。调用异步方法时,应该(在调用结束时)用await等待它返回的task对象。一定要避免使用Task.Wait或Task .Result方法,因为它们会导致死锁线程是一个独立的运行单元,每个进程内部有多个线程,每个线程可以各自同时执行指令。每个线程有自己独立的栈,但是与进程内的其他线程共享内存 每个.NET应用程序都维护着一个线程池,这种情况下,应用程序几乎不需要自行创建新的线程。你若要为COMinterop程序创建SAT线程,就得创建线程,这是唯一需要线程的情况 线程是低级别的抽象,线程池是稍微高级一点的抽象 并发编程用到的集合有两类:并发变成+不可变集合 大多数并发编程技术都有一个类似点:它们本质上都是函数式的。这里的函数式是作为一种基于函数组合的编程模式。函数式的一个编程原则是简洁(避免副作用),另一个是不变性(指一段数据不能被修改) .NET4.0引入了并行任务库(TPL),完全支持数据并行和任务并行。但是一些资源较少的平台(例如手机),通常不支持TPL。TPL是.NET框架自带的 (2)异步编程基础指数退避是一种重试策略,重试的延迟时间会逐次增加。在访问Web服务时,最好的方式就是采用指数退避,它可以防止服务器被太多的重试阻塞 staticasyncTaskstringDownloadStringWithRetries(stringuri){using(varclient=newHttpClient()){//第1次重试前等1秒,第2次等2秒,第3次等4秒。varnextDelay=TimeSpan.FromSeconds(1);for(inti=0;i!=3;++i){try{returnawaitclient.GetStringAsync(uri);}catch{}awaitTask.Delay(nextDelay);nextDelay=nextDelay+nextDelay;}//最后重试一次,以便让调用者知道出错信息。returnawaitclient.GetStringAsync(uri);}} Task.Delay适合用于对异步代码进行单元测试或者实现重试逻辑。要实现超时功能的话,最好使用CancellationToken 如何实现一个具有异步签名的同步方法。如果从异步接口或基类继承代码,但希望用同步的方法来实现它,就会出现这种情况。解决办法是可以使用Task.FromResult方法创建并返回一个新的Task 对象,这个Task对象是已经完成的,并有指定的值使用IProgress 和Progress类型。编写的async方法需要有IProgress参数,其中T是需要报告的进度类型,可以展示操作的进度Task.WhenALl可以等待所有任务完成,而当每个Task抛出异常时,可以选择性捕获异常 Task.WhenAny可以等待任一任务完成,使用它虽然可以完成超时任务(其中一个Task设为Task.Delay),但是显然用专门的带有取消标志的超时函数处理比较好 第一章提到async和上下文的问题:在默认情况下,一个async方法在被await调用后恢复运行时,会在原来的上下文中运行。而加上扩展方法ConfigureAwait(false)后,则会在await之后丢弃上下文 (3)并行开发的基础Parallel类有一个简单的成员Invoke,可用于需要并行调用一批方法,并且这些方法(大部分)是互相独立的 staticvoidProcessArray(double[]array){Parallel.Invoke(()=ProcessPartialArray(array,0,array.Length/2),()=ProcessPartialArray(array,array.Length/2,array.Length));}staticvoidProcessPartialArray(double[]array,intbegin,intend){//计算密集型的处理过程...} 在并发编程中,Task类有两个作用:作为并行任务,或作为异步任务。并行任务可以使用阻塞的成员函数,例如Task.Wait、Task.Result、Task.WaitAll和Task.WaitAny。并行任务通常也使用AttachedToParent来建立任务之间的“父/子”关系。并行任务的创建需要用Task.Run或者Task.Factory.StartNew。 相反的,异步任务应该避免使用阻塞的成员函数,而应该使用await、Task.WhenAll和Task.WhenAny。异步任务不使用AttachedToParent,但可以通过await另一个任务,建立一种隐式的“父/子”关系。 (4)测试技巧MSTest从VisualStudio版本开始支持asyncTask类型的单元测试 如果单元测试框架不支持asyncTask类型的单元测试,就需要做一些额外的修改才能等待异步操作。其中一种做法是使用Task.Wait,并在有错误时拆开AggregateException对象。我的建议是使用NuGet包Nito.AsyncEx中的AsyncContext类 这里附上一个ABP中实现的可操作AsyncHelper类,就是基于AsyncContext实现 ///summary///Providessomehelpermethodstoworkwithasyncmethods.////summarypublicstaticclassAsyncHelper{///summary///Checksifgivenmethodisanasyncmethod.////summary///paramname="method"Amethodtocheck/parampublicstaticboolIsAsyncMethod(MethodInfomethod){return(method.ReturnType==typeof(Task) (method.ReturnType.IsGenericTypemethod.ReturnType.GetGenericTypeDefinition()==typeof(Task)));}///summary///Runsaasyncmethodsynchronously.////summary///paramname="func"Afunctionthatreturnsaresult/param///typeparamname="TResult"Resulttype/typeparam///returnsResultoftheasyncoperation/returnspublicstaticTResultRunSyncTResult(FuncTaskTResultfunc){returnAsyncContext.Run(func);}///summary///Runsaasyncmethodsynchronously.////summary///paramname="action"Anasyncaction/parampublicstaticvoidRunSync(FuncTaskaction){AsyncContext.Run(action);}} 在async代码中,关键准则之一就是避免使用asyncvoid。我非常建议大家在对asyncvoid方法做单元测试时进行代码重构,而不是使用AsyncContext。 (5)集合线程安全集合是可同时被多个线程修改的可变集合。线程安全集合混合使用了细粒度锁定和无锁技术,以确保线程被阻塞的时间最短(通常情况下是根本不阻塞)。对很多线程安全集合进行枚举操作时,内部创建了该集合的一个快照(snapshot),并对这个快照进行枚举操作。线程安全集合的主要优点是多个线程可以安全地对其进行访问,而代码只会被阻塞很短的时间,或根本不阻塞 ConcurrentDictionary是数据结构中的精品,它是线程安全的,混合使用了细粒度锁定和无锁技术,以确保绝大多数情况下能进行快速访问. ConcurrentDictionary内置了AddOrUpdate,TryRemove,TryGetValue等方法。如果多个线程读写一个共享集合,使用ConcurrentDictionary是最合适的,如果不会频繁修改,那就更适合使用ImmutableDictionary。而如果是一些线程只添加元素,一些线程只移除元素,最好使用生产者/消费者集合 (6)函数式OOP异步编程是函数式的(functional),.NET引入的async让开发者进行异步编程的时候也能用过程式编程的思维来进行思考,但是在内部实现上,异步编程仍然是函数式的 伟人说过,世界既是过程式的,也是函数式的,但是终究是函数式的 可以用await等待的是一个类(如Task对象),而不是一个方法。可以用await等待某个方法返回的Task,无论它是不是async方法。 类的构造函数里是不能进行异步操作的,一般可以使用如下方法。相应的,我们可以通过varinstance=newProgram.CreateAsync(); classProgram{privateProgram(){}privateasyncTaskProgramInitializeAsync(){awaitTask.Delay(TimeSpan.FromSeconds(1));returnthis;}publicstaticTaskProgramCreateAsync(){varresult=newProgram();returnresult.InitializeAsync();}} 在编写异步事件处理器时,事件参数类最好是线程安全的。要做到这点,最简单的办法就是让它成为不可变的(即把所有的属性都设为只读) (7)同步同步的类型主要有两种:通信和数据保护 如果下面三个条件都满足,就需要用同步来保护共享的数据 多段代码正在并发运行 这几段代码在访问(读或写)同一个数据 至少有一段代码在修改(写)数据 观察以下代码,确定其同步和运行状态 classSharedData{publicintValue{get;set;}}asyncTaskModifyValueAsync(SharedDatadata){awaitTask.Delay(TimeSpan.FromSeconds(1));data.Value=data.Value+1;}//警告:可能需要同步,见下面的讨论。asyncTaskintModifyValueConcurrentlyAsync(){vardata=newSharedData();//启动三个并发的修改过程。vartask1=ModifyValueAsync(data);vartask2=ModifyValueAsync(data);vartask3=ModifyValueAsync(data);awaitTask.WhenAll(task1,task2,task3);returndata.Value;} 本例中,启动了三个并发运行的修改过程。需要同步吗?答案是“看情况”。如果能确定这个方法是在GUI或ASP.NET上下文中调用的(或同一时间内只允许一段代码运行的任何其他上下文),那就不需要同步,因为这三个修改数据过程的运行时间是互不相同的。例如,如果它在GUI上下文中运行,就只有一个UI线程可以运行这些数据修改过程,因此一段时间内只能运行一个过程。因此,如果能够确定是“同一时间只运行一段代码”的上下文,那就不需要同步。但是如果从线程池线程(如Task.Run)调用这个方法,就需要同步了。在那种情况下,这三个数据修改过程会在独立的线程池线程中运行,并且同时修改data.Value,因此必须同步地访问data.Value。 不可变类型本身就是线程安全的,修改一个不可变集合是不可能的,即便使用多个Task.Run向集合中添加数据,也并不需要同步操作 线程安全集合(例如ConcurrentDictionary)就完全不同了。与不可变集合不同,线程安全集合是可以修改的。线程安全集合本身就包含了所有的同步功能 关于锁的使用,有四条重要的准则 限制锁的作用范围(例如把lock语句使用的对象设为私有成员) 文档中写清锁的作用内容 锁范围内的代码尽量少(锁定时不要进行阻塞操作) 在控制锁的时候绝不运行随意的代码(不要在语句中调用事件处理,调用虚拟方法,调用委托) 如果需要异步锁,请尝试SemaphoreSlim 不要在ASP.NET中使用Task.Run,这是因为在ASP.NET中,处理请求的代码本来就是在线程池线程中运行的,强行把它放到另一个线程池线程通常会适得其反 (7)实用技巧 程序的多个部分共享了一个资源,现在要在第一次访问该资源时对它初始化 staticint_simpleValue;staticreadonlyLazyTaskintMySharedAsyncInteger=newLazyTaskint(()=Task.Run(async()={awaitTask.Delay(TimeSpan.FromSeconds(2));return_simpleValue++;}));asyncTaskGetSharedIntegerAsync(){intsharedValue=awaitMySharedAsyncInteger.Value;} 来源:北京有没有专业看白癜风的医院白癜风怎么能好
|