.Net性能调优——并行和并发

前言

本章讲叙述这个最显而易见的改善程序性能的方式——利用多核处理器。阅读本文你将知道:
1.为什么要有并发和并行
2.CLR如何运行线程池
3.任务并行
4.数据并行
5.数据并行的高级使用方式
6.并行Linq
7.C#异步方法
8.1同步
8.2.无锁代码
8.3.内存模型和volatile变量
9.1.Window主要使用什么实现同步
9.2.Windows和.Net提供了那些同步机制
10.缓存
11..Net中使用GPU计算
12.总结
13.并行开发的趋势

为什么要有并发和并行

1.发起异步I/O操作可以改善应用程序的响应时间
2.跨多个线程的并行任务可以提高系统的资源利用率
3.一次执行多个I/O操作可以提升吞吐量,因为大部分时间都是浪费在等待I/O操作完成上的

CLR如何运行线程池

CLR线程池由若干协同部件组成

1.FIFO(先入先出)队列。当不属于线程池的线程(如主程序线程)将工作发到线程池时,将被压入FIFO队列
2.LIFO(后入先出)队列。每个线程池线程都有自己的LIFO队列,由该线程创建的工作项将被压入这个队列,如果线程池线程空闲时,则会先执行LIFO。如果一个线程池队列的LIFO队列耗尽,会进行"窃取",执行其他线程的LIFO。如果所有的LIFO队列都为空,会执行FIFO队列里的工作项

任务并行

我们将任务视为对线程的高级抽象。C#提供了这些API:
Task.WaitAll(firstTask,secondTask)
Parallel.Invoke(firstTask,secondTask)
PS:并行更适合那些粒度足够粗的任务

数据并行

并行任务主要处理任务,而数据并行是要从直观上移除任务,用一种更高级的抽象——并行循环,来替换任务
Parallel.For(start,end,number=>{})
Parallel.For会考虑政治执行的任务数量,防止因动态划分迭代区间导致的行为粒度过细的问题
Parallel.Foreach(list,item=>{})
Parallel.Foreach不需要使用同步机制,因为当多个线程访问它时,会自动使用同步机制

数据并行的高级使用方式

在共享状态的情况下进行并行循环,首先要考虑的优化是聚合

List<int> primes=new List<int>();
Parallel.For(3,200000
  ()=>new List<int>(),
  (i,pls,localPrimes)=(){
  localPrimes.Add(i);
  return localPrimes;
  },
  localPrimes=>{
    (lock)(primes){
      prims.AddRange(localPrimes);
    }
  }
);

在上述代码中,锁的数量大大减少了。我们只需要对每个执行并行循环的线程使用一次锁。

Parallel.For(Partitioner.Create(3,200000),range=>{
  for(int i=range.Item1;i<range.Item2;++i)
});

在上述代码中,优化了过小的循环迭代无法有效的并行执行的问题

并行Linq

只需要将IEnumerable<T>改成ParalleQuery<T>即可,例如:list.AsParallel()

C#异步方法

firstTask.ContinueWith(_=>{})
TaskFactory.ContinueWhenAll(new Task{first,right})
TaskFactory.ContinueWhenAny(new Task{first,right})
我们也可以使用async和await来表示后续操作,不必再用ContinueWith API

同步

除了只读数据,任何访问共享内存地址的行为都需要同步,但不同的同步机制会产生不同的性能和伸缩性
就连++i操作,都将被翻译成3条机器执行执行

无锁代码

CAS同步原语,这是一种将同步的重任交给操作系统的方法

int n=1000;
if(Interlocked.CompareExchange(ref n,1,0)==0){
  //dosomething
}

C#用自旋锁SpinLock将其封装

.内存模型和volatile变量

一般来说,某种特殊语言或环境的内存模型所描述的是编译器和处理器如何在不同的线程所工作的内存上对操作指令进行重新排序
C#开发者有很多办法可以解决内存指令重排的问题。
1.使用volatile关键字
2.Windows同步机制和原始的无锁同步机制,都会在必要的时候发布一个内存栅栏来避免重排

volatile int locked;

Window主要使用什么实现同步

Windows内核主要是通过阻塞来实现同步的。如果锁正在被占用,那么要获取该锁的线程就会被阻塞
Windows内核大量使用自旋锁来实现内部同步。例如,调度器数据库、文件系统缓存块列表等。但是也会有问题:
1.从先进先出的角度来说,自旋锁是不公平的,本质上是一直轮询请求锁
2.自旋锁的拥有者释放锁之后,当前自旋的所有处理器缓存都会失效,但其实只有一个处理器真正得到锁
PS:所以在一些很快速的操作上,可以使用自旋锁

Windows和.Net提供了那些同步机制

Windows提供了为数众多的同步机制,如事件、信号量、互斥锁及条件变量
有这些信号量:
*Mutex
*Semaphore
*ManualResetEvent
*AutoResetEvent
*Monitor
*Barrier
*ReaderWriterLock(读)
*RederWriterLock(写)

缓存

在并行程序中,单处理器上的缓存大小和命中率就十分重要,多处理器缓存之间的交互就更加重要了
当某个处理器向处于其他处理器缓存中的内存位置写入数据时(获取锁后),硬件就会造成缓存失效,将位于其他处理器缓存行的缓存标记成无效
例如:同上4个线程访问一个数组,一个缓存行里有16个整数(1缓存行=64字节=16个整数),失效后就需要重新读取 ,这就是性能的浪费

.Net中使用GPU计算

这里想说一点废话,大家不要打我!
发现身边有很多的程序员并不是工科出生,或者并没有认认真真的学习过计算机工程,每天都只坐着copy的工作。不,这不是程序员,这是码农。仅代表个人观点的说,基础知识真的很重要
CPU和GPU的差别就是人和机器人,CPU什么都能做,GPU需要事先设定好,所以一般都使用GPU做图像渲染。但是GPU占据体积大,核心数量多,处理速度快等等优势。如果能使用GPU做计算,性能的提升是数量级的
在.Net中使用GPU计算只需要加上[Kernel]特性即可,前提是引用GPU.Net或者CUDAfy.Net,他们都是商业发行版,这里没办法做演示,抱歉了大家!

总结

通过本章的叙述,可以明确地指出:并行对于性能的优化工作来说是至关重要的。因为程序无法充分的挖掘计算机的全部计算能力,那么空闲的CPU和GPU就是浪费的。换句话说"CPU跑不到百分百,这是啥家庭啊,家里有矿啊"

并行开发的趋势

如果小编有能力,以后会给大家带来小编还未涉及的领域——分布式计算和"云"
PS:谢谢大家


13767355-da086e62270f0bdf.PNG
捕获.PNG

猜你喜欢

转载自blog.csdn.net/weixin_33975951/article/details/87129145