进程通信和线程间通信 1.docx

上传人:b****6 文档编号:8247964 上传时间:2023-01-30 格式:DOCX 页数:12 大小:28.44KB
下载 相关 举报
进程通信和线程间通信 1.docx_第1页
第1页 / 共12页
进程通信和线程间通信 1.docx_第2页
第2页 / 共12页
进程通信和线程间通信 1.docx_第3页
第3页 / 共12页
进程通信和线程间通信 1.docx_第4页
第4页 / 共12页
进程通信和线程间通信 1.docx_第5页
第5页 / 共12页
点击查看更多>>
下载资源
资源描述

进程通信和线程间通信 1.docx

《进程通信和线程间通信 1.docx》由会员分享,可在线阅读,更多相关《进程通信和线程间通信 1.docx(12页珍藏版)》请在冰豆网上搜索。

进程通信和线程间通信 1.docx

进程通信和线程间通信1

进程通信和线程间通信1

进程通信和线程间通信

(1)2010-04-0915:

32进程间通信与线程间通信Linux下进程间通信(IPC)的方式数不胜数,光UNPv2列出的就有:

pipe、FIFO、POSIX消息队列、共享内存、信号(signals)等等,更不必说Sockets了。

同步原语(synchronizationprimitives)也很多,互斥器(mutex)、条件变量(conditionvariable)、读写锁(reader-writerlock)、文件锁(Recordlocking)、信号量(Semaphore)等等。

如何选择呢?

根据我的个人经验,贵精不贵多,认真挑选三四样东西就能完全满足我的工作需要,而且每样我都能用得很熟,,不容易犯错。

5进程间通信进程间通信我首选Sockets(主要指TCP,我没有用过UDP,也不考虑Unixdomain协议),其最大的好处在于:

可以跨主机,具有伸缩性。

反正都是多进程了,如果一台机器处理能力不够,很自然地就能用多台机器来处理。

把进程分散到同一局域网的多台机器上,程序改改host:

port配置就能继续用。

相反,前面列出的其他IPC都不能跨机器(比如共享内存效率最高,但再怎么着也不能高效地共享两台机器的内存),限制了scalability。

在编程上,TCPsockets和pipe都是一个文件描述符,用来收发字节流,都可以read/write/fcntl/select/poll等。

不同的是,TCP是双向的,pipe是单向的(Linux),进程间双向通讯还得开两个文件描述符,不方便;而且进程要有父子关系才能用pipe,这些都限制了pipe的使用。

在收发字节流这一通讯模型下,没有比sockets/TCP更自然的IPC了。

当然,pipe也有一个经典应用场景,那就是写Reactor/Selector时用来异步唤醒select(或等价的poll/epoll)调用(SunJVM在Linux就是这么做的)。

TCPport是由一个进程独占,且操作系统会自动回收(listeningport和已建立连接的TCPsocket都是文件描述符,在进程结束时操作系统会关闭所有文件描述符)。

这说明,即使程序意外退出,也不会给系统留下垃圾,程序重启之后能比较容易地恢复,而不需要重启操作系统(用跨进程的mutex就有这个风险)。

还有一个好处,既然port是独占的,那么可以防止程序重复启动(后面那个进程抢不到port,自然就没法工作了),造成意料之外的结果。

两个进程通过TCP通信,如果一个崩溃了,操作系统会关闭连接,这样另一个进程几乎立刻就能感知,可以快速failover。

当然,应用层的心跳也是必不可少的,我以后在讲服务端的日期与时间处理的时候还会谈到心跳协议的设计。

与其他IPC相比,TCP协议的一个自然好处是"可记录可重现",tcpdump/Wireshark是解决两个进程间协议/状态争端的好帮手。

另外,如果网络库带"连接重试"功能的话,我们可以不要求系统里的进程以特定的顺序启动,任何一个进程都能单独重启,这对开发牢靠的分布式系统意义重大。

使用TCP这种字节流(bytestream)方式通信,会有marshal/unmarshal的开销,这要求我们选用合适的消息格式,准确地说是wireformat。

这将是我下一篇blog的主题,目前我推荐GoogleProtocolBuffers。

有人或许会说,具体问题具体分析,如果两个进程在同一台机器,就用共享内存,否则就用TCP,比如MSSQLServer就同时支持这两种通信方式。

