多线程服务器的常用编程模型

本文主要探讨了多线程服务器的编程模型,包括单线程服务器模型、One Loop per Thread模型和线程池模型。推荐使用非阻塞IO结合线程循环和线程池的方式来提高性能和效率。文中还介绍了线程间通信和同步原语的使用,如互斥锁、条件变量和读写锁,并强调了正确使用这些同步机制的重要性。最后,文章指出在多核环境下,掌握多线程编程是必要的,并强调了程序正确性优于性能优化的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

多线程服务器的常用编程模型


 

陈硕 (giantchen_AT_gmail)

Blog.youkuaiyun.com/Solstice

2009 Feb 12

 

建议阅读本文 PDF 版下载: http://files.cppblog.com/Solstice/multithreaded_server.pdf

 

本文主要讲我个人在多线程开发方面的一些粗浅经验。总结了一两种常用的线程模型,归纳了进程间通讯与线程同步的最佳实践,以期用简单规范的方式开发多线程程序。

文中的“多线程服务器”是指运行在 Linux 操作系统上的独占式网络应用程序。硬件平台为 Intel x64 系列的多核 CPU,单路或双路 SMP 服务器(每台机器一共拥有四个核或八个核,十几 GB 内存),机器之间用百兆或千兆以太网连接。这大概是目前民用 PC 服务器的主流配置。

本文不涉及 Windows 系统,不涉及人机交互界面(无论命令行或图形);不考虑文件读写(往磁盘写 log 除外),不考虑数据库操作,不考虑 Web 应用;不考虑低端的单核主机或嵌入式系统,不考虑手持式设备,不考虑专门的网络设备,不考虑高端的 >=32 核 Unix 主机;只考虑 TCP,不考虑 UDP,也不考虑除了局域网络之外的其他数据收发方式(例如串并口、USB口、数据采集板卡、实时控制等)。

 

有了以上这么多限制,那么我将要谈的“网络应用程序”的基本功能可以归纳为“收到数据,算一算,再发出去”。在这个简化了的模型里,似乎看不出用多线程的必要,单线程应该也能做得很好。“为什么需要写多线程程序”这个问题容易引发口水战,我放到另一篇博客里讨论。请允许我先假定“多线程编程”这一背景。

 

“服务器”这个词有时指程序,有时指进程,有时指硬件(无论虚拟的或真实的),请注意按上下文区分。另外,本文不考虑虚拟化的场景,当我说“两个进程不在同一台机器上”,指的是逻辑上不在同一个操作系统里运行,虽然物理上可能位于同一机器虚拟出来的两台“虚拟机”上。

 

本文假定读者已经有多线程编程的知识与经验,这不是一篇入门教程。

 

本文承蒙 Milo Yip 先生审读,在此深表谢意。当然,文中任何错误责任均在我。

 

 

 

目  录

1 进程与线程 2

2 典型的单线程服务器编程模型 3

3 典型的多线程服务器的线程模型 3

One loop per thread 4

线程池 4

归纳 5

4 进程间通信与线程间通信 5

5 进程间通信 6

6 线程间同步 7

互斥器 (mutex) 7

跑题:非递归的 mutex 8

条件变量 10

读写锁与其他 11

封装 MutexLockMutexLockGuard 和 Condition 11

线程安全的 Singleton 实现 14

归纳 15

7 总结 15

后文预览:Sleep 反模式 16

 

1 进程与线程

“进程/process”是操作里最重要的两个概念之一(另一个是文件),粗略地讲,一个进程是“内存中正在运行的程序”。本文的进程指的是 Linux 操作系统通过 fork() 系统调用产生的那个东西,或者 Windows 下 CreateProcess() 的产物,不是 Erlang 里的那种轻量级进程。

每个进程有自己独立的地址空间 (address space)“在同一个进程”还是“不在同一个进程”是系统功能划分的重要决策点。Erlang 书把“进程”比喻为“人”,我觉得十分精当,为我们提供了一个思考的框架。

 

每个人有自己的记忆 (memory),人与人通过谈话(消息传递)来交流,谈话既可以是面谈(同一台服务器),也可以在电话里谈(不同的服务器,有网络通信)。面谈和电话谈的区别在于,面谈可以立即知道对方死否死了(crash, SIGCHLD),而电话谈只能通过周期性的心跳来判断对方是否还活着。

 

有了这些比喻,设计分布式系统时可以采取“角色扮演”,团队里的几个人各自扮演一个进程,人的角色由进程的代码决定(管登陆的、管消息分发的、管买卖的等等)。每个人有自己的记忆,但不知道别人的记忆,要想知道别人的看法,只能通过交谈。(暂不考虑共享内存这种 IPC。)然后就可以思考容错(万一有人突然死了)、扩容(新人中途加进来)、负载均衡(把 的活儿挪給 做)、退休(要修复 bug,先别给他派新活儿,等他做完手上的事情就把他重启)等等各种场景,十分便利。

 

“线程”这个概念大概是在 1993 年以后才慢慢流行起来的,距今不过十余年,比不得有 40 年光辉历史的 Unix 操作系统。线程的出现给 Unix 添了不少乱,很多 库函数(strtok(), ctime())不是线程安全的,需要重新定义;signal 的语意也大为复杂化。据我所知,最早支持多线程编程的(民用)操作系统是 Solaris 2.2 和 Windows NT 3.1,它们均发布于 1993 年。随后在 1995 年,POSIX threads 标准确立。

 

