VC下使用何种IPC可以实现多进程是怎样实现的之间的广播

安全检查中...
请打开浏览器的javascript,然后刷新浏览器
< 浏览器安全检查中...
还剩 5 秒&22398人阅读
C/C++(47)
1 Windows进程间通信的各种方法
进程是装入内存并准备执行的程序,每个进程都有私有的虚拟地址空间,由代码、数据以及它可利用的系统资源(如文件、管道等)组成。
多进程/多线程是Windows操作系统的一个基本特征。Microsoft Win32应用编程接口(Application Programming Interface, API)
提供了大量支持应用程序间数据共享和交换的机制,这些机制行使的活动称为进程间通信(InterProcess Communication, IPC),
进程通信就是指不同进程间进行数据共享和数据交换。
  正因为使用Win32 API进行进程通信方式有多种,如何选择恰当的通信方式就成为应用开发中的一个重要问题,
下面本文将对Win32中进程通信的几种方法加以分析和比较。
2 进程通信方法
2.1 文件映射
文件映射(Memory-Mapped Files)能使进程把文件内容当作进程地址区间一块内存那样来对待。因此,进程不必使用文件I/O操作,
只需简单的指针操作就可读取和修改文件的内容。
Win32 API允许多个进程访问同一文件映射对象,各个进程在它自己的地址空间里接收内存的指针。通过使用这些指针,不同进程就可以读或修改文件的内容,
实现了对文件中数据的共享。
应用程序有三种方法来使多个进程共享一个文件映射对象。
(1)继承:第一个进程建立文件映射对象,它的子进程继承该对象的句柄。
(2)命名文件映射:第一个进程在建立文件映射对象时可以给该对象指定一个名字(可与文件名不同)。第二个进程可通过这个名字打开此文件映射对象。
另外,第一个进程也可以通过一些其它IPC机制(有名管道、邮件槽等)把名字传给第二个进程。
(3)句柄复制:第一个进程建立文件映射对象,然后通过其它IPC机制(有名管道、邮件槽等)把对象句柄传递给第二个进程。
第二个进程复制该句柄就取得对该文件映射对象的访问权限。
文件映射是在多个进程间共享数据的非常有效方法,有较好的安全性。但文件映射只能用于本地机器的进程之间,不能用于网络中
,而开发者还必须控制进程间的同步。
2.2 共享内存
Win32 API中共享内存(Shared Memory)实际就是文件映射的一种特殊情况。进程在创建文件映射对象时用0xFFFFFFFF来代替 文件句柄(HANDLE),
就表示了对应的文件映射对象是从操作系统页面文件访问内存,其它进程打开该文件映射对象就可以访问该内存块。由于共享内存是用 文件映射实现的,
所以它也有较好的安全性,也只能运行于同一计算机上的进程之间。
a.设定一块共享内存区域
HANDLE CreateFileMapping(HANDLE,LPSECURITY_ATTRIBUTES, DWORD, DWORD, DWORD, LPCSTR)// 产生一个file-mapping核心对象
LPVOID MapViewOfFile(
HANDLE hFileMappingObject,
DWORD dwDesiredAcess,
DWORD dwFileOffsetHigh,
DWORD dwFileOffsetLow,
DWORD dwNumberOfBytesToMap
);得到共享内存的指针
b.找出共享内存
决定这块内存要以点对点(peer to peer)的形式呈现每个进程都必须有相同的能力,产生共享内存并将它初始化。每个进程都应该调用CreateFileMapping(),
然后调用GetLastError().如果传回的错误代码是 ERROR_ALREADY_EXISTS,那么进程就可以假设这一共享内存区 域已经被别的进程打开并初始化了,
否则该进程就可以合理的认为自己 排在第 一位,并接下来将共享内存初始化。还是要使用client/server架构中只有server进程才应该产生并初始化共享内存。
所有的进程都应该使用
HANDLE OpenFileMapping(DWORD dwDesiredAccess,
BOOL bInheritHandle,
LPCTSTR lpName);
再调用MapViewOfFile(),取得共享内存的指针
c.同步处理(Mutex)
d.清理(Cleaning up) BOOL UnmapViewOfFile(LPCVOID lpBaseAddress);
CloseHandle()
2.3 匿名管道
管道(Pipe)是一种具有两个端点的通信通道:有一端句柄的进程可以和有另一端句柄的进程通信。管道可以是单向-一端是只读的,另一端点是只写的;
也可以是双向的一管道的两端点既可读也可写。
匿名管道(Anonymous Pipe)是 在父进程和子进程之间,或同一父进程的两个子进程之间传输数据的无名字的单向管道。通常由父进程创建管 道,
然后由要通信的子进程继承通道的读端点句柄或写 端点句柄,然后实现通信。父进程还可以建立两个或更多个继承匿名管道读和写句柄的子进程。
这些子进程 可以使用管道直接通信,不需要通过父进程。
匿名管道是单机上实现子进程标准I/O重定向的有效方法,它不能在网上使用,也不能用于两个不相关的进程之间。
2.4 命名管道
命名管道(Named Pipe)是服务器进程和一个或多个客户进程之间通信的单向或双向管道。不同于匿名管道的是命名管道可以在不相关的进程之间和不 同计算机之间使用,
服务器建立命名管道时给它指定一个名字,任何进程都可以通过该名字打开管道的另一端,根据给定的权限和服务器进程通信。
命名管道提供了相对简单的编程接口,使通过网络传输数据并不比同一计算机上两进程之间通信更困难,不过如果要同时和多个进程通信它就力不从心了。
2.5 邮件槽
邮件槽(Mailslots)提 供进程间单向通信能力,任何进程都能建立邮件槽成为邮件槽服务器。其它进程,称为邮件槽客户,可以通过邮件槽的名字给
邮件槽服务器进程发送消息。进来的消 息一直放在邮件槽中,直到服务器进程读取它为止。一个进程既可以是邮件槽服务器也可以是邮件槽客户,
因此可建立多个 邮件槽实现进程间的双向通信。
通过邮件槽可以给本地计算机上的邮件槽、其它计算机上的邮件槽或指定网络区域中所有计算机上有同样名字的邮件槽发送消息。
广播通信的消息长度不能超过400字节,非广播消息的长度则受邮件槽服务器指定的最大消息长度的限制。
邮件槽与命名管道相&#20284;,不过它传输数据是通过不可靠的数据报(如TCP/IP协议中的UDP包)完成的,一旦网络发生错误则无法保证消息正确地接收,
而 命名管道传输数据则是建立在可靠连接基础上的。不过邮件槽有简化的编程接口和给指定网络区域内的所有计算机广播消息的能力,
所以邮件槽不失为应用程序发送 和接收消息的另一种选择。
2.6 剪贴板
   剪贴板(Clipped Board)实质是Win32 API中一组用来传输数据的函数和消息,为Windows应用程序之间进行数据共享提供了一个 中介,