我问,是否值得为那么一点性能提升而让代码的复杂度大大增加呢?

TCP是字节流协议,只能顺序读取,有写缓冲;共享内存是消息协议,a进程填好一块内存让b进程来读,基本是"停等"方式。

要把这两种方式揉到一个程序里,需要建一个抽象层,封装两种IPC。

这会带来不透明性,并且增加测试的复杂度,而且万一通信的某一方崩溃,状态reconcile也会比sockets麻烦。

为我所不取。

再说了,你舍得让几万块买来的SQLServer和你的程序分享机器资源吗?

产品里的数据库服务器往往是独立的高配置服务器,一般不会同时运行其他占资源的程序。

TCP本身是个数据流协议,除了直接使用它来通信,还可以在此之上构建RPC/REST/SOAP之类的上层通信协议,这超过了本文的范围。

另外,除了点对点的通信之外,应用级的广播协议也是非常有用的,可以方便地构建可观可控的分布式系统。

本文不具体讲Reactor方式下的网络编程,其实这里边有很多值得注意的地方,比如带backoff的retryconnecting,用优先队列来组织timer等等,留作以后分析吧。

6线程间同步线程同步的四项原则,按重要性排列:

1.首要原则是尽量最低限度地共享对象,减少需要同步的场合。

一个对象能不暴露给别的线程就不要暴露;如果要暴露,优先考虑immutable对象;实在不行才暴露可修改的对象,并用同步措施来充分保护它。

2.其次是使用高级的并发编程构件,如TaskQueue、Producer-ConsumerQueue、CountDownLatch等等;

3.最后不得已必须使用底层同步原语(primitives)时,只用非递归的互斥器和条件变量,偶尔用一用读写锁;

4.不自己编写lock-free代码,不去凭空猜测"哪种做法性能会更好",比如spinlockvs.mutex。

前面两条很容易理解,这里着重讲一下第3条:

底层同步原语的使用。

互斥器(mutex)互斥器(mutex)恐怕是使用得最多的同步原语,粗略地说,它保护了临界区,一个时刻最多只能有一个线程在临界区内活动。

(请注意,我谈的是pthreads里的mutex,不是Windows里的重量级跨进程Mutex。

)单独使用mutex时,我们主要为了保护共享数据。

我个人的原则是:

l用RAII手法封装mutex的创建、销毁、加锁、解锁这四个操作。

l只用非递归的mutex(即不可重入的mutex)。

l不手工调用lock()和unlock()函数,一切交给栈上的Guard对象的构造和析构函数负责,Guard对象的生命期正好等于临界区(分析对象在什么时候析构是C++程序员的基本功)。

这样我们保证在同一个函数里加锁和解锁,避免在foo()里加锁,然后跑到bar()里解锁。

l在每次构造Guard对象的时候,思考一路上(调用栈上)已经持有的锁,防止因加锁顺序不同而导致死锁(deadlock)。

由于Guard对象是栈上对象,看函数调用栈就能分析用锁的情况,非常便利。

次要原则有:

l不使用跨进程的mutex,进程间通信只用TCPsockets。

l加锁解锁在同一个线程,线程a不能去unlock线程b已经锁住的mutex。

(RAII自动保证)

l别忘了解锁。

(RAII自动保证)

l不重复解锁。

(RAII自动保证)

l必要的时候可以考虑用PTHREAD_MUTEX_ERRORCHECK来排错

用RAII封装这几个操作是通行的做法,这几乎是C++的标准实践,后面我会给出具体的代码示例,相信大家都已经写过或用过类似的代码了。

Java里的synchronized语句和C#的using语句也有类似的效果,即保证锁的生效期间等于一个作用域,不会因异常而忘记解锁。

Mutex恐怕是最简单的同步原语,安照上面的几条原则,几乎不可能用错。

我自己从来没有违背过这些原则,编码时出现问题都很快能招到并修复。

跑题:

非递归的mutex谈谈我坚持使用非递归的互斥器的个人想法。

Mutex分为递归(recursive)和非递归(non-recursive)两种,这是POSIX的叫法,另外的名字是可重入(Reentrant)与非可重入。

