在多进程并发服务器中:父进程accept
一个连接,fork
一个子进程,该子进程处理与该连接客户端之间的通信。多进程会存在下面的问题:
fork
是昂贵的。fork
要把父进程的内存映像复制到子进程,并在子进程中复制所有描述符等。当今的实现使用称为写时复制(copy-on-write)的技术,用以避免在子进程切实需要自己的副本之前把父进程的数据空间复制到子进程。然而即便有这样的优化措施,fork
仍然是昂贵的。fork
返回之后父子进程之间信息的传递需要进程间通信(IPC)。调用fork
之前父进程向尚未存在的子进程传递信息相当容易,因为子进程将从父进程数据空间及所有描述符的一个副本开始运行。然而从子进程往父进程返回信息却比较费力。
线程
线程的创建可能比进程的创建快10~100倍。同一进程内的所有线程共享相同的全局内存。这使得线程之间易于共享信息,然而伴随这种简易性而来的却是同步问题。
同一进程内的所有线程除了共享全局变量外还共享:
- 进程指令
- 大多数数据
- 打开的文件(即描述符)
- 信号处理函数和信号处置
- 当前工作目录
- 用户ID和组ID
不过每个线程有各自的:
- 线程ID
- 寄存器集合,包括程序计数器和栈指针
- 栈(用于存放局部变量和返回地址)
- errno
- 信号掩码
- 优先级
基本线程函数
pthread_create函数
当一个程序由exec启动执行时,称为初始化线程或主线程的单个线程就创建了。其余线程则由pthread_create
函数创建。
1 |
|
一个进程内的每个线程都由一个线程ID标识,其数据类型为pthread_t
(往往是unsigned int
)。如果新的线程成功创建,其ID就通过tid指针返回。
每个线程都有许多属性:优先级、初始化大小、是否应该成为一个守护线程等等。在创建线程时通过初始化一个取代默认设置的pthread_attr_t
变量指定这些属性。通常采用默认设置,这是把attr
参数指定为空指针。
创建一个线程时最后指定的参数是由该线程执行的函数及其参数。该线程通过调用这个函数开始执行,然后或者显示终止(通过调pthread_exit
),或者隐式地终止(通过让函数返回)。该函数的地址由func
参数指定,该函数的唯一调用参数是指针arg
。如果需要给该函数传递多个参数,可以把它们打包成一个结构,然后把这个结构的地址作为单个参数传递给这个起始函数。
pthread_join函数
我们可以通过调用pthread_join
等待一个给定线程终止。对比线程和UNIX进程,pthread_create
类似于fork
,pthread_join
类似于waitpid
。
1 |
|
我们必须指定要等待线程的tid。不幸的是,Pthread没有办法等待任意一个线程(类似指定进程ID参数为-1调用waitpid)。
pthread_self函数
每个线程都有一个在所属进程内标识自身的ID。线程ID由pthread_create
返回。每个线程使用pthread_self
获取自身的线程ID。
1 |
|
对比线程和UNIX进程,pthread_self
类似于getpid
。
pthread_detach函数
一个线程或者是可汇合的(joinable,默认值),或者是脱离(detached)。当一个可汇合的线程终止时,它的线程ID和退出状态留存到另一个线程对它调用pthread_join
。脱离的线程却像守护进程,当它们终止时,所有相关资源都被释放,不能等待它们终止。如果一个线程需要知道另一个线程什么时候终止,那就最好保持第二个线程的可汇合状态。
pthread_detach
函数把指定的线程转变为脱离状态。
1 |
|
本函数通常由想让自己脱离的线程调用,就如下语句:
1 | pthread_detach(pthread_self()); |
pthread_exit函数
让一个线程终止的方法之一是调用pthread_exit
。
1 |
|
如果本线程未曾脱离,它的线程ID和退出状态将一直留存到调用进程内的某个其他进程对它调用pthread_join
。
指针status
不能指向局部于调用线程的对象,因为线程终止时这样的对象也消失。
让一个线程终止的另外两个方法是:
- 启动线程的函数(即
pthread_create
的第三个参数)可以返回。既然该函数必须返回一个void指针,它的返回值就是相应线程的终止状态。 - 如果进程的main函数返回或者任何线程调用了
exit
,整个进程就终止,其中包括它的任何线程
使用多线程的echo服务器程序
1 |
|
main函数
accept
返回之后,改为调用pthread_create
取代调用fork
,为每个连接的客户端创建一个线程。传递给线程执行函数thread_main
的参数是已连接套接字描述符connfd
。
thread_main函数
thread_main
是由线程执行的函数。线程首先让自身脱离,因为主线程不用等待它创建的每个线程。然后调用str_echo
函数。该函数返回之后,必须close
已连接套接字,因为本线程和主线程共享所有的描述符。对应使用fork
的情形,子进程就不必close
已连接套接字,因为子进程随即终止,而所有打开的描述符在进程终止时都被关闭。
还要注意的是,主线程不关闭已连接套接字,而在调用fork
的并发服务器程序中需要关闭已连接的套接字,这是因为同一进程内的所有线程共享全部描述符,要是主线程调用close
,它就会终止相应的连接。创建新线程并不影响已打开描述符的引用计数,这一点不同于fork
。
CHECK宏
用于检测线程函数的返回值是否正确。
运行程序,使用pstree
查看线程:
[heql@ubuntu ~]$ pstree -p 2696
server(2696)─┬─{server}(2721)
├─{server}(2798)
└─{server}(2823)
上面的服务器程序创建了三个线程,用于处理三个不同的客户端的连接。
使用多线程的echo客户端程序
1 |
|
main函数
创建套接字,调用connect
建立于服务器的连接。调用str_cli
函数。
str_cli函数
创建一个线程,执行copy_to
线程函数,线程的参数为标准输入的描述符和连接套接字的结构体。主线程调用readline
和fputs
,把从套接字读入的每个文本行复制到标准输出。
copy_to线程函数
该线程只是把读入标准输入的每个文本行发送给服务器。当在标准输入上读到EOF时,它通过调用shutdown
从套接字发送 FIN,然后返回。注意这里不能直接调用close
,因为主线程还需要从该套接字读取服务器返回的数据。
运行程序,使用pstree
查看线程:
[heql@ubuntu ~]$ pstree -p 2949
client(2949)───{client}(2951)