线程的特点是共享地址空间,从而可以高效地共享数据。一台机器上的多个进程能高效地共享代码段(操作系统可以映射为同样的物理内存),但不能共享数据。如果多个进程大量共享内存,等于是把多进程程序当成多线程来写,掩耳盗铃。

 

“多线程”的价值,我认为是为了更好地发挥对称多路处理 (SMP) 的效能。在 SMP 之前,多线程没有多大价值。Alan Cox 说过 A computer is a state machine. Threads are for people who can't program state machines. (计算机是一台状态机。线程是给那些不能编写状态机程序的人准备的。)如果只有一个执行单元,一个 CPU,那么确实如 Alan Cox 所说,按状态机的思路去写程序是最高效的,这正好也是下一节展示的编程模型。

 

2 典型的单线程服务器编程模型

UNP3e 对此有很好的总结(第 章:IO 模型,第 30 章:客户端/服务器设计范式),这里不再赘述。据我了解,在高性能的网络程序中,使用得最为广泛的恐怕要数“non-blocking IO + IO multiplexing”这种模型,即 Reactor 模式,我知道的有:

l lighttpd,单线程服务器。(nginx 估计与之类似,待查)

l libevent/libev

l ACEPoco C++ librariesQT 待查)

l Java NIO (Selector/SelectableChannel), Apache Mina, Netty (Java)

l POE (Perl)

l Twisted (Python)

相反,boost::asio 和 Windows I/O Completion Ports 实现了 Proactor 模式,应用面似乎要窄一些。当然,ACE 也实现了 Proactor 模式,不表。

在“non-blocking IO + IO multiplexing”这种模型下,程序的基本结构是一个事件循环 (event loop):(代码仅为示意,没有完整考虑各种情况)

while (!done)

{

  int timeout_ms = max(1000, getNextTimedCallback());

  int retval = ::poll(fds, nfds, timeout_ms);

  if (retval < 0) {

    处理错误

  } else {

    处理到期的 timers

    if (retval > 0) {

      处理 IO 事件

    }

  }

}

当然,select(2)/poll(2) 有很多不足,Linux 下可替换为 epoll,其他操作系统也有对应的高性能替代品(搜 c10k problem)。

Reactor 模型的优点很明显,编程简单,效率也不错。不仅网络读写可以用,连接的建立(connect/accept)甚至 DNS 解析都可以用非阻塞方式进行,以提高并发度和吞吐量 (throughput)。对于 IO 密集的应用是个不错的选择,Lighttpd 即是这样,它内部的 fdevent 结构十分精妙,值得学习。(这里且不考虑用阻塞 IO 这种次优的方案。)

 

当然,实现一个优质的 Reactor 不是那么容易,我也没有用过坊间开源的库,这里就不推荐了。

 

3 典型的多线程服务器的线程模型

这方面我能找到的文献不多,大概有这么几种:

1. 每个请求创建一个线程,使用阻塞式 IO 操作。在 Java 1.4 引入 NIO 之前,这是 Java 网络编程的推荐做法。可惜伸缩性不佳。

2. 使用线程池,同样使用阻塞式 IO 操作。与 相比,这是提高性能的措施。

3. 使用 non-blocking IO + IO multiplexing。即 Java NIO 的方式。

4. Leader/Follower 等高级模式

在默认情况下,我会使用第 种,即 non-blocking IO + one loop per thread 模式
http://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod#THREADS_AND_COROUTINES

One loop per thread

此种模型下,程序里的每个 IO 线程有一个 event loop (或者叫 

$(function(){ $.fn.extend({ SimpleTree:function(options){ //初始化参数 var option = $.extend({ click:function(a){ } },options); option.tree=this; /* 在参数对象中添加对当前菜单树的引用,以便在对象中使用该菜单树 */ option._init=function(){ /* * 初始化菜单展开状态,以及分叉节点的样式 */ this.tree.find("ul ul").hide(); /* 隐藏所有子级菜单 */ this.tree.find("ul ul").prev("li").removeClass("open"); /* 移除所有子级菜单父节点的 open 样式 */ this.tree.find("ul ul[show='true']").show(); /* 显示 show 属性为 true 的子级菜单 */ this.tree.find("ul ul[show='true']").prev("li").addClass("open"); /* 添加 show 属性为 true 的子级菜单父节点的 open 样式 */ }/* option._init() End */ /* 设置所有超链接不响应单击事件 */ this.find("a").click(function(){ $(this).parent("li").click(); return false; }); /* 菜单项 接受单击 */ this.find("li").click(function(){ /* * 当单击菜单项 * 1.触发用户自定义的单击事件,将该 标签中的第一个超链接做为参数传递过去 * 2.修改当前菜单项所属的子菜单的显示状态(如果等于 true 将其设置为 false,否则将其设置为 true) * 3.重新初始化菜单 */ option.click($(this).find("a")[0]); /* 触发单击 */ /* * 如果当前节点下面包含子菜单,并且其 show 属性的值为 true,则修改其 show 属性为 false * 否则修改其 show 属性为 true */ /* if($(this).next("ul").attr("show")=="true"){ $(this).next("ul").attr("show","false"); }else{ $(this).next("ul").attr("show","true"); }*/ /* 初始化菜单 */ option._init(); }); /* 设置所有父节点样式 */ this.find("ul").prev("li").addClass("folder"); /* 设置节点“是否包含子节点”属性 */ this.find("li").find("a").attr("hasChild",false); this.find("ul").prev("li").find("a").attr("hasChild",true); /* 初始化菜单 */ option._init(); }/* SimpleTree Function End */ }); });
评论 31
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值