这两种mutex作为线程间(inter-thread)的同步工具时没有区别,它们的惟一区别在于:

同一个线程可以重复对recursivemutex加锁,但是不能重复对non-recursivemutex加锁。

首选非递归mutex,绝对不是为了性能,而是为了体现设计意图。

non-recursive和recursive的性能差别其实不大,因为少用一个计数器,前者略快一点点而已。

在同一个线程里多次对non-recursivemutex加锁会立刻导致死锁,我认为这是它的优点,能帮助我们思考代码对锁的期求,并且及早(在编码阶段)发现问题。

毫无疑问recursivemutex使用起来要方便一些,因为不用考虑一个线程会自己把自己给锁死了,我猜这也是Java和Windows默认提供recursivemutex的原因。

(Java语言自带的intrinsiclock是可重入的,它的concurrent库里提供ReentrantLock,Windows的CRITICAL_SECTION也是可重入的。

似乎它们都不提供轻量级的non-recursivemutex。

正因为它方便,recursivemutex可能会隐藏代码里的一些问题。

典型情况是你以为拿到一个锁就能修改对象了,没想到外层代码已经拿到了锁,正在修改(或读取)同一个对象呢。

具体的例子:

std:

vectorfoos;

MutexLockmutex;

voidpost(constFoo&f)

MutexLockGuardlock(mutex);

foos.push_back(f);

voidtraverse()

MutexLockGuardlock(mutex);

for(autoit=foos.begin();it!

=foos.end();++it){//用了0x新写法

it-doit();

post()加锁,然后修改foos对象;traverse()加锁,然后遍历foos数组。

将来有一天,Foo:

doit()间接调用了post()(这在逻辑上是错误的),那么会很有戏剧性的:

1.Mutex是非递归的,于是死锁了。

2.Mutex是递归的,由于push_back可能(但不总是)导致vector迭代器失效,程序偶尔会crash。

这时候就能体现non-recursive的优越性:

把程序的逻辑错误暴露出来。

死锁比较容易debug,把各个线程的调用栈打出来((gdb)threadapplyallbt),只要每个函数不是特别长,很容易看出来是怎么死的。

(另一方面支持了函数不要写过长。

)或者可以用PTHREAD_MUTEX_ERRORCHECK一下子就能找到错误(前提是MutexLock带debug选项。

程序反正要死,不如死得有意义一点,让验尸官的日子好过些。

如果一个函数既可能在已加锁的情况下调用,又可能在未加锁的情况下调用,那么就拆成两个函数:

1.跟原来的函数同名,函数加锁,转而调用第2个函数。

2.给函数名加上后缀WithLockHold,不加锁,把原来的函数体搬过来。

就像这样:

voidpost(constFoo&f)

MutexLockGuardlock(mutex);

postWithLockHold(f);//不用担心开销,编译器会自动内联的

//引入这个函数是为了体现代码作者的意图,尽管push_back通常可以手动内联

voidpostWithLockHold(constFoo&f)

foos.push_back(f);

这有可能出现两个问题(感谢水木网友ilovecpp提出):

a)误用了加锁版本,死锁了。

b)误用了不加锁版本,数据损坏了。

对于a),仿造前面的办法能比较容易地排错。

对于b),如果pthreads提供isLocked()就好办,可以写成:

voidpostWithLockHold(constFoo&f)

assert(mutex.isLocked());//目前只是一个愿望

//.

另外,WithLockHold这个显眼的后缀也让程序中的误用容易暴露出来。

C++没有annotation,不能像Java那样给method或field标上@GuardedBy注解,需要程序员自己小心在意。

虽然这里的办法不能一劳永逸地解决全部多线程错误,但能帮上一点是一点了。

我还没有遇到过需要使用recursivemutex的情况,我想将来遇到了都可以借助wrapper改用non-recursivemutex,代码只会更清晰。

===回到正题===

本文这里只谈了mutex本身的正确使用,在C++里多线程编程还会遇到其他很多racecondition,请参考拙作《当析构函数遇到多线程--C++中线程安全的对象回调》。

请注意这里的class命名与那篇文章有所不同。

我现在认为MutexLock和MutexLockGuard是更好的名称。

性能注脚:

Linux的pthreadsmutex采用futex实现,不必每次加锁解锁都陷入系统调用,效率不错。

