中的并发编程 净核心
并发编程-异步与多线程代码
并行编程是一个宽泛的术语,我们应该通过观察异步方法和实际多线程之间的区别来讨论它。虽然。NET Core使用任务来表达相同的概念,一个关键的区别是内部处理的不同。异步方法在后台运行,而调用线程正在做其他事情。这意味着这些方法是I/O密集型的,也就是说,它们将大部分时间花在输入和输出操作上,例如文件或网络访问。只要有可能,使用异步输入/输出方法而不是同步操作是有意义的。同时,调用线程可以在桌面应用程序或服务器应用程序中处理用户交互的同时处理其他请求,而不仅仅是等待操作完成。
计算密集型方法需要CPU周期,并且只能在其专用后台线程中运行。并行运行时,中央处理器内核的数量限制了可用线程的数量。操作系统负责在剩余的线程之间切换,给它们执行代码的机会。这些方法仍然同时执行,但不必并行执行。虽然这意味着方法不会同时执行,但是当其他方法挂起时,它们可以执行。
并行与并发
在最后一段中,本文将重点介绍中的多线程和并发编程。NET核心。
任务并行库
那个。NET Framework 4引入了任务并行库(TPL)作为编写并发代码的首选API。那个。NET Core采用相同的编程模式。要在后台运行一段代码,它需要包装成一个任务:
var backgroundTask=Task。run(()=DoComplexCalculation(42));//do other work var result=background task。结果;当需要返回结果时,任务。运行方法接收一个函数;任务方法。当不需要返回结果时,Run会收到一个Action。当然,lambda表达式可以在所有情况下使用,就像在我上面的例子中调用一个带有一个参数的长时间方法一样。线程池中的一个线程将处理该任务。的运行时。NET Core包括一个默认调度器,它使用线程池来处理队列和执行任务。您可以通过派生TaskScheduler类而不是默认的类来实现自己的调度算法,但是这超出了本文的范围。正如我们之前看到的,我使用Result属性来合并被调用的后台线程。对于不需要返回结果的线程,我可以调用Wait()来代替。这两种方法都将被阻止,直到后台任务完成。为了避免阻塞调用线程(如在ASP.NET核心应用程序中),可以使用await关键字:
var backgroundTask=Task。run(()=DoComplexCalculation(42));//do other work var result=wait background task;这样,被调用的线程将被释放以处理其他传入的请求。一旦任务完成,一个可用的工作线程将继续处理该请求。当然,控制器动作方法必须是异步:
公共异步taskiactionresult index()处理的异常{//methodbody}
当两个线程合并在一起时,任务引发的任何异常都将传递给调用线程:
如果使用结果或等待(),它们将被打包在AggregateException中。实际的异常将被抛出并存储在其InnerException属性中。
如果使用await,原始异常将不会被打包。
在这两种情况下,调用堆栈的信息将保持不变。
取消任务
由于任务可以运行很长时间,您可能希望有一个提前取消任务的选项。要实现此选项,您需要在创建任务时传入取消令牌,然后使用该令牌触发任务的取消:
var token source=new cancelationtoken source();var cancellableTask=Task。run(()={ for(int I=0;i 100i ){if(标记源。令牌。iscancelingrequested){//在退出令牌源之前清理。token . throwifcancelationrequested();}//做长时间运行处理}返回42;},令牌源。令牌);//取消任务令牌源。取消();尝试{等待取消任务;} catch(operationcanceledexception e){//handlethe exception }事实上,为了提前取消任务,您需要检查任务中的取消令牌,并在需要取消时做出反应。此方法将引发OperationCanceledException,以便可以在调用线程中执行相应的处理。
协调多任务处理
如果你需要运行多个后台任务,这里有一些方法可以帮助你。要同时运行多个任务,只需连续启动它们并收集它们的引用,例如,在一个数组中:
var backgroundTasks=new []{Task。运行(()=文档计算(1)),任务。运行(()=文档计算(2)),任务。run(()=DoComplexCalculation(3))};现在,您可以使用Task类的静态方法,并等待它们异步或同步执行。
//等待同步询问。wait any(background tasks);任务。wait all(background tasks);//等待异步等待任务。when any(background tasks);等待任务。when all(background tasks);事实上,这两种方法最终都会返回到所有任务本身,可以像任何其他任务一样再次操作。要获取相应任务的结果,可以检查任务的Result属性。处理多任务异常有点棘手。每当任何任务收集到异常时,WaitAll和WhenAll方法都会引发异常。但是,对于WaitAll,所有异常都将被收集到相应的InnerExceptions属性中;对于WhenAll,只引发第一个异常。要确认哪个任务引发哪个异常,您需要分别检查每个任务的状态和异常属性。使用等待和任何时候都要小心。他们会等到第一个任务完成(成功或失败),即使任务发生异常也不会抛出任何异常。它们只返回已完成任务的索引或单独返回已完成任务。您必须等到任务完成,或者在访问它的result属性时,捕获异常,例如:
var completedTask=等待任务。when any(background tasks);尝试{ var result=await completedTask} catch(exception e){//handleexception }如果要连续运行多个任务而不是并发任务,可以使用continuations方法:
var compositeTask=任务。Run(()=DoComplexCalculation(42))。continue with(previous=doanathercomplexcalculation(previous。结果)、任务条件选项。ononrantocompletion)Continuewith()方法允许您逐个执行多个任务。这个继续的任务将获得对前一个任务的结果或状态的引用。您仍然可以添加条件来判断是否执行延续任务,例如,只有在前一个任务成功执行或引发异常时。与连续等待多个任务相比,提高了灵活性。当然,您可以将延续任务与前面讨论的所有功能结合起来:异常处理、取消和并行运行任务。这为您提供了很大的表演空间,并以不同的方式将它们结合起来:
var multipleTasks=new[]{Task。运行(()=文档计算(1)),任务。运行(()=文档计算(2)),任务。run(()=DoComplexCalculation(3))};var combinedTask=Task。WhenAll(多任务);var successfulContinuation=combined task。continue with(task=CombineResults(task。结果)、任务持续选项。ononrantocompletion);var failed continuation=combined task。继续(任务=处理错误(任务。异常),TaskContinuationOptions。NotOnRanToCompletion);等待任务。任何时候(成功继续,失败继续);任务同步
如果任务是完全独立的,那么我们刚才看到的协调方法就足够了。但是,一旦需要同时共享数据,就需要额外的同步来防止数据损坏。当两个或多个线程同时更新一个数据结构时,数据很快就会变得不一致。就像下面的示例代码一样:
var counters=new Dictionary int,int();if(计数器。contains KeY(key)){ counters[KeY];} else { counters[key]=1;}当多个线程同时执行上述代码时,不同线程中特定的指令顺序可能会导致数据不正确,例如:
所有线程都会检查集合中是否存在相同的键结果,并且都会进入else分支,并将这个键的值设置为1,最终的结果将是1而不是2。如果代码一个接一个地执行,那将是预期的结果。在上面的代码中,临界区一次只允许一个线程进入。在C#中,可以通过使用lock语句来实现
var counters=new Dictionary int,int();lock (syncObject){if(计数器。contains KeY(key)){ counters[KeY];} else { counters[key]=1;}}在此方法中,所有线程必须共享同一个syncObject。作为最佳实践,syncObject应该是一个特殊的对象实例,专门用于保护对独立关键区域的访问并避免外部访问。在lock语句中,只允许一个线程访问内部的代码块。它会阻止下一个试图访问它的线程,直到前一个线程退出。这将确保一个线程完全执行关键部分代码,而不会被另一个线程中断。当然,这会降低并行性,降低代码的整体执行速度,所以最好尽量减少关键部分的数量,使其尽可能短。
使用Monitor类简化锁声明:
var lockWasTaken=falsevar temp=syncObject尝试{监视器。输入(温度,参考锁定状态);//lock语句体}最后{ if(locksastken){ Monitor。出口(温度);}}虽然您希望大多数时候都使用lock语句,但Monitor类可以在需要时提供额外的控制。例如,您可以使用TryEnter()代替Enter(),并指定一个有限的时间,以避免无限期等待锁被释放。
其他同步原语
监视器只是中许多同步原语之一。NET核心。根据实际情况,其他图元可能更适合。
Mutex是Monitor的较重版本,它依赖于底层操作系统,并跨多个进程提供对资源的同步访问[1]。它是互斥同步的推荐替代方案。
信号量lim和Semaphore可以限制同时访问资源的线程的最大数量,而不是像Monitor那样只限制一个线程。信号量比信号量轻,但仅限于单个进程。如果可能的话,你最好用信号量来代替信号量。
ReaderWriterLockSlim可以区分两种访问资源的方式。它允许无限数量的读者同时访问资源,并且只限制一个作者同时访问锁定的资源。读取时是线程安全的,但修改数据时需要独占资源,很好的保护了资源。
自动设置事件、手动设置事件和手动设置事件将阻止传入的线程,直到它们收到信号(即调用集())。然后等待的线程将继续执行。自动设置事件将一直阻塞,直到下一次调用set(),并且只允许一个线程继续执行。除非调用Reset(),否则手动重置事件和手动重置事件不会阻塞线程。ManualResetEventSlim比前两个更轻,更值得推荐。
互锁提供了——个原子的选择,这是锁定和其他同步原语(如果适用)的更好选择:
//带有lock lock lock(syncObject){ counter的非原子操作;}//不需要lock互锁的等效原子操作。增量(参考计数器);并发集
当关键领域需要确保对数据结构的原子访问时,用于并发访问的特殊数据结构可能是更好、更有效的替代方案。例如,使用ConcurrentDictionary代替Dictionary可以简化锁语句示例:
var counters=new ConcurrentDictionary int,int();计数器。TryAdd(键,0);lock(SyncObject){ counters[key];}自然,也有可能像下面这样:
计数器。AddOrUpdate(key,1),(oldKey,old value)=old value 1);由于update的委托是临界区外的方法,第二个线程可能在第一个线程更新值之前读取相同的旧值,并用自己的值有效覆盖第一个线程的更新值,从而丢失一个增量。并发集合的误用也是多线程造成的不可避免的问题。并发集合的另一种选择是不可变集合。类似于并发集合,它们也是线程安全的,但是底层实现是不同的。任何与更改数据结构相关的操作都不会更改原始实例。相反,它们返回一个已更改的副本,并保持原始实例不变:
var original=new Dictionary int,int()。toimumtabledictionary();var已修改=原始。添加(键、值);因此,一个线程中对该集合的任何更改对其他线程都是不可见的。因为它们仍然引用原始的未修改的集合,这就是不变集合本质上是线程安全的原因。当然,这使得它们在解决不同集合的问题时很有效。在最好的情况下,多个线程在同一个输入集下独立地修改数据,并且在最后一步中所有线程的更改可能被合并。对于常规集合,需要提前为每个线程创建集合的副本。
平行LINQ
并行LINQ是任务并行库的替代。顾名思义,它非常依赖语言集成查询功能。对于在一个大的集合中执行同样昂贵的操作的场景,这很有用。与所有操作都按顺序执行的普通LINQ到对象不同,PLINQ可以在多个CPU上并行执行这些操作。发挥优势所需的代码更改也非常少:
//顺序执行顺序=可枚举。范围(0,40)。选择(n=费用操作(n))。ToArray();//parallel execution var parallel=Enumerable。范围(0,40)。天冬氨酸()。选择(n=费用操作(n))。ToArray();如您所见,这两个代码片段之间的唯一区别是调用天冬氨酸()。这将IEnumerable转换为ParallelQuery,导致查询的部分并行运行。要切换回顺序执行,可以调用AsSequential(),它将再次返回一个IEnumerable。默认情况下,PLINQ不会保留集合中的顺序以提高流程效率。但是当秩序很重要的时候,你可以打电话给asorded () :
var parallel=可枚举。范围(0,40)。天冬氨酸()。AsOrdered()。选择(n=费用操作(n))。ToArray();同样,您可以通过调用AsUnordered()进行切换。
完整的并发编程。NET框架
作为。NET Core是一个简化的完整实现。NET框架中,所有的并行编程方法。NET框架也可以用于。NET核心。唯一的例外是不可变集合,它们不是完整集合的一部分。NET框架。它们作为单独的NuGet包(系统)分发。集合。不可变的),您需要在项目中安装和使用它。
结论:
每当应用程序包含可以并行运行的CPU密集型代码时,使用并发编程来提高性能和硬件利用率是有意义的。中的API。NET Core抽象了许多细节,使得编写并发代码更加容易。然而,我们应该注意一些潜在的问题,其中大多数涉及从多个线程访问共享数据。如果可以,你应该完全避免这种情况。如果没有,请务必选择最合适的同步方法或数据结构。
以上就是本文的全部内容。希望对大家的学习有帮助,支持我们。
版权声明:中的并发编程 净核心是由宝哥软件园云端程序自动收集整理而来。如果本文侵犯了你的权益,请联系本站底部QQ或者邮箱删除。