.Net中的并行编程-6.常用优化策略

            本文是.Net中的并行编程第六篇,今天就介绍一些我在实际项目中的一些常用优化策略

     一、避免线程之间共享数据

避免线程之间共享数据主要是因为锁的问题,无论什么粒度的锁,最好的线程之间同步方式就是不加锁,这个地方主要措施就是找出数据之间的哪个地方需要共享数据和不需要共享数据的地方,再设计上避免多线程之间共享数据。

在以前做过的某项目,开始时设计的方案:

绘图1

   开始设计时所有的数据都放入到了公共队列,然后队列通知多个线程去处理数据,队列采用互斥锁保证线程同步,造成的结果就是线程进行频繁的上下切换,CPU时间都浪费在了上下文切换上从而导致队列拥堵无法及时处理数据,最终程序因无法处理数据而内存溢出。

  一开始解决这个问题是想到了采用更细粒度的锁如原子操作,但使用原子操作又需要回退机制保证活锁问题还要防止浪费CPU的时间等等一系列的问题。。,如果开发完成其实就相当于自己实现了一个如lock之类的互斥锁。所以重新整理了业务需求,改良后的设计如下:

绘图3

    在数据输入进来以后采用一个单独的数据转发器分发到不同的队列,这样就避免了线程之间的竞争,数据转发器其实就相当于我们开发网站时用到了负载均衡器,而且可以根据队列里面的数据不同选择不用的队列或者可以结合CPU,内存使用率进行动态调度。当然改良后的设计满足了性能需求。

    所以在进行多线程开发时,尽量避免使用锁,对于必须使用锁的情景要选择合适的锁,对于选择什么类型的锁,我的原则是:能满足性能要求就好,不要刻意追求细粒度的锁。粗粒度的锁性能低但易于使用和理解,细粒度的锁性能高但难以使用和理解,关于操作系统的锁的介绍可参考《windows核心编程》线程同步的章节,在.net平台下也可参考《CLR VIA C#》线程相关章节介绍。

二、注意CPU高速缓存失效,避免频繁的上下文切换。

       开发多核程序时高速缓存往往是提高性能的关键,这里的缓存指的是CPU的缓存(L1,L2,L3)。缓存运用得当往往能提高2倍以上的性能。

      1.造成CPU缓存失效的原因有:

      (1)频繁的修改内存中的数据

      (2)使用了原子操作,lock锁等同步方式。

      (3)线程上下文的切换。

      (4)伪共享造成频繁的刷新高速缓存。

      2. 关于频繁的上下文切换造成的原因有:

      (1)程序本身的线程都在抢夺CPU资源,也就是CPU无法调度其他的线程,

      (2)很多线程都在等待获取互斥锁但是只有一个线程能获取,其他线程不断在唤醒和睡眠之间切换。

      3.以上问题的解决方案(只供参考,项目不同方案不同):

      (1)避免使用任何形式的锁。

      (2)在满足性能的前提下,用最少的线程做最少的事。

      (3)再设计上避免修改数据,如以前开发的实时计算的程序,所有的数据只允许读取不允许修改,需要修改则创建一条新数据来代替,类似于Erlang程序的开发方式。

      具体有关高速缓存基础内容可以参考《深入理解计算机操作系统》第六章的内容。

三、线程池的的使用场景和注意点:

       (1)对于执行时间较短的任务都应当交给线程池去处理而不是开启一个新线程,对于需要长时间处理的任务交给单独的线程去处理。

      (2)对于读写文件的任务不要交给线程池处理,因为线程池内的线程属于后台线程,应用程序意外关闭后线程也关闭从而丢失数据。

      (3)永远不要自己开发线程池,真正能用到产品级别线程池需要几个月的时间而且需要大量的测试,否则遇到问题悔之晚矣。

四、关于NUMA架构机器的优化。

        (1)现代服务器基本都是NUMA架构,在这些机器上我们编写程序时最好开启.net的服务端垃圾回收模式,这样我们分配对象时就能在最近使用CPU对应的内存上去分配。

       (2)不要在.net程序中使用绑定线程到指定内核或提升线程优先级的做法,因为如果绑定的线程正好与垃圾回收线程进行竞争那么性能会更慢。

五、选择合适的编程模型

        编写并行程序都有固定的编程模型,基本上其他模型都是这几种模型的自由组合,常见的编程模型:

       (1)数据并行

       (2)任务并行

       (3)流水线并行

六、去队列里拉数据还是队列主动推送数据?

         一般我们写多线程的程序会将数据先放到队列中,然后函数立即返回,后续再有其他的线程进行处理以达到快速响应客户端的目的,这就涉及到队列主动发送信号通知线程处理还是线程定时去队列里拉取数据,如果采用推送的方式可能造成状态丢失最终有些数据得不到处理而一直待在队列中,如果采用拉取的方式线程的睡眠时间不好把握,睡多了数据处理速度慢,睡少了又浪费CPU,所以我一般采用两者结合的方式,具体参考我的第四篇文章《.Net中的并行编程-4.实现高性能异步队列》

七、异步IO还是同步IO?

         异步IO可以解决同步IO的线程阻塞问题(这里的IO分两种:磁盘和网络),基本上所有的web服务器都采用的异步的网络IO,但是对于磁盘最好不要使用异步的磁盘IO,除非在具有SSD的机器上。因为对磁盘异步的读写会造成磁盘存储数据时的碎片化,本来可以顺序写的操作最终可能变成随机写。

      结束语:

     本文设计的内容只是并行程序优化很少一部分,更多内容还需要我们在实践中不断积累。

     本来想写的内容是”.net并行“,结果写完才发现和.net没有关系,当然,这些基础知识和语言没什么太大关系。

     本文设计的内容只是建议,我们在编写程序时不要具有教条主义,合理的才是最好的,某些原则不是适合所有情况,我们所做的是不断探索适应变化以达到提升程序性能的目的。 

原文地址:https://www.cnblogs.com/zw369/p/4304566.html