Windows的CRITICAL_SECTION也是类似。

条件变量条件变量(conditionvariable)顾名思义是一个或多个线程等待某个布尔表达式为真,即等待别的线程"唤醒"它。

条件变量的学名叫管程(monitor)。

JavaObject内置的wait(),notify(),notifyAll()即是条件变量(它们以容易用错著称)。

条件变量只有一种正确使用的方式,对于wait()端:

1.必须与mutex一起使用,该布尔表达式的读写需受此mutex保护

2.在mutex已上锁的时候才能调用wait()

3.把判断布尔条件和wait()放到while循环中

写成代码是:

MutexLockmutex;

Conditioncond(mutex);

std:

dequequeue;

intdequeue()

MutexLockGuardlock(mutex);

while(queue.empty()){//必须用循环;必须在判断之后再wait()

cond.wait();//这一步会原子地unlockmutex并进入blocking,不会与enqueue死锁

assert(!

queue.empty());

inttop=queue.front();

queue.pop_front();

returntop;

对于signal/broadcast端:

1.不一定要在mutex已上锁的情况下调用signal(理论上)

2.在signal之前一般要修改布尔表达式

3.修改布尔表达式通常要用mutex保护(至少用作fullmemorybarrier)

写成代码是:

voidenqueue(intx)

MutexLockGuardlock(mutex);

queue.push_back(x);

cond.notify();

上面的dequeue/enqueue实际上实现了一个简单的unboundedBlockingQueue。

条件变量是非常底层的同步原语,很少直接使用,一般都是用它来实现高层的同步措施,如BlockingQueue或CountDownLatch。

读写锁与其他读写锁(Reader-Writerlock),读写锁是个优秀的抽象,它明确区分了read和write两种行为。

需要注意的是,readerlock是可重入的,writerlock是不可重入(包括不可提升readerlock)的。

这正是我说它"优秀"的主要原因。

遇到并发读写,如果条件合适,我会用《借shared_ptr实现线程安全的copy-on-write》介绍的办法,而不用读写锁。

当然这不是绝对的。

信号量(Semaphore),我没有遇到过需要使用信号量的情况,无从谈及个人经验。

说一句大逆不道的话,如果程序里需要解决如"哲学家就餐"之类的复杂IPC问题,我认为应该首先考察几个设计,为什么线程之间会有如此复杂的资源争抢(一个线程要同时抢到两个资源,一个资源可以被两个线程争夺)?

能不能把"想吃饭"这个事情专门交给一个为各位哲学家分派餐具的线程来做,然后每个哲学家等在一个简单的conditionvariable上,到时间了有人通知他去吃饭?

从哲学上说,教科书上的解决方案是平权,每个哲学家有自己的线程,自己去拿筷子;我宁愿用集权的方式,用一个线程专门管餐具的分配,让其他哲学家线程拿个号等在食堂门口好了。

这样不损失多少效率,却让程序简单很多。

虽然Windows的WaitForMultipleObjects让这个问题trivial化,在Linux下正确模拟WaitForMultipleObjects不是普通程序员该干的。

封装MutexLock、MutexLockGuard和Condition本节把前面用到的MutexLock、MutexLockGuard、Conditionclasses的代码列出来,前面两个classes没多大难度,后面那个有点意思。

MutexLock封装临界区(Criticalsecion),这是一个简单的资源类,用RAII手法[CCS:

13]封装互斥器的创建与销毁。

临界区在Windows上是CRITICAL_SECTION,是可重入的;在Linux下是pthread_mutex_t,默认是不可重入的。

MutexLock一般是别的class的数据成员。

MutexLockGuard封装临界区的进入和退出,即加锁和解锁。

MutexLockGuard一般是个栈上对象,它的作用域刚好等于临界区域。

这两个classes应该能在纸上默写出来,没有太多需要解释的:

#include

#includeclassMutexLock:

boost:

noncopyablepublic:

MutexLock()//为了节省版面,单行函数都没有正确缩进

{pthread_mutex_init(&mutex_,NULL);}

~MutexLock()

{pthread_mutex_destroy(&mutex_);}

voidlock()//程序一般不主动调用

{pthread_mutex_lock(&mutex_);}

voidunlock()//程序一般不主动调用

{pthread_mutex_unlock(&mutex_);}

pthread_mutex_t*getPthreadMutex()//仅供Condition调用,严禁自己调用

{return&mutex_;}

private:

pthread_mutex_tmutex_;

};

