1. 进程与线程
“进程(process)”是操作里最重要的两个概念之一,一个进程是“内存中正在运行的程序”。
每个进程都有自己独立的地址空间,可以把“进程”比喻为“人”。每个人都有自己的记忆(memory),人与人之间通过谈话(消息传递)来进行交流,谈话可以是面谈(同一台服务器),也可以在电话里谈(不同的服务器,使用网络通信)。
“线程”这个概念大概是在1993年以后才开始流行,线程的特点是共享地址空间,从而可以高效地共享数据。
2. 单线程服务器的常用编程模型
在高性能的网络程序中,使用得最为广泛的应该就是“non-blocking IO + IO multiplexing”这种模型,即Reactor模式。程序的基本结构就是一个时间循环(event loop),以事件驱动(event-driven)和事件回调的方式实现业务逻辑:
while(!done)
{
int timeout_ms = max(1000, getNextTimeCallBack());
int retval = ::poll(fds, nfds, timeout_ms);
if(retval < 0)
{
//错误处理,回调用户的 error handler
}
else
{
//处理到期的 timer,回调用户的 timer handler
if(retval > 0)
{
//处理IO事件,回调用户的IO event handler
}
}
}
Reactor 模型的优点很明显,编程不难,效率也不错。不仅可以用于读写socket,连接的建立甚至DNS解析都可以用非阻塞方式进行,以提高并发度的吞吐量,对于 IO 密集型的应用是一个不错的选择。
当然基于事件驱动的编程模型也有其本质的缺点,它要求事件回调函数必须是非阻塞的。对于设计网络 IO 的请求响应式协议,它容易割裂业务逻辑,使其散布多个回调函数之中,相对不容易理解和维护。
3. 多线程服务器的常用编程模型
non-blocking IO + one loop per thread 作为本节重点。
3.1 one loop per thread
程序里的每个 IO 线程有一个 event loop(或者叫做Reactor),用于处理读写事件和定时事件。这种做法的好处是:
- 线程数目基本固定,可以在程序启动的时候设置,不会被频繁创建于销毁;
- 可以很方便地在线程间调配负载;
- IO 事件发生的线程是固定的,同一个 TCP 连接不必考虑事情并发。
3.2 线程池
对于没有 IO 而光有计算任务的线程,使用 event loop 优点浪费,我会用一种补充方案,即用 blocking queue 实现的任务队列(TaskQueue):
typedef boost::function<void()> Functor;
BlockingQueue<Functor> taskQueue;
void workerThread()
{
while (running)
{
Functor task = taskQueue.take();
task();
}
}
用这种方式实现线程池特别容易,以下是启动容量(并发数)为N的线程池:
int N = num_of_computing_threads;
for(int i =0 ; i < N; ++i)
{
create_thread(&workerThread); //伪代码,启动线程
}
使用起来也很简单:
Foo foo;
boost::function<void()> task = boost::bind(&Foo::calc, &foo);
taskQueue.post(task);
4. 进程间通信只用TCP
linux下进程间通信(IPC)的方式数不胜数,包括:匿名管道(pipe)、具名管道(FIFO)、POSIX消息队列、共享内存、信号(signals)等,更不比说Sockets了。
同步原语也很多,如:互斥器(mutex)、条件变量(condition variable)、读写锁(read-writer lock)、文件锁(record locking)、信号量(semaphore)等等。