Windows已建立的剪切(复制)-粘贴的机制为不同应用程序之间共享不同&#26684;式数据提供了一条捷径。当用户在应用程序中执行剪切或复制操作时,
应 用程序把选取的数据用一种或多种&#26684;式放在剪贴板上。然后任何其它应用程序都可以从剪贴板上拾取数据,从给定&#26684;式中选择适合自己的&#26684;式。
剪贴板 是一个非常松散的交换媒介,可以支持任何数据&#26684;式,每一&#26684;式由一无符号整数标识,对标准(预定义)剪贴板&#26684;式,该&#20540;是Win32 API定义的常量;
对非 标准&#26684;式可以使用Register Clipboard Format函数注册为新的剪贴板&#26684;式。利用剪贴板进行交换的数据只需在数据&#26684;式上一致或都可以
转化为某种&#26684;式就行。但剪贴板只能在基于Windows的程序中使用,不能在网络上使用。
2.7 动态数据交换
动态数据交换(DDE)是使用共享内存在应用程序之间进行数据交换的一种进程间通信形式。应用程序可以使用DDE进行一次性数据传输,也可以当出现新数据时,
通过发送更新&#20540;在应用程序间动态交换数据。
DDE和剪贴板一样既支持标准数据&#26684;式(如文本、位图等),又可以支持自己定义的数据&#26684;式。但它们的数据传输机制却不同,一个明显区别是剪贴板操作几乎
总是用作对用户指定操作的一次性应答-如从菜单中选择Paste命令。尽管DDE也可以由用户启动,但它继续发挥作用一般不必用户进一步干预。DDE有三 种数据交换方式:
(1) 冷链:数据交换是一次性数据传输,与剪贴板相同。
(2) 温链:当数据交换时服务器通知客户,然后客户必须请求新的数据。
(3) 热链:当数据交换时服务器自动给客户发送数据。
DDE交换可以发生在单机或网络中不同计算机的应用程序之间。开发者还可以定义定制的DDE数据&#26684;式进行应用程序之间特别目的IPC,它们有更紧密耦合的通信要求。
大多数基于Windows的应用程序都支持DDE。
2.8 对象连接与嵌入
应用程序利用对象连接与嵌入(OLE)技术管理复合文档(由多种数据&#26684;式组成的文档),OLE提供使某应用程序更容易调用其它应用程序进行数据编辑的服 务。
例如,OLE支持的字处理器可以嵌套电子表&#26684;,当用户要编辑电子表&#26684;时OLE库可自动启动电子表&#26684;编辑器。当用户退出电子表&#26684;编辑器时,
该表&#26684;已在原 始字处理器文档中得到更新。在这里电子表&#26684;编辑器变成了字处理器的扩展,而如果使用DDE,用户要显式地启动电子表&#26684;编辑器。
同DDE技术相同,大多数基于Windows的应用程序都支持OLE技术。
2.9 动态连接库
Win32动态连接库(DLL)中的全局数据可以被调用DLL的所有进程共享,这就又给进程间通信开辟了一条新的途径,当然访问时要注意同步问题。
虽然可以通过DLL进行进程间数据共享,但从数据安全的角度考虑,我们并不提倡这种方法,使用带有访问权限控制的共享内存的方法更好一些。
2.10 远程过程调用
Win32 API提供的远程过程调用(RPC)使应用程序可以使用远程调用函数,这使在网络上用RPC进行进程通信就像函数调用那样简单。
RPC既可以在单机不同进程间使用也可以在网络中使用。
由于Win32 API提供的RPC服从OSF-DCE (Open Software Foundation Distributed Computing Environment)标准。
所以通过 Win32 API编写的RPC应用程序能与其它操作系统上支持DEC的RPC应用程序通信。使用RPC开发者可以建立高性能、紧密耦合的分布式应用程 序。
2.11 NetBios函数
Win32 API提供NetBios函数用于处理低级网络控制,这主要是为IBM NetBios系统编写与Windows的接口。除非那些有特殊低级网络功能要求的应用程序,
其它应用程序最好不要使用NetBios函数来进行进程间通信。
2.12 Sockets
Windows Sockets规范是以U.C.Berkeley大学BSD UNIX中流行的Socket接口为范例定义的一套Windows下的网 络编程接口。除了Berkeley Socket原有的库函数以外
,还扩展了一组针对Windows的函数,使程序员可以充分利用Windows的消息机 制进行编程。
现在通过Sockets实现进程通信的网络应用越来越多,这主要的原因是Sockets的跨平台性要比其它IPC机制好得多,另 外WinSock 2.0不仅支持TCP/IP协议,
而且还支持其它协议(如IPX)。Sockets的唯一缺点是它支持的是底层通信操作,这使得在单机 的进程间进行简单数据传递不太方便,
这时使用下面将介绍的WM_COPYDATA消息将更合适些。
2.13 WM_COPYDATA消息
WM_COPYDATA是一种非常强大却鲜为人知的消息。当一个应用向另一个应用传送数据时,发送方只需使用调用SendMessage函数,
参数是目 的窗口的句柄、传递数据的起始地址、WM_COPYDATA消息。接收方只需像处理其它消息那样处理WM_COPY DATA消息,这样收发双方就实现了 数据共享。
WM_COPYDATA是一种非常简单的方法,它在底层实际上是通过文件映射来实现的。
它的缺点是灵活性不高,并且它只能用于Windows平台的单机环境下。
  Win32 API为应用程序实现进程间通信提供了如此多种选择方案,
那么开发者如何进行选择呢?通常在决定使用哪种IPC方法之前应考虑以下一些问题:
(1)应用程序是在网络环境下还是在单机环境下工作。
方法一:WM_COPYDATA
HWND hReceiveDataWindow = FindWindow(NULL,....)
COPYDATASTRUCT
data.cbdata = strlen(pStr);
data.lpData = pS
SendMessage(hReceiveDataWindow ,WM_COPYDATA,(WPARAM)GetFocus(),(LPARAM)&data);
REF.最简单的方式
/TechLab/archive//2272.aspx
方法二:dll共享
#pragma data_seg (&.ASHARE&)
int iWhatYouUseInTwo = 0;
#pragma data_seg()
方法三:映象文件
REF.最基础,效率最高的方法
最好的参考书《Windows核心编程》第17章 内存映射文件
/2005/10/interprocess_communications.html
方法四:匿名管道:CreatePipe
方法五:命名管道:createnamedpipe
/bbshtml/bbs8/pediy8-724.htm
方法六:邮件通道
方法七:网络接口,socket,但要求有网卡。可以实现不同主机间的IPC
另一篇总结的比较好的文章
/doc/Html/Visual%20C&#43;&#43;/.html
进程通常被定义为一个正在运行的程序的实例,它由两个部分组成:
一个是操作系统用来管理进程的内核对象。内核对象也是系统用来存放关于进程的统计信息的地方
另一个是地址空间,它包含所有的可执行模块或DLL模块的代码和数据。它还包含动态分配的空间。
如线程堆栈和堆分配空间。每个进程被赋予它自己的虚拟地址空间,当进程中的一个线程正在运行时,该线程可以访问只属于它的进程的内存。
属于其它进程的内存则是隐藏的,并不能被正在运行的线程访问。
为了能在两个进程之间进行通讯,由以下几种方法可供参考:
0。剪贴板Clipboard: 在16位时代常使用的方式,CWnd中提供支持
1。窗口消息 标准的Windows消息以及专用的WM_COPYDATA消息 SENDMESSAGE()接收端必须有一个窗口
2。使用共享内存方式(Shared Memory)
a.设定一块共享内存区域
HANDLE CreateFileMapping(HANDLE,LPSECURITY_ATTRIBUTES, DWORD, DWORD, DWORD, LPCSTR)
产生一个file-mapping核心对象
LPVOID MapViewOfFile(
HANDLE hFileMappingObject,
DWORD dwDesiredAcess,
DWORD dwFileOffsetHigh,
DWORD dwFileOffsetLow,
DWORD dwNumberOfBytesToMap
得到共享内存的指针
b.找出共享内存
决定这块内存要以点对点(peer to peer)的形式呈现
每个进程都必须有相同的能力,产生共享内存并将它初始化。每个进程
都应该调用CreateFileMapping(),然后调用GetLastError().如果传回的
错误代码是ERROR_ALREADY_EXISTS,那么进程就可以假设这一共享内存区 域已经被别的进程打开并初始化了,否则该进程就可以合理的认为自己 排在第 一位,并接下来将共享内存初始化。
还是要使用client/server架构中
只有server进程才应该产生并初始化共享内存。所有的进程都应该使用
HANDLE OpenFileMapping(DWORD dwDesiredAccess,
BOOL bInheritHandle,
LPCTSTR lpName);
再调用MapViewOfFile(),取得共享内存的指针
c.同步处理(Mutex)
d.清理(Cleaning up) BOOL UnmapViewOfFile(LPCVOID lpBaseAddress);
CloseHandle()
3。动态数据交换(DDE)通过维护全局分配内存使的应用程序间传递成为可能
其方式是再一块全局内存中手工放置大量的数据,然后使用窗口消息传递内存 指针.这是16位WIN时代使用的方式,因为在WIN32下已经没有全局和局部内存
了,现在的内存只有一种就是虚存。
4。消息管道(Message Pipe)
用于设置应用程序间的一条永久通讯通道,通过该通道可以象自己的应用程序
访问一个平面文件一样读写数据。
匿名管道(Anonymous Pipes)
单向流动,并且只能够在同一电脑上的各个进程之间流动。
命名管道(Named Pipes)
双向,跨网络,任何进程都可以轻易的抓住,放进管道的数据有固定的&#26684; 式,而使用ReadFile()只能读取该大小的倍数。
可以被使用于I/O Completion Ports
5 邮件槽(Mailslots)
广播式通信,在32系统中提供的新方法,可以在不同主机间交换数据,在 WIN9X下只支持邮件槽客户
6。Windows套接字(Windows Socket)
它具备消息管道所有的功能,但遵守一套通信标准使的不同操作系统之上的应 用程序之间可以互相通信。
7。Internet通信 它让应用程序从Internet地址上载或下载文件
8。RPC:远程过程调用,很少使用,因其与UNIX的RPC不兼容。
9。串行/并行通信(Serial/Parallel Communication)
它允许应用程序通过串行或并行端口与其他的应用程序通信
10。COM/DCOM
通过COM系统的代理存根方式进行进程间数据交换,但只能够表现在对接口
函数的调用时传送数据,通过DCOM可以在不同主机间传送数据。
&&相关文章推荐
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:574026次
积分:4443
积分:4443
排名:第7089名
原创:34篇
转载:102篇
评论:68条
阅读:96712
(2)(1)(1)(2)(1)(2)(1)(1)(2)(3)(3)(10)(1)(1)(3)(2)(16)(1)(3)(2)(19)(23)(8)(9)(1)(1)(4)(1)(1)(5)(7)
(window.slotbydup = window.slotbydup || []).push({
id: '4740881',
container: s,
size: '200,200',
display: 'inlay-fix'多线程间通信和多进程之间通信有什么不同,分别怎么实现?【芯学苑吧】_百度贴吧
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&签到排名:今日本吧第个签到,本吧因你更精彩,明天继续来努力!
本吧签到人数:0成为超级会员,使用一键签到本月漏签0次!成为超级会员,赠送8张补签卡连续签到:天&&累计签到:天超级会员单次开通12个月以上,赠送连续签到卡3张
关注:21贴子:
多线程间通信和多进程之间通信有什么不同,分别怎么实现?收藏
进程间的通信:bind机制(IPC-&AIDL),linux级共享内存,boradcast,Activity 之间,activity & serview之间的通信,无论他们是否在一个进程内。
登录百度帐号推荐应用这么解释问题吧:&br&&br&1。单进程单线程:一个人在一个桌子上吃菜。&br&2。单进程多线程:多个人在同一个桌子上一起吃菜。&br&3。多进程单线程:多个人每个人在自己的桌子上吃菜。&br&&br&多线程的问题是多个人同时吃一道菜的时候容易发生争抢,例如两个人同时夹一个菜,一个人刚伸出筷子,结果伸到的时候已经被夹走菜了。。。此时就必须等一个人夹一口之后,在还给另外一个人夹菜,也就是说资源共享就会发生冲突争抢。&br&&br&&br&1。对于 Windows 系统来说,【开桌子】的开销很大,因此 Windows 鼓励大家在一个桌子上吃菜。因此 Windows 多线程学习重点是要大量面对资源争抢与同步方面的问题。&br&&br&&br&2。对于 Linux 系统来说,【开桌子】的开销很小,因此 Linux 鼓励大家尽量每个人都开自己的桌子吃菜。这带来新的问题是:坐在两张不同的桌子上,说话不方便。因此,Linux 下的学习重点大家要学习进程间通讯的方法。&br&&br&--&br&补充:有人对这个开桌子的开销很有兴趣。我把这个问题推广说开一下。&br&&br&开桌子的意思是指创建进程。开销这里主要指的是时间开销。&br&可以做个实验:创建一个进程,在进程中往内存写若干数据,然后读出该数据,然后退出。此过程重复 1000 次,相当于创建/销毁进程 1000 次。在我机器上的测试结果是:
&br&UbuntuLinux:耗时 0.8 秒
&br&Windows7:耗时 79.8 秒
&br&两者开销大约相差一百倍。&br&&br&这意味着,在 Windows 中,进程创建的开销不容忽视。换句话说就是,Windows 编程中不建议你创建进程,如果你的程序架构需要大量创建进程,那么最好是切换到 Linux 系统。&br&&br&大量创建进程的典型例子有两个,一个是 gnu autotools 工具链,用于编译很多开源代码的,他们在 Windows 下编译速度会很慢,因此软件开发人员最好是避免使用 Windows。另一个是服务器,某些服务器框架依靠大量创建进程来干活,甚至是对每个用户请求就创建一个进程,这些服务器在 Windows 下运行的效率就会很差。这&可能&也是放眼全世界范围,Linux
服务器远远多于 Windows 服务器的原因。&br&&br&--&br&再次补充:如果你是写服务器端应用的,其实在现在的网络服务模型下,开桌子的开销是可以忽略不计的,因为现在一般流行的是按照 CPU 核心数量开进程或者线程,开完之后在数量上一直保持,进程与线程内部使用协程或者异步通信来处理多个并发连接,因而开进程与开线程的开销可以忽略了。&br&&br&另外一种新的开销被提上日程:核心切换开销。&br&&br&现代的体系,一般 CPU 会有多个核心,而多个核心可以同时运行多个不同的线程或者进程。&br&&br&当每个 CPU 核心运行一个进程的时候,由于每个进程的资源都独立,所以 CPU 核心之间切换的时候无需考虑上下文。&br&&br&当每个 CPU 核心运行一个线程的时候,由于每个线程需要共享资源,所以这些资源必须从 CPU 的一个核心被复制到另外一个核心,才能继续运算,这占用了额外的开销。换句话说,在 CPU 为多核的情况下,多线程在性能上不如多进程。&br&&br&因而,当前面向多核的服务器端编程中,需要习惯多进程而非多线程。
这么解释问题吧: 1。单进程单线程:一个人在一个桌子上吃菜。 2。单进程多线程:多个人在同一个桌子上一起吃菜。 3。多进程单线程:多个人每个人在自己的桌子上吃菜。 多线程的问题是多个人同时吃一道菜的时候容易发生争抢,例如两个人同时夹一个菜,一个…
更新:由于此文陆陆续续收到赞同,而且其中有些地方并不完全正确,特在本文最后予以订正&br&&br&我不了解楼主的层次,我必须从很多基础的概念开始构建这个答案,并且可能引申到很多别的问题。&br&&br&首先我们来定义流的概念,一个流可以是文件,socket,pipe等等可以进行I/O操作的内核对象。&br&不管是文件,还是套接字,还是管道,我们都可以把他们看作流。&br&之后我们来讨论I/O的操作,通过read,我们可以从流中读入数据;通过write,我们可以往流写入数据。现在假定一个情形,我们需要从流中读数据,&b&但是流中还没有数据&/b&,(典型的例子为,客户端要从socket读如数据,但是服务器还没有把数据传回来),这时候该怎么办?&br&&ul&&li&阻塞。阻塞是个什么概念呢?比如某个时候你在等快递,但是你不知道快递什么时候过来,而且你没有别的事可以干(或者说接下来的事要等快递来了才能做);那么你可以去睡觉了,因为你知道快递把货送来时一定会给你打个电话(假定一定能叫醒你)。&/li&&li&非阻塞&b&忙&/b&轮询。接着上面等快递的例子,如果用忙轮询的方法,那么你需要知道快递员的手机号,然后每分钟给他挂个电话:“你到了没?”&/li&&/ul&很明显一般人不会用第二种做法,不仅显很无脑,浪费话费不说,还占用了快递员大量的时间。&br&大部分程序也不会用第二种做法,因为第一种方法经济而简单,经济是指消耗很少的CPU时间,如果线程睡眠了,就掉出了系统的调度队列,暂时不会去瓜分CPU宝贵的时间片了。&br&&br&为了了解阻塞是如何进行的,我们来讨论缓冲区,以及内核缓冲区,最终把I/O事件解释清楚。缓冲区的引入是为了减少频繁I/O操作而引起频繁的系统调用(你知道它很慢的),当你操作一个流时,更多的是以缓冲区为单位进行操作,这是相对于用户空间而言。对于内核来说,也需要缓冲区。&br&假设有一个管道,进程A为管道的写入方,B为管道的读出方。&br&&ol&&li&假设一开始内核缓冲区是空的,B作为读出方,被阻塞着。然后首先A往管道写入,这时候内核缓冲区由空的状态变到非空状态,内核就会产生一个事件告诉B该醒来了,这个事件姑且称之为“缓冲区非空”。&/li&&li&但是“缓冲区非空”事件通知B后,B却还没有读出数据;且内核许诺了不能把写入管道中的数据丢掉这个时候,A写入的数据会滞留在内核缓冲区中,如果内核也缓冲区满了,B仍未开始读数据,最终内核缓冲区会被填满,这个时候会产生一个I/O事件,告诉进程A,你该等等(阻塞)了,我们把这个事件定义为“缓冲区满”。&/li&&li&假设后来B终于开始读数据了,于是内核的缓冲区空了出来,这时候内核会告诉A,内核缓冲区有空位了,你可以从长眠中醒来了,继续写数据了,我们把这个事件叫做“缓冲区非满”&/li&&li&也许事件Y1已经通知了A,但是A也没有数据写入了,而B继续读出数据,知道内核缓冲区空了。这个时候内核就告诉B,你需要阻塞了!,我们把这个时间定为“缓冲区空”。&/li&&/ol&这四个情形涵盖了四个I/O事件,缓冲区满,缓冲区空,缓冲区非空,缓冲区非满(注都是说的内核缓冲区,且这四个术语都是我生造的,仅为解释其原理而造)。这四个I/O事件是进行阻塞同步的根本。(如果不能理解“同步”是什么概念,请学习操作系统的锁,信号量,条件变量等任务同步方面的相关知识)。&br&&br&然后我们来说说阻塞I/O的缺点。但是阻塞I/O模式下,一个线程只能处理一个流的I/O事件。如果想要同时处理多个流,要么多进程(fork),要么多线程(pthread_create),很不幸这两种方法效率都不高。&br&于是再来考虑非阻塞忙轮询的I/O方式,我们发现我们可以同时处理多个流了(把一个流从阻塞模式切换到非阻塞模式再此不予讨论):&br&while true {&br&
for i in stream[]; {&br&
if i has data&br&
read until unavailable&br&}&br&}&br&我们只要不停的把所有流从头到尾问一遍,又从头开始。这样就可以处理多个流了,但这样的做法显然不好,因为如果所有的流都没有数据,那么只会白白浪费CPU。这里要补充一点,阻塞模式下,内核对于I/O事件的处理是阻塞或者唤醒,而非阻塞模式下则把I/O事件交给其他对象(后文介绍的select以及epoll)处理甚至直接忽略。&br&&br&为了避免CPU空转,可以引进了一个代理(一开始有一位叫做select的代理,后来又有一位叫做poll的代理,不过两者的本质是一样的)。这个代理比较厉害,可以同时观察许多流的I/O事件,在空闲的时候,&b&会把当前线程阻塞掉&/b&,当有一个或多个流有I/O事件时,就从阻塞态中醒来,于是我们的程序就会轮询一遍所有的流(于是我们可以把“忙”字去掉了)。代码长这样:&br&while true {&br&
select(streams[])&br&
for i in streams[] {&br&
if i has data&br&
read until unavailable&br&}&br&}&br&于是,如果没有I/O事件产生,我们的程序就会阻塞在select处。但是依然有个问题,我们从select那里仅仅知道了,有I/O事件发生了,但却并不知道是那几个流(可能有一个,多个,甚至全部),我们只能&b&无差别轮询&/b&所有流,找出能读出数据,或者写入数据的流,对他们进行操作。&br&但是使用select,我们有O(n)的无差别轮询复杂度,同时处理的流越多,每一次无差别轮询时间就越长。再次&br&&b&说了这么多,终于能好好解释epoll了&/b&&br&epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll之会把哪个流发生了怎样的I/O事件通知我们。此时我们对这些流的操作都是有意义的。(复杂度降低到了O(k),k为产生I/O事件的流的个数,也有认为O(1)的[更新 1])&br&在讨论epoll的实现细节之前,先把epoll的相关操作列出[更新 2]:&br&&ul&&li&epoll_create 创建一个epoll对象,一般epollfd = epoll_create()&br&&/li&&li&epoll_ctl (epoll_add/epoll_del的合体),往epoll对象中增加/删除某一个流的某一个事件&br&比如&br&epoll_ctl(epollfd, EPOLL_CTL_ADD, socket, EPOLLIN);//有缓冲区内有数据时epoll_wait返回&br&epoll_ctl(epollfd, EPOLL_CTL_DEL, socket, EPOLLOUT);//缓冲区可写入时epoll_wait返回&/li&&li&epoll_wait(epollfd,...)等待直到注册的事件发生&br&&/li&&/ul&(注:当对一个非阻塞流的读写发生缓冲区满或缓冲区空,write/read会返回-1,并设置errno=EAGAIN。而epoll只关心缓冲区非满和缓冲区非空事件)。&br&一个epoll模式的代码大概的样子是:&br&while true {&br&
active_stream[] = epoll_wait(epollfd)&br&
for i in active_stream[] {&br&
read or write till unavailable&br&}&br&}&br&限于篇幅,我只说这么多,以揭示原理性的东西,至于epoll的使用细节,请参考man和google,实现细节,请参阅linux kernel source。&br&======================================&br&[更新1]: 原文为O(1),但实际上O(k)更为准确&br&[更新2]: 原文所列第二点说法让人产生EPOLLIN/EPOLLOUT等同于“缓冲区非空”和“缓冲区非满”的事件,但并非如此,详细可以Google关于epoll的边缘触发和水平触发。
更新:由于此文陆陆续续收到赞同,而且其中有些地方并不完全正确,特在本文最后予以订正 我不了解楼主的层次,我必须从很多基础的概念开始构建这个答案,并且可能引申到很多别的问题。 首先我们来定义流的概念,一个流可以是文件,socket,pipe等…
&p&从架构上来讲,降低共享内存的使用,本来就是解耦和的重要手段之一,举几个例子&/p&&p&&b&案例:MMORPG AOI 模块&/b&&/p&&p&MMORPG 服务器逻辑依赖实时计算 AOI,AOI计算模块需要实时告诉其他模块,对于某个玩家:&/p&&ul&&li&有哪些人进入了我的视线范围?&/li&&li&有哪些人离开了我的视线范围?&/li&&li&区域内的角色发生了些什么事情?&/li&&/ul&&img src=&/v2-dde6c76f22f_b.png& data-rawwidth=&269& data-rawheight=&238& class=&content_image& width=&269&&&br&&p&所有逻辑都依赖上述计算结果,因此角色有动作的时候才能准确的通知到对它感兴趣的人。这个计算很费 CPU,特别是 ARPG跑来跑去那种,一般放在另外一个线程来做,但这个模块又需要频繁读取各个角色之间的位置信息和一些用户基本资料。&/p&&p&最早的做法就是简单的加锁:&/p&&p&第一是对主线程维护的用户位置信息加锁,保证AOI模块读取不会出错。&/p&&p&第二是对AOI模块生成的结果数据加锁,方便主线程访问。&/p&&p&如此代码得写的相当小心,性能问题都不说了,稍有不慎状态就会弄挂,写一处代码要经常回过头去看另外一处是怎么写的,担心自己这样写会不会出错。新人加入后,经常因为漏看老代码,考虑少了几处情况,弄出问题来你还要定位一半天难以查证。&/p&&p&演进后的合理做法当然是 AOI和主线程之间不再有共享内存,主线程维护玩家上线下线和移动,那么它会把这些变化情况抄一份用消息发送给 AOI模块,AOI模块根据这些消息在内部构建出另外一份完整的玩家数据,自己访问不必加锁;计算好结果后,又用消息投递给主线程,主线程根据AOI模块的消息自己在内存中构建出一份AOI结果数据来,自己频繁访问也不需要加锁。&/p&&p&由此AOI模块得以完全脱离游戏,单独开发优化,相互之间的偶合已经降低到只有消息级别的了,由于AOI并不需要十分精密的结果,主线程针对角色位置变化不必要每次都通知AOI,隔一段时间(比如0.2秒)通知下变动情况即可。而两个线程都需要频繁的访问全局玩家坐标信息,这样各自维护一份以后,将“高频率的访问” 这个动作限制在了各自线程自己的私有数据中,完全避免了锁冲突和逻辑状态冲突。&/p&&p&用一定程度的数据冗余,换取了较低的模块偶合。出问题概率大大降低,可靠性也上升了,每个模块还可以单独的开发优化。&/p&&br&&p&&b&案例:IM广播进程&/b&&/p&&p&同频道/房间/群 人数少于5000,那么你基本不需要考虑优化广播;而你如果需要处理同频道/房间/群的人数超过 1万,甚至线上跑到10万的时候,广播优化就不得不考虑了。&/p&&p&第二代广播当然是拆线程,拆了线程以后跟AOI一样的由广播线程维护用户状态。然而针对不同的用户集合(频道、房间、群)广播模块需要维护的状态太多了,群的广播需要写一套,房间广播又需要写一套,用户离线推送还需要写一套,都是不同的用户数据结构。&/p&&p&于是第三代广播系统彻底独立成了一个唯一的广播进程,使用 “用户标签” 来决定广播的范围,不光是何种类型的逻辑需要广播了,他只是在同一个用户身上加入了不同的标签(唯一字符串),比如群1的所有用户都有一个群1的标签,频道3的用户都有一个频道3的标签。&/p&&p&所有逻辑模块在用户登录的时候都给用户打一个标签,这个打标签的消息汇总到广播进程自己维护的用户状态数据区,以:用户&-&标签 双向关系进行维护,发广播时逻辑模块只需要告诉广播进程给什么标签的所有用户发什么广播,优先级多少即可。&/p&&p&广播进程组会做好命令拆分,用户分组筛选,消息合并,丢弃,压缩,节拍控制,等一系列标准化操作,比起第一代来,单次实时广播支持广播的人数从几千上升到几十万,模块间也彻底解耦了。&/p&&br&&p&两个例子,做的事情都是把原来共享内存干掉,重新设计了以消息为主的接口方式,各自维护一份数据,以一定程度的数据冗余换取了更低的代码偶合,提升了性能和稳定性还有可维护性。&/p&&p&很多教多线程编程的书讲完多线程就讲数据锁,给人一个暗示好像以后写程序也是这样,建立了一个线程,接下来就该考虑数据共享访问的事情了。所以Erlang的成功就是给这些老模式很好的举了个反例。&/p&&p&所以 “减少共享内存” 和多用 “消息”,并不单单是物理分布问题,这本来就是一种良好的编程模型。它不仅针对数据,代码结构设计也同样实用,有时候不要总想着抽象点什么,弄出一大堆 Base Object/Inerface 的后果有时候是灾难性的。不同模块内部做一定程度的冗余代码,有时反而能让整个项目逻辑更加清晰起来。&/p&&p&所以才会说:高内聚低耦合嘛&/p&&br&&p&关于冗余与偶合的关系,推荐阅读这篇文章:&/p&&p&&a href=&///?target=http%3A///blog/redundancy-vs-dependencies-which-is-worse.html& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Redundancy vs dependencies: which is worse?&i class=&icon-external&&&/i&&/a&&/p&&br&&p&------------------------------------------------------------------------&/p&&p&&b&案例3:NUMA 架构&/b&&/p&&p&多CPU共享一块内存的结构很难再有大的发展,各个核之间的数据同步和控制协议的复杂度随着核的数量上升而成几何级数上升,并发访问性能却不断下降,传统的SMP结构如今碰到了很大瓶颈,&/p&&p&因此同物理主机内部也出现了 NUMA结构,让不同核心访问各自独立的内存区域,由此核心数量可以大大提升,Linux内核已早已支持这样的结构。而很多程序至今仍然用SMP的方式进行编码。&/p&&p&倘若哪天NUMA逐步取代SMP时,要写高性能服务端代码,共享内存这玩意儿,估计你想用都用不了了。&/p&&p&-----------------------------------------------------------------------&/p&&p&&b&反例:XXGAME服务端引擎&/b&&/p&&p&国内某两个字母的最大型的休闲游戏平台,XXGAME,游戏为了避免逻辑崩溃影响网络链接,十多年前就把网络进程独立出来了,逻辑一个进程,网络一个进程,其实就是大多数架构的 LinkServer / Gate 和业务的关系,网络进程和业务之间使用socket通信即可(Linux2.6以后本地 socket通行有 short cut,性能和本地管道一样,基本等同 两次memcpy)。可XXGame服务端引擎,发明了一个 “牛逼的” 共享内存模块,用共享内存+RingBuffer 来给网络进程和逻辑进程做数据交换用,然后写了一大堆觉得很高明的代码来维护这个东西。&/p&&p&听说这套引擎后来还用到了该公司其他牛逼的大型游戏中去了。&/p&&p&这里问一句,网卡每秒钟能传输多少数据?内存的带宽是网卡的多少倍?写那么多的代码避免了一到两次memcpy换来把时间从 100降低到 99,却让代码之间充满了各种偶合,飞线,好玩么?十多年前我听说这套架构的时候就笑了,如今十多年过去了,面对那么多新产生的架构方法和设计理念,你们这套模块自己都不敢怎么改了吧?新人都不敢给他们怎么维护了吧?要不怎么我最近听着还有好几个游戏在用这么老的模式呢。&/p&&br&&p&----&/p&&p&今天也并非向大家提倡纯粹无状态的actor,上面aoi的例子内部实现仍然是个状态机。但进程和线程间的状态隔离内存隔离,以冗余换低耦合本来就是一种经住实践考验的好思路。&/p&
从架构上来讲,降低共享内存的使用,本来就是解耦和的重要手段之一,举几个例子案例:MMORPG AOI 模块MMORPG 服务器逻辑依赖实时计算 AOI,AOI计算模块需要实时告诉其他模块,对于某个玩家:有哪些人进入了我的视线范围?有哪些人离开了我的视线范围?区域…
来看这个代码:&br&&div class=&highlight&&&pre&&code class=&language-text&&int fun(int& a)
return a+b+c;
int main()
//.........做一些和a无关的事
return fun(a);
&/code&&/pre&&/div&这个代码是很好优化的,因为编译器知道a的值是1,参考上下文,编译器又能知道b和c的值也是1,&br&而且根本没有人用到了a,b,c三个变量,也没有任何人在修改a,b,c三个的值,所以编译器可能就直接&br&把这个函数优化成:&br&&div class=&highlight&&&pre&&code class=&language-text&&int main() { return 3; }
&/code&&/pre&&/div&了.&br&&br&这么优化有什么问题吗? 单线程没问题,但多线程就有问题了,如果是多线程,&br&a的值虽然在当前上下文中不会被修改,但可能正在被其他线程修改啊.于是上面的优化&br&就不对了. 那么,volatile关键字在这里就可以帮助我们了,volatile关键字提醒编译器: &br&a可能随时被&b&意外&/b&修改.&br&意外的意思是虽然当前这段代码里看起来a不会变,但可能别的地方正在修改a的值哦.&br&所谓&别的地方&,某些情况下指的就是其他线程了.&br&&br&那么,如果把代码修改如下:&br&&div class=&highlight&&&pre&&code class=&language-text&&int fun(volatile int& a)
return a+b+c;
int main()
volatile int a=1;
//.........做一些和a无关的事
return fun(a);
&/code&&/pre&&/div&编译器就不敢优化了:&br&&br&&div class=&highlight&&&pre&&code class=&language-text&&int fun(volatile int& a)
int b = //这里从内存读一下a吧,谁知道a还等不等于1呢
int c = //这里再从内存读一下a吧,谁知道a还等不等于1呢
return a+b+c;
//这里也从内存读一下a吧,谁知道a还等不等于1呢
int main()
volatile int a=1;
//.........做一些和a无关的事
return fun(a); //完全不敢优化啊,鬼知道a变成多少了....
&/code&&/pre&&/div&&br&同理的,这段代码:&br&&div class=&highlight&&&pre&&code class=&language-text&&//..........
//做一些和a无关的事
if(a==0) doSomething();
//..........
&/code&&/pre&&/div&编译器会发现,a肯定等于0啊,那我还if个毛啊,直接优化掉!&br&&div class=&highlight&&&pre&&code class=&language-text&&//..........
//做一些和a无关的事
doSomething(); //if被去掉了
//..........
&/code&&/pre&&/div&但,一旦添加了volatile,编译器就不敢优化了.例如:&br&&div class=&highlight&&&pre&&code class=&language-text&&//..........
volatile int a=0;
//做一些和a无关的事
if(a==0) doSomething(); //可不敢优化这里! 谁知道a变成多少了!
//..........
&/code&&/pre&&/div&&br&这便是volatile的作用了.&br&&br&必须补充说明,volatile和锁没有一毛钱的关系,该加锁依然需要加锁.给变量添加volatile并不会让其自动拥有一个锁.所以该加锁还得加.&br&&br&//------------------- 更新答案 -------------------------------------------&br&&br&感谢大家的鼓励,受宠若惊! 重新看了一下答案,感觉还可以再补充一下,再举一个例子吧:&br&&br&网上教程里经常见到双检锁保证单例模式的代码,简化一下,大概逻辑如下:&br&&div class=&highlight&&&pre&&code class=&language-text&&static int*
int& get_instance()
if( !instance ) { //检查如果单例的指针是0
此处有某种锁; //则在此处上锁
if( !instance ) {
//再判断一次,以防等待锁期间有别的线程已经new完了
instance = //确认无误则new之
int main()
int& i = get_instance();
&/code&&/pre&&/div&&br&耳听为虚眼见为实,咱们看看反汇编如何(Intel ICC,O2,为了方便看反汇编禁用inline):&br&&div class=&highlight&&&pre&&code class=&language-text&&...................
eax,dword ptr ds:[010B5100h] //读取instance指针到eax
eax,eax //检查eax是否为0
get_instance+12h (010B1042h) //如果为0,则跳转下文010B1042处
...................
//此处为下文中跳回的位置
...................
//get_instance()函数返回
................... //010B1042从这里开始
dword ptr ds:[10B309Ch] //这里面call进去是malloc函数
esp,4 //调整栈
dword ptr ds:[010B5100h],eax//将malloc出的写回instance地址
get_instance+0Dh (010B103Dh) //跳回前面的代码
.........................
&/code&&/pre&&/div&&br&反汇编发现什么问题没? 喂! 判断只做了一次啊!!!! 第二个if去哪里了!&br&哪里去了? 被编译器优化掉了.... 因为这里的优化逻辑很简单:&br&如果第一个判断某值==0成功,根本没必要去做第二个判断,因为编译器能发现此值没被这段代码&br&修改,同时编译器认为此值也不会被其他人&意外&修改,于是,苦心积虑所做的双检锁失效了.跟没写一样.&br&&br&好了,见证奇迹的时候到了,我们就改一行代码:&br&&div class=&highlight&&&pre&&code class=&language-text&&static int*
&/code&&/pre&&/div&&br&再编译一下,看看反汇编:&br&&div class=&highlight&&&pre&&code class=&language-text&&
eax,dword ptr ds:[h]
//读取instance指针到eax
//检查eax是否为0
get_instance+17h (h)//如果为0,则跳转下文h处
.................
//get_instance()函数返回
.................
//以下为上文中跳转位置:
eax,dword ptr ds:[h] //再次读取instance指针到eax
//再次检查eax是否为0
get_instance+0Dh (0120103Dh) //如果非0,跳回上文return处
//如果还是0,往下执行malloc什么的.
dword ptr ds:[120309Ch] //这里进去是malloc
...........
dword ptr ds:[h],eax //将malloc好的值写回instance
get_instance+0Dh (0120103Dh) //返回上文
...........
&/code&&/pre&&/div&&br&终于,双检锁的逻辑正确了.因为volatile已经提示编译器,instance指针可能被&意外&修改.不要瞎做优化.&br&&br&这里有一个要吐槽的,intel ICC用最高等级优化,不加volatile的话连第一个判断都被优化掉了,&br&而MSVC无论怎么开优化,加不加volatile,永远两个判断全做,不愧是安全第一...&br&&br&特别提醒: 实际上即使加了volatile,这样的双检锁依然不安全,只有原子操作才安全,&br&详情请见我的另一个答案:&br&&a href=&/question//answer/& class=&internal&&对int变量赋值的操作是原子的吗? - 知乎用户的回答&/a&&br&&br&//------------------------------------&br&&br&评论区有朋友问是否多线程都要加volatile,首先,无论加不加volatile关键字,&br&任何多线程同时读/写变量,不加锁不用原子操作,则都是race condition,&br&在C++11标准中,race condition是未定义行为.这样做就跟*((int*)0)=1一样危险.&br&所以,上文中的双检锁依然是危险的.因为对instance本身的读写没有锁,且是非原子的.&br&&br&但是,回到现实中,很多锁或者大部分原子操作都附带memory read/write barrier, &br&一定程度上可以保证内存读写的顺序不会被编译器瞎优化.确实能避免一些危险.&br&至于memory barrier能不能就完全替代volatile了,基本可以确定是不能,但我水平有限,举不出例子.&br&&br&最后的最后归纳一下吧,多线程读写变量? 要安全? 加volatile! 加原子操作/锁!&br&&br&PS: 如果想转载请署我名且通知我,因为这样我会好爽好爽
来看这个代码: int fun(int& a)
return a+b+c;
int main()
//.........做一些和a无关的事
return fun(a);
}这个代码是很好优化的,因为编译器知道a的值是1,参考上下文,编译器又能知道b和c的值也是1, 而且根本没…
&b&虽然不是大侠,但以下回答基于我自己的面试不同公司经历,有一定参考性,&/b&本人也是在简历中声称熟悉多线程,原来觉得这是真的,但后来发现这是吹牛不怕吹死的无畏精神。我觉得大多数只是干过多线的人工作经验上其实很少能cover完全下面这么多topic的。因此还是要准备准备。。&br&不同平台提供的工具很不一样,看公司要求。关键还是要了解原理。下面的案例差不多是6家左右要求多线程熟练的公司应聘的总结。&br&1. 了解进程线程的基本概念,能用一种语言在一个平台上实现一个多线程的例子。(这些不会还写熟悉多线程就太大无畏了)&br&2. 了解为什么要用Mutex之类的工具做锁来同步和保护资源。弄懂诸如racing condition,死锁之类的概念。50%公司的见面题,用来砍死大无畏。&br&3. 了解编译器优化带来的影响,了解cache的影响,了解volatile,memory barrier之类的概念。如果是主Java的话,去了解一下JVM的内存模型。以上这些偏硬偏系统端的公司喜欢问,不过由于太基础,稍稍好奇一点的多线程领域程序员都应该会了解,否则略显大无畏。&br&4. 了解一下你主攻平台+语言所提供的工具库,知道常用的工具的用法和使用场景:Mutex,Semaphore,原子操作集,Condition Variable,spin lock。这几个算是比较常用的,在各个平台+语言也都有对应实现。老实说,spinlock,condition variable是我工作里从没用过的,但是也被问过,其他几个都太常用了,如果是java的话再多看一组Executor相关的,以及Java多线程相关的keywords,和object本身提供的同步函数,wait notify之类的,在主Java的公司问过。&br&5. 了解常用的多线程设计范式,比如读写锁(Reader/Writer Lock,非常经典的范式,有偏向读和写的不同变形,至少被要求写过3次),生产消费范式(写过2次),一些常用容器的实现,比如BlockingQueue(写过3次)或者concurrentHashmap(写过2次)。如果是主Java的话可以看看JDK的实现。熟悉一下一些算不上多线程设计模式的小技巧,比如传递只读对象可以避免加锁,或者Copy传递以防外部修改之类的(讨论环节被问过)。另外值得特别一提的一个小细节是,Singleton的线程安全是个很有意思而且容易出错的话题,值得一看(只被问过一次,不过我答挂了,所以印象及其深)。还有可能会问的是一些有趣的小场景让你实现一些功能需要线程安全,无法特别准备,但是你能了解上面说的这些范式,不傻的话大多数都能想出来。&br&如果和我一样多线程方面是主Java的话,记得Doug Lea的书写的很明白,不过不记得当时读完的是哪本,70%可能是下面这个&br&&a href=&///?target=http%3A///title/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&/Java-Concurrency-Practice-Brian-Goetz/dp/&i class=&icon-external&&&/i&&/a&&br&否则就是&br&&a href=&///?target=http%3A///Concurrent-Programming-Java%25C2%25BF-Principles-Pattern/dp//ref%3Dsr_1_1%3Fs%3Dbooks%26ie%3DUTF8%26qid%3D%26sr%3D1-1%26keywords%3Dconcurrent%2Bjava%2Bpattern& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Concurrent Programming in Java: Design Principles and Pattern (2nd Edition): Doug Lea: 2: : Books&i class=&icon-external&&&/i&&/a&&br&&br&这个大致是一些公司对多线程部分的要求,如果应聘者声称熟悉这个部分。上面所有点都是本人面试被问到的,基本上能看完上面这些,可以做到不用很心虚在简历上写自己熟悉多线程而不会被揭穿。
虽然不是大侠,但以下回答基于我自己的面试不同公司经历,有一定参考性,本人也是在简历中声称熟悉多线程,原来觉得这是真的,但后来发现这是吹牛不怕吹死的无畏精神。我觉得大多数只是干过多线的人工作经验上其实很少能cover完全下面这么多topic的。因此还…
&p&&b&自旋锁(spinlock)&/b&很好理解。对自旋锁加锁的操作,你可以认为是类似这样的:&/p&&div class=&highlight&&&pre&&code class=&language-cpp&&&span class=&k&&while&/span& &span class=&p&&(&/span&&span class=&err&&抢锁&/span&&span class=&p&&(&/span&&span class=&n&&lock&/span&&span class=&p&&)&/span& &span class=&o&&==&/span& &span class=&err&&没抢到&/span&&span class=&p&&)&/span& &span class=&p&&{&/span&
&span class=&p&&}&/span&
&/code&&/pre&&/div&&p&只要没有锁上,就不断重试。显然,如果别的线程长期持有该锁,那么你这个线程就一直在 while while while 地检查是否能够加锁,浪费 CPU 做无用功。&/p&&p&仔细想想,其实没有必要一直去尝试加锁,因为只要锁的持有状态没有改变,加锁操作就肯定是失败的。所以,抢锁失败后只要锁的持有状态一直没有改变,那就让出 CPU 给别的线程先执行好了。这就是&b&互斥器(mutex)&/b&也就是题目里的互斥锁(不过个人觉得既然英语里本来就不带 lock,就不要称作锁了吧)。对互斥器加锁的操作你可以认为是类似这样的:&/p&&div class=&highlight&&&pre&&code class=&language-cpp&&&span class=&k&&while&/span& &span class=&p&&(&/span&&span class=&err&&抢锁&/span&&span class=&p&&(&/span&&span class=&n&&lock&/span&&span class=&p&&)&/span& &span class=&o&&==&/span& &span class=&err&&没抢到&/span&&span class=&p&&)&/span& &span class=&p&&{&/span&
&span class=&err&&本线程先去睡了请在这把锁的状态发生改变时再唤醒&/span&&span class=&p&&(&/span&&span class=&n&&lock&/span&&span class=&p&&);&/span&
&span class=&p&&}&/span&
&/code&&/pre&&/div&&p&操作系统负责线程调度,为了实现「锁的状态发生改变时再唤醒」就需要把锁也交给操作系统管理。所以互斥器的加锁操作通常都需要涉及到上下文切换,操作花销也就会比自旋锁要大。&/p&&p&以上两者的作用是加锁互斥,保证能够排它地访问被锁保护的资源。&/p&&p&不过并不是所有场景下我们都希望能够独占某个资源,很快你可能就会不得不写出这样的代码:&/p&&div class=&highlight&&&pre&&code class=&language-cpp&&&span class=&c1&&// 这是「生产者消费者问题」中的消费者的部分逻辑&/span&
&span class=&c1&&// 等待队列非空,再从队列中取走元素进行处理&/span&
&span class=&err&&加锁&/span&&span class=&p&&(&/span&&span class=&n&&lock&/span&&span class=&p&&);&/span&
&span class=&c1&&// lock 保护对 queue 的操作&/span&
&span class=&k&&while&/span& &span class=&p&&(&/span&&span class=&n&&queue&/span&&span class=&p&&.&/span&&span class=&n&&isEmpty&/span&&span class=&p&&())&/span& &span class=&p&&{&/span&
&span class=&c1&&// 队列为空时等待&/span&
&span class=&err&&解锁&/span&&span class=&p&&(&/span&&span class=&n&&lock&/span&&span class=&p&&);&/span&
&span class=&c1&&// 这里让出锁,让生产者有机会往 queue 里安放数据&/span&
&span class=&err&&加锁&/span&&span class=&p&&(&/span&&span class=&n&&lock&/span&&span class=&p&&);&/span&
&span class=&p&&}&/span&
&span class=&n&&data&/span& &span class=&o&&=&/span& &span class=&n&&queue&/span&&span class=&p&&.&/span&&span class=&n&&pop&/span&&span class=&p&&();&/span&
&span class=&c1&&// 至此肯定非空,所以能对资源进行操作&/span&
&span class=&err&&解锁&/span&&span class=&p&&(&/span&&span class=&n&&lock&/span&&span class=&p&&);&/span&
&span class=&err&&消费&/span&&span class=&p&&(&/span&&span class=&n&&data&/span&&span class=&p&&);&/span&
&span class=&c1&&// 在临界区外做其它处理&/span&
&/code&&/pre&&/div&&p&你看那个 while,这不就是自己又搞了一个自旋锁么?区别在于这次你不是在 while 一个抽象资源是否可用,而是在 while 某个被锁保护的具体的条件是否达成。&/p&&p&有了前面自旋锁、互斥器的经验就不难想到:「只要条件没有发生改变,while 里就没有必要再去加锁、判断、条件不成立、解锁,完全可以让出 CPU 给别的线程」。不过由于「条件是否达成」属于业务逻辑,操作系统没法管理,需要让能够作出这一改变的代码来手动「通知」,比如上面的例子里就需要在生产者往 queue 里 push 后「通知」!queue.isEmpty() 成立。&/p&&p&也就是说,我们希望把上面例子中的 while 循环变成这样:&/p&&div class=&highlight&&&pre&&code class=&language-cpp&&&span class=&k&&while&/span& &span class=&p&&(&/span&&span class=&n&&queue&/span&&span class=&p&&.&/span&&span class=&n&&isEmpty&/span&&span class=&p&&())&/span& &span class=&p&&{&/span&
&span class=&err&&解锁后等待通知唤醒再加锁&/span&&span class=&p&&(&/span&&span class=&err&&用来收发通知的东西&/span&&span class=&p&&,&/span& &span class=&n&&lock&/span&&span class=&p&&);&/span&
&span class=&p&&}&/span&
&/code&&/pre&&/div&&p&生产者只需在往 queue 中 push 数据后这样,就可以完成协作:&/p&&div class=&highlight&&&pre&&code class=&language-cpp&&&span class=&err&&触发通知&/span&&span class=&p&&(&/span&&span class=&err&&用来收发通知的东西&/span&&span class=&p&&);&/span&
&span class=&c1&&// 一般有两种方式:&/span&
&span class=&c1&&//
通知所有在等待的(notifyAll / broadcast)&/span&
&span class=&c1&&//
通知一个在等待的(notifyOne / signal)&/span&
&/code&&/pre&&/div&&p&这就是&b&条件变量(condition variable)&/b&,也就是问题里的条件锁。它解决的问题不是「互斥」,而是「等待」。&/p&&p&至于&b&读写锁(readers-writer lock)&/b&,看英文可以顾名思义,在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥。读写锁不需要特殊支持就可以直接用之前提到的几个东西实现,比如可以直接用两个 spinlock 或者两个 mutex 实现:&/p&&div class=&highlight&&&pre&&code class=&language-cpp&&&span class=&kt&&void&/span& &span class=&err&&以读者身份加锁&/span&&span class=&p&&(&/span&&span class=&n&&rwlock&/span&&span class=&p&&)&/span& &span class=&p&&{&/span&
&span class=&err&&加锁&/span&&span class=&p&&(&/span&&span class=&n&&rwlock&/span&&span class=&p&&.&/span&&span class=&err&&保护当前读者数量的锁&/span&&span class=&p&&);&/span&
&span class=&n&&rwlock&/span&&span class=&p&&.&/span&&span class=&err&&当前读者数量&/span& &span class=&o&&+=&/span& &span class=&mi&&1&/span&&span class=&p&&;&/span&
&span class=&k&&if&/span& &span class=&p&&(&/span&&span class=&n&&rwlock&/span&&span class=&p&&.&/span&&span class=&err&&当前读者数量&/span& &span class=&o&&==&/span& &span class=&mi&&1&/span&&span class=&p&&)&/span& &span class=&p&&{&/span&
&span class=&err&&加锁&/span&&span class=&p&&(&/span&&span class=&n&&rwlock&/span&&span class=&p&&.&/span&&span class=&err&&保护写操作的锁&/span&&span class=&p&&);&/span&
&span class=&p&&}&/span&
&span class=&err&&解锁&/span&&span class=&p&&(&/span&&span class=&n&&rwlock&/span&&span class=&p&&.&/span&&span class=&err&&保护当前读者数量的锁&/span&&span class=&p&&);&/span&
&span class=&p&&}&/span&
&span class=&kt&&void&/span& &span class=&err&&以读者身份解锁&/span&&span class=&p&&(&/span&&span class=&n&&rwlock&/span&&span class=&p&&)&/span& &span class=&p&&{&/span&
&span class=&err&&加锁&/span&&span class=&p&&(&/span&&span class=&n&&rwlock&/span&&span class=&p&&.&/span&&span class=&err&&保护当前读者数量的锁&/span&&span class=&p&&);&/span&
&span class=&n&&rwlock&/span&&span class=&p&&.&/span&&span class=&err&&当前读者数量&/span& &span class=&o&&-=&/span& &span class=&mi&&1&/span&&span class=&p&&;&/span&
&span class=&k&&if&/span& &span class=&p&&(&/span&&span class=&n&&rwlock&/span&&span class=&p&&.&/span&&span class=&err&&当前读者数量&/span& &span class=&o&&==&/span& &span class=&mi&&0&/span&&span class=&p&&)&/span& &span class=&p&&{&/span&
&span class=&err&&解锁&/span&&span class=&p&&(&/span&&span class=&n&&rwlock&/span&&span class=&p&&.&/span&&span class=&err&&保护写操作的锁&/span&&span class=&p&&);&/span&
&span class=&p&&}&/span&
&span class=&err&&解锁&/span&&span class=&p&&(&/span&&span class=&n&&rwlock&/span&&span class=&p&&.&/span&&span class=&err&&保护当前读者数量的锁&/span&&span class=&p&&);&/span&
&span class=&p&&}&/span&
&span class=&kt&&void&/span& &span class=&err&&以写者身份加锁&/span&&span class=&p&&(&/span&&span class=&n&&rwlock&/span&&span class=&p&&)&/span& &span class=&p&&{&/span&
&span class=&err&&加锁&/span&&span class=&p&&(&/span&&span class=&n&&rwlock&/span&&span class=&p&&.&/span&&span class=&err&&保护写操作的锁&/span&&span class=&p&&);&/span&
&span class=&p&&}&/span&
&span class=&kt&&void&/span& &span class=&err&&以写者身份解锁&/span&&span class=&p&&(&/span&&span class=&n&&rwlock&/span&&span class=&p&&)&/span& &span class=&p&&{&/span&
&span class=&err&&加锁&/span&&span class=&p&&(&/span&&span class=&n&&rwlock&/span&&span class=&p&&.&/span&&span class=&err&&保护写操作的锁&/span&&span class=&p&&);&/span&
&span class=&p&&}&/span&
&/code&&/pre&&/div&&p&如果整个场景中只有一个读者、一个写者,那么其实可以等价于直接使用互斥器。不过由于读写锁需要额外记录读者数量,花销要大一点。&/p&&p&你可以认为读写锁是针对某种特定情景的「优化」。但个人还是建议忘掉读写锁,直接用互斥器。&/p&&p&&/p&
自旋锁(spinlock)很好理解。对自旋锁加锁的操作,你可以认为是类似这样的:while (抢锁(lock) == 没抢到) {
}只要没有锁上,就不断重试。显然,如果别的线程长期持有该锁,那么你这个线程就一直在 while while while 地检查是否能够加锁,浪费 CPU 做无用…
具体情况需具体分析。&br&&br&一部分朋友觉得用锁会影响性能,其实锁指令本身很简单,影响性能的是锁争用(Lock Contention),什么叫锁争用,就是你我都想进入临界区,但只能有一个线程能进去,这样就影响了并发度。可以去看看glibc中pthread_mutex_lock的源码实现,在没有contention的时候,就是一条CAS指令,内核都没有陷入;在contention发生的时候,就选择陷入内核然后睡觉,等待某个线程unlock后唤醒(详见Futex)。&br&&br&“只有一个线程在临界区”这件事对lockfree也是成立的,只不过所有线程都可以进临界区,最后只有一个线程可以make progress,其它线程再做一遍。&br&&br&所以contention在有锁和无锁编程中都是存在的,那为什么无锁有些时候会比有锁更快?他们的不同体现在拿不到锁的态度:有锁的情况就是睡觉,无锁的情况就不断spin。睡觉这个动作会陷入内核,发生context switch,这个是有开销的,但是这个开销能有多大呢,当你的临界区很小的时候,这个开销的比重就非常大。这也是为什么临界区很小的时候,换成lockfree性能通常会提高很多的原因。&br&&br&再来看lockfree的spin,一般都遵循一个固定的格式:先把一个不变的值X存到某个局部变量A里,然后做一些计算,计算/生成一个新的对象,然后做一个CAS操作,判断A和X还是不是相等的,如果是,那么这次CAS就算成功了,否则再来一遍。如果上面这个loop里面“计算/生成一个新的对象”非常耗时并且contention很严重,那么lockfree性能有时会比mutex差。另外lockfree不断地spin引起的CPU同步cacheline的开销也比mutex版本的大。&br&&br&lockfree的意义不在于绝对的高性能,它比mutex的优点是使用lockfree可以避免死锁/活锁,优先级翻转等问题。但是因为ABA problem、memory order等问题,使得lockfree比mutex难实现得多。&br&&br&除非性能瓶颈已经确定,否则还是乖乖用mutex+condvar,等到以后出bug了就知道mutex的好了。如果一定要换lockfree,请一定要先profile,profile,profile!请确保时间花在刀刃上。
具体情况需具体分析。 一部分朋友觉得用锁会影响性能,其实锁指令本身很简单,影响性能的是锁争用(Lock Contention),什么叫锁争用,就是你我都想进入临界区,但只能有一个线程能进去,这样就影响了并发度。可以去看看glibc中pthread_mutex_lock的源码实…
volatile 只能保证 “可见性”,不能保证 “原子性”。&br&&br&count++; 这条语句由3条指令组成:&br&
(1)将 count 的值从内存加载到 cpu 的某个寄存器r&br&
(2)将 寄存器r 的值 +1,结果存放在 寄存器s&br&
(3)将 寄存器s 中的值写回内存&br&&br&所以,如果有多个线程同时在执行 count++;,在某个线程执行完第(3)步之前,其它线程是看不到它的执行结果的。&br&&br&在没有 volatile 的时候,执行完 count++;,执行结果其实是写到CPU缓存中,没有马上写回到内存中,后续在某些情况下(比如CPU缓存不够用)再将CPU缓存中的值flush到内存。正因为没有马上写到内存,所以不能保证其它线程可以及时见到执行的结果。&br&在有 volatile 的时候,执行完 count++;,执行结果写到CPU缓存中,并且同时写回到内存,因为已经写回内存了,所以可以保证其它线程马上看到执行的结果。&br&但是,volatile 并没有保证原子性,在某个线程执行(1)(2)(3)的时候,volatile 并没有锁定 count 的值,也就是并不能阻塞其他线程也执行(1)(2)(3)。可能有两个线程同时执行(1),所以(2)计算出来一样的结果,然后(3)存回的也是同一个值。&br&&br&&b&补充几篇资料:&/b&&br&(1)java 内存模型:&a class=& wrap external& href=&///?target=http%3A///java-concurrency/java-memory-model.html& target=&_blank& rel=&nofollow noreferrer&&Java Memory Model&i class=&icon-external&&&/i&&/a&&br&(2)java volatile 关键字:&a class=& wrap external& href=&///?target=http%3A///java-concurrency/volatile.html& target=&_blank& rel=&nofollow noreferrer&&Java Volatile Keyword&i class=&icon-external&&&/i&&/a&&br&(3)使用 volatile 的一些pattern:&a href=&///?target=https%3A///developerworks/java/library/j-jtp06197/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Java theory and practice: Managing volatility&i class=&icon-external&&&/i&&/a&
volatile 只能保证 “可见性”,不能保证 “原子性”。 count++; 这条语句由3条指令组成: (1)将 count 的值从内存加载到 cpu 的某个寄存器r (2)将 寄存器r 的值 +1,结果存放在 寄存器s (3)将 寄存器s 中的值写回内存 所以,如果有多个线程同时在执…
你的问题主要在于编码不多而想得太多。
你的问题主要在于编码不多而想得太多。
刚出道的码农(见下面定义)喜欢琢磨这玩意,其实本质上所谓lock-free的数据结构,都是依赖于CAS(compare and swap)的intrinsic function来实现的。&br&&br&CAS指令其实就是硬件实现上的总线锁,所以其实只是软件意义上的lock-free,其比较的对象是操作系统提供的同步原语。后者往往需要通过调度来实现,也就意味着存在线程切换的可能,线程切换就意味着大范围的cache missing,从而导致较差的性能。所以很多处女座码农喜欢用spin lock,本质上就是把茅坑占住随时可以拉屎(自己效率高了),但对其他任务真的很不公平,整个系统的吞吐可能会更差。&br&&br&硬件实现上,CAS可以通过CPU内部的高速链接通知其他处理器核心,从而避免干到北桥或内存控制器上,所以一般来说效率和不带lock的同样指令差不多。&br&&br&我的个人建议是:粗力度的同步,该用OS primitives就用。细粒度的同步,仅仅用大牛写的lock-free的实现,而且前提是充分理解,千万不要上来就自己撸。因为这么细粒度的同步,往往伴随着另外的问题,比如false sharing和cache ping-pong。具体可以自己google.&br&&br&------------------------------------------------&br&更新一下:&br&可能一开头的“刚出道的码农”另很多人不愉快,这里表示歉意。&br&我的这样说的意思是,lock free除了科研教学意义,在实际项目中是非常非常底层的东西。我不认为实际需要并行计算的项目中有多少机会考虑到这么底层,如果真碰到了,那么请考虑:&br&1. 是否可以在更高层的程序逻辑上避免这么细粒度的同步;&br&2. 是否能证明原来的实现的瓶颈是OS同步原语导致调度;&br&3. 传统的基于同步的实现是否很容易造成死锁;&br&在这些问题被认真思考前建议不要一上来就开撸lock free. 参考我亲身经历:&br&曾经共事过的一码农,憋了几天后突然兴奋的说我把xxx模块里pthread的join全部改成lock free了,然后问性能提高多少,答曰没有profile过但肯定有质的提升。过了段时间,测试发现某产品的某严重bug被证明是之前那个lock-free的改动引起,找到此码农,答曰我可以用A技术然后结合B同时还需要把C模块里的pthread同步也变成lock-free的,问需要多少天,答曰2-3周。然后,然后,我立刻去revert了他之前的代码压了压惊。
刚出道的码农(见下面定义)喜欢琢磨这玩意,其实本质上所谓lock-free的数据结构,都是依赖于CAS(compare and swap)的intrinsic function来实现的。 CAS指令其实就是硬件实现上的总线锁,所以其实只是软件意义上的lock-free,其比较的对象是操作系统提供的…
一个程序员碰到了一个问题,他决定用多线程来解决。现在两个他问题了有…
一个程序员碰到了一个问题,他决定用多线程来解决。现在两个他问题了有…
首先是:&br&x86汇编中,对任何内存地址中的1byte的读永远是原子的.也就是说对一个char的读取永远是原子的,对内存地址对齐2byte的int16类型的读取是原子的,对4byte对齐的int32类型读取是原子的,从从奔腾开始,对8byte对齐地址的int64读取是原子的.所以如果你用的是汇编,保证这些就行了.但C/C++中又是另一番情景:&br&&br&C/C++中,编译器保证基础类型的内存对齐,例如保证double类型的对齐是8(或者4,忘了),&br&即使是malloc出来的也可以保证对齐.但是由于各种不可避免的指针转换,例如 char a[4],float* p=(float*)a的存在,使得对齐的保证基本名存实亡.而且,当一个比较长的类型,例如double被编译器放入寄存器的时候,C++标准根本不保证只用一条指令就将它放入一个寄存器中.例如我可以先把前半部分放入eax,等一会儿再把后半部分放入edx等等.不过,如果你能够确保对齐,那么大多数情况下虽然UB,但你的代码还是有可能正常工作的.&br&&br&再然后,其实上面说的根本不用考虑,因为在C/C++标准中,一个变量除了使用atomic相关的函数以外,任何多线程同时进行的读写实际上都是UB.所以,除非使用标准中的atomic功能,或者使用编译器自带的一些扩展,例如InterlockedAdd之类的,否则都是bug的隐患.例如,有非常多的开O2以上优化就出错的多线程相关代码就是由于类似的原因导致的.&br&&br&一个很经典的例子就是一个网上流传的很广的C++的单例类,以下是那段代码:&br&&div class=&highlight&&&pre&&code class=&language-text&&class SingleTon{
static SingleTon* getInstance(){
if(NULL == instance){
EnterCriticalSection(&cs);
if(NULL == instance){//双检锁,在进入临界区后再检测一次是否对象已经建立
instance = new SingleTon();
LeaveCriticalSection(&cs);
static SingleTon*
//........................
&/code&&/pre&&/div&&br&这个双检锁的代码很可能不能正常工作,因为首先是编写者没有告知编译器必须假设instance是可能被其他线程改变的,因此编译器完全可以认为两次if只保留一个就行了(当然也可能不会).因此首先instance必须改为volatile的,然后就是上面所说的原子性,instance应该改为atomic&Singleton*&.&br&&br&C/C++中变量的原子性其实是个巨大的坑,C++11和C11之前对多线程的问题几乎只字不提,也没有语言层面对原子性的保证,(上文中那段单例的代码应该也是C11之前出现的).所以程序员也没有更好的办法,只能使用GCC和VC里自带的那堆原子操作,或者懒了就直接不考虑这问题了.因此只能写这种有隐含问题的代码,现在没问题了,大胆用atomic&&吧.
首先是: x86汇编中,对任何内存地址中的1byte的读永远是原子的.也就是说对一个char的读取永远是原子的,对内存地址对齐2byte的int16类型的读取是原子的,对4byte对齐的int32类型读取是原子的,从从奔腾开始,对8byte对齐地址的int64读取是原子的.所以如果你用的…
&ul&&li&&b&多线程模型适用于处理短连接,且连接的打开关闭非常频繁的情形,但不适合处理长连接。&/b&多线程模型默认情况下,(在Linux)每个线程会开8M的栈空间,再TCP长连接的情况下,2000/分钟的请求,几乎可以假定有上万甚至十几万的并发连接,假定有10000个连接,开这么多个线程需要G的内存空间!即使调整每个线程的栈空间,也很难满足更多的需求。甚至攻击者可以利用这一点发动DDoS,只要一个连接连上服务器什么也不做,就能吃掉服务器几M的内存,&b&这不同于多进程模型,线程间内存无法共享,因为所有线程处在同一个地址空间中。内存是多线程模型的软肋。&/b&&/li&&li&&b&在UNIX平台下多进程模型擅长处理并发长连接,但却不适用于连接频繁产生和关闭的情形。&/b&Windows平台忽略此项。 同样的连接需要的内存数量并不比多线程模型少,但是得益于操作系统虚拟内存的Copy on
Write机制,fork产生的进程和父进程共享了很大一部分物理内存。但是多进程模型在执行效率上太低,接受一个连接需要几百个时钟周期,产生一个进程 可能消耗几万个CPU时钟周期,两者的开销不成比例。而且由于每个进程的地址空间是独立的,如果需要进行进程间通信的话,只能使用IPC进行进程间通 信,而不能直接对内存进行访问。&b&在CPU能力不足的情况下同样容易遭受DDos,攻击者只需要连上服务器,然后立刻关闭连接,服务端则需要打开一个进程再关闭。&/b&&/li&&/ul&&ul&&li&&b&同时需要保持很多的长连接,而且连接的开关很频繁,最高效的模型是非阻塞、异步IO模型。&/b&而且不要用select/poll,这两个API的有着O(N)的时间复杂度。在Linux用epoll,BSD用kqueue,Windows用IOCP,或者用libevent封装的统一接口(对于不同平台libevent实现时采用各个平台特有的API),这些平台特有的API时间复杂度为O(1)。&b&
然而在非阻塞,异步I/O模型下的编程是非常痛苦的。&/b&由于I/O操作不再阻塞,报文的解析需要小心翼翼,并且需要亲自管理维护每个链接的状态。并且为了充分利用CPU,还应结合线程池,避免在轮询线程中处理业务逻辑。&br&
但这种模型的效率是极高的。以知名的http服务器nginx为例,可以轻松应付上千万的空连接+少量活动链接,每个连接连接仅需要几K的内核缓冲区,想要应付更多的空连接,只需简单的增加内存(数据来源为淘宝一位工程师的一次技术讲座,并未实测)。&b&这使得DDoS攻击者的成本大大增加,这种模型攻击者只能将服务器的带宽全部占用,才能达到目的,而两方的投入是不成比例的。&br&&/b&&/li&&/ul&
多线程模型适用于处理短连接,且连接的打开关闭非常频繁的情形,但不适合处理长连接。多线程模型默认情况下,(在Linux)每个线程会开8M的栈空间,再TCP长连接的情况下,2000/分钟的请求,几乎可以假定有上万甚至十几万的并发连接,假定有10000个连接,开这…
虽然 Mutex和Semaphore 在一定程度上可以互相替代,比如你可以把 值最大为1 的Semaphore当Mutex用,也可以用Mutex+计数器当Semaphore。&br&&br&但是对于设计理念上还是有不同的,Mutex管理的是资源的使用权,而Semaphore管理的是资源的数量,有那么一点微妙的小区别。&br&&br&打个比方,在早餐餐厅,大家要喝咖啡。&br&如果用Mutex的方式,同时只有一个人可以使用咖啡机,他获得了咖啡机的使用权后,开始做咖啡,其他人只能在旁边等着,直到他做好咖啡后,另外一个人才能获得咖啡机的使用权。&br&&br&如果用Semaphore的模式,服务员会把咖啡做好放到柜台上,谁想喝咖啡就拿走一杯,服务员会不断做咖啡,如果咖啡杯被拿光了,想喝咖啡的人就排队等着。&br&&br&Mutex管理的是咖啡机的使用权,而Semaphore管理的是做好的咖啡数量。
虽然 Mutex和Semaphore 在一定程度上可以互相替代,比如你可以把 值最大为1 的Semaphore当Mutex用,也可以用Mutex+计数器当Semaphore。 但是对于设计理念上还是有不同的,Mutex管理的是资源的使用权,而Semaphore管理的是资源的数量,有那么一点微妙的小区…
面试多线程比较容易,线程安全性、mutex、条件变量、死锁、race condition、false sharing、如何 debug、给一段代码找错/改错,足够撑满一节 45 分钟的面试。&br&&br&面试 Sockets 比较难,因为现在很少直接用 Sockets API 开发,一般都基于现成的库或框架,各种细节都屏蔽了,考 API 用法是没意义的。不过可以结合 TCP/IP 问问在各种条件下的程序表现。&br&&br&比如两台机器上的A和B进程通过 TCP 通信:&br&&ul&&li&进程 crash 会怎么样&/li&&li&进程 deadlock 会怎么样&/li&&li&进程或机器过载,反应变慢会怎么样&/li&&li&进程死循环,拼命发消息会怎么样&/li&&li&机器重启会怎么样&/li&&li&机器死机会怎么样&/li&&li&机器网卡抽风,丢包严重会怎么样&/li&&li&交换机或路由器坏了或过载会怎么样&/li&&li&路由器过热重启会怎么样&/li&&li&A和B之间的带宽被别的服务占用了会怎么样&br&&/li&&/ul&&p&如何诊断以上这些情况。如果A和B之间有防火墙,还会出哪些情况。&/p&&p&以上这些问题足够撑满 45 分钟的面试,让你获得足够的信息。&/p&
面试多线程比较容易,线程安全性、mutex、条件变量、死锁、race condition、false sharing、如何 debug、给一段代码找错/改错,足够撑满一节 45 分钟的面试。 面试 Sockets 比较难,因为现在很少直接用 Sockets API 开发,一般都基于现成的库或框架,各种细…
互斥锁和互斥量在我的理解里没啥区别,不同叫法。广义上讲可以值所有实现互斥作用的同步机制。狭义上讲指的就是mutex这种特定的二元锁机制。&br&&br&互斥锁的作用就是互斥,mutual exclusive,是用来保护临界区(critical section)的。所谓临界区就是代码的一个区间,如果两个线程同时执行就有可能出问题,所以需要互斥锁来保护。&br&&br&信号量(semaphore)是一种更高级的同步机制,mutex可以说是semaphore在仅取值0/1时的特例。Semaphore可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥。&br&&br&自旋锁是一种互斥锁的实现方式而已,相比一般的互斥锁会在等待期间放弃cpu,自旋锁(spinlock)则是不断循环并测试锁的状态,这样就一直占着cpu。&br&&br&同步锁好像没啥特殊说法,你可以理解为能实现同步作用的都可以叫同步锁,比如信号量。&br&&br&最后,不要钻这些名词的牛角尖,更重要的是理解这些东西背后的原理,叫什么名字并没有什么好说的。这些东西在不同的语言和平台上又有可能会有不同的叫法,其实本质上就这么回事。
互斥锁和互斥量在我的理解里没啥区别,不同叫法。广义上讲可以值所有实现互斥作用的同步机制。狭义上讲指的就是mutex这种特定的二元锁机制。 互斥锁的作用就是互斥,mutual exclusive,是用来保护临界区(critical section)的。所谓临界区就是代码的一个区间…
已有帐号?
无法登录?
社交帐号登录

我要回帖

更多关于 workerman 多进程实现 的文章

 

随机推荐