classMutexLockGuard:

boost:

noncopyablepublic:

explicitMutexLockGuard(MutexLock&mutex):

mutex_(mutex)

{mutex_.lock();}

~MutexLockGuard()

{mutex_.unlock();}

private:

MutexLock&mutex_;

};

#defineMutexLockGuard(x)static_assert(false,"missingmutexguardvarname")

注意代码的最后一行定义了一个宏,这个宏的作用是防止程序里出现如下错误:

voiddoit()

MutexLockGuard(mutex);//没有变量名,产生一个临时对象又马上销毁了,没有锁住临界区

//正确写法是MutexLockGuardlock(mutex);

//临界区

这里MutexLock没有提供trylock()函数,因为我没有用过它,我想不出什么时候程序需要"试着去锁一锁",或许我写过的代码太简单了。

我见过有人把MutexLockGuard写成template,我没有这么做是因为它的模板类型参数只有MutexLock一种可能,没有必要随意增加灵活性,于是我人肉把模板具现化(instantiate)了。

此外一种更激进的写法是,把lock/unlock放到private区,然后把Guard设为MutexLock的friend,我认为在注释里告知程序员即可,另外check-in之前的codereview也很容易发现误用的情况(grepgetPthreadMutex)。

这段代码没有达到工业强度:

a)Mutex创建为PTHREAD_MUTEX_DEFAULT类型,而不是我们预想的PTHREAD_MUTEX_NORMAL类型(实际上这二者很可能是等同的),严格的做法是用mutexattr来显示指定mutex的类型。

b)没有检查返回值。

这里不能用assert检查返回值,因为assert在releasebuild里是空语句。

我们检查返回值的意义在于防止ENOMEM之类的资源不足情况,这一般只可能在负载很重的产品程序中出现。

一旦出现这种错误,程序必须立刻清理现场并主动退出,否则会莫名其妙地崩溃,给事后调查造成困难。

这里我们需要non-debug的assert,或许google-glog的CHECK()是个不错的思路。

以上两点改进留作练习。

Conditionclass的实现有点意思。

Pthreadsconditionvariable允许在wait()的时候指定mutex,但是我想不出什么理由一个conditionvariable会和不同的mutex配合使用。

Java的intrinsiccondition和Conditonclass都不支持这么做,因此我觉得可以放弃这一灵活性,老老实实一对一好了。

相反boost:

thread的condition_varianle是在wait的时候指定mutex,请参观其同步原语的庞杂设计:

lConcept有四种Lockable,TimedLockable,SharedLockable,UpgradeLockable.

lLock有五六种:

lock_guard,unique_lock,shared_lock,upgrade_lock,upgrade_to_unique_lock,scoped_try_lock.

lMutex有七种:

mutex,try_mutex,timed_mutex,recursive_mutex,recursive_try_mutex,recursive_timed_mutex,shared_mutex.

恕我愚钝,见到boost:

thread这样如RubeGoldbergMachine一样"灵活"的库我只得三揖绕道而行。

这些class名字也很无厘头,为什么不老老实实用reader_writer_lock这样的通俗名字呢?

非得增加精神负担,自己发明新名字。

我不愿为这样的灵活性付出代价,宁愿自己做几个简简单单的一看就明白的classes来用,这种简单的几行代码的轮子造造也无妨。

提供灵活性固然是本事,然而在不需要灵活性的地方把代码写死,更需要大智慧。

下面这个Condition简单地封装了pthreadcondvar,用起来也容易,见本节前面的例子。

这里我用notify/notifyAll作为函数名,因为signal有别的含义,C++里的signal/slot,C里的signalhandler等等。

就别overload这个术语了。

classCondition:

boost:

noncopyablepublic:

Condition(MutexLock&mutex):

mutex_(mutex)

{pthread_cond_init(&pcond_,NULL);}

~Condition()

{pthread_cond_destroy(&pcond_);}

voidwait()

{pthread_cond_w

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 小学教育 > 语文

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1