你的位置:首页 > ASP.net教程

[ASP.net教程]并发思想提炼(2)(Lock free,轮询及线程池)


8.    告别Lock

不是一直说Lock比较麻烦危险吗,那就不要好了。其实有一个Lock free的方法。

首先引入一个概念——原子变量。在这种变量上的操作是原子操作(atomic operation)。原子操作就是说这个操作要么都完成,要么都不完成,部分完成是不行的。就像物理化学中的原子一样,借用不可再分的意思。按照这样理解,对这个原子变量的访问操作就必定是串行的。一个原子操作完成后才能进行另一个原子操作。这样子的变量类型多半是基本变量,什么int啊double啊boolean啊之类。

就可以简单这样想,只要调用原子操作,就能保证对象的串行访问。

常用语言原子操作内部实现基本用到了锁。原子操作常用的有++,--,compare & swap。特别地,这个CAS(compare and swap)配合循环操作就能实现lock free方法。看下面的stack.push(…)伪代码。

1 Atomic<Node*> head;2 Node* new_node= new Node(….);3 New_node->next=head; (1)4 While(!head.compare_and_swap(new_node->next, new_node)); (2)

设有一个Stack,需要push一个值进去,head指针申明为原子量。(1)把新值的next设为head。(2)更新head,如果head==new_node->next那么,head=new_node,返回true,跳出循环;否则new_node->next更新为head,并返回false,继续循环。猜想?什么情况会new_node->next不等于head?如果在(1)执行完毕后head被其它线程修改。此时的old_head(new_node->next)就需要更新为当前最新的head,才能进行接下来的操作。这种方式实现的stack push就是用了lock free思想。是不是比加lock代码看起来简洁多了? 代码中一个lock操作都没有看到哦。

Lock free需要一个自旋锁,耗CPU时间,而lock方法是直接block住,释放占用的CPU时间。发现没?这个“自旋锁”思想在前文的try lock中也出现了?甚至可以这样猜想,如果把所有的锁都换成自旋锁,是不是就能防止死锁了?前面的stack.push操作也可以添加CAS试做次数,操作一定次数或时间限制,表示此次push失败,也就跳出了此自旋锁。另外,没有所谓的lock free就是比lock好,根据不同的业务情况,自行选择吧。

问:如果我的关键实体是一个对象,那么可以把对象最为一个原子量吗?这个,恐怕不能,不过可以分解对象中的一些基本类型字段作为原子量。到底使用哪些基本类型,需要对业务需求有深刻的理解。

9.    轮询操作

观察者设计模式这里不多说了,主要思想是把主动轮询,转变为被动通知。不过在某些并发程序集中“轮询”比“通知”思想还要普遍,或者说“通过轮询来通知”。甚至这样说,我感觉在某些场合轮询比通知更有用,特别是对顺序要求敏感的地方。这类程序基于规避乱序风险从而选择此架构方式,而且轮询的架构方式能人为制造“程序栅栏”。Thread barrier这个操作听说过吧,这是一种同步程序的方式。这种方式在某些分布式程序中用到(特别是消息队列模块),而且非常利于Debug。

一个完整的系统轮询和通知都很重要,没有谁好谁不好一说。写到这里想想,在上文轮询中,提到“顺序”两个字,你懂的,顺序执行容易引发性能瓶颈,及其连锁反应。而且轮询数据库什么的,数据多起来能直接把系统搞得不响应好吗,考虑下触发器嘛。所以架构就是一种舍一种得的心态,要根据具体业务需求来定夺。

“通知”的理解以后有机会再说。对了,轮询时记得yield, 别到时候轮询线程一跑起来把CPU时间片占光了,其它并发操作受到影响。

10.    线程操作

线程是执行一个函数,很有函数式编程理念的感觉。但是并不是线程开得越多程序处理就越快,这和实际处理器数和线程上下文切换频率就很大关系,这些情况baidu google一下就知道,不细说,基本上最好的线程数量就大概是空闲处理器的数量。

这里说的一个思想是线程池Thread pool。就是说线程创建后不会因为执行完毕而被销毁,它会放入一个池子中等待新任务唤醒它,然后开始执行。线程创建是要耗CPU资源的,重复利用当然是好的。这个功能在高级语言中有对应的实现,比如说C#的task,它的默认调度类就实现了此思想。所以说高级语言使用起来比较顺畅,你不用从头开始造轮子。C++的boost库中也有类似方法。编程的时候用上呗,何乐而不为?

如上图,2个线程做5个任务,而不是用5个线程。而且在task3之后,线程1就处于空转或阻塞的等待状态而不是销毁自身,直到task4出现又开始执行。但是,如果5个task并行执行,且确实有如此多空闲处理器,开5条线程,处理效率更高。线程池的方式是否适用此场景还需好好考虑下。

我们看得稍微抽象点,前文所说的线程池技术,就是任务调度操作,这个以后有机会在说吧。。。