并发模型第贰讲-accept+thread
前言
今天总结下一个并发模型,利用线程来达到并发的效果。从某种方面来说,现在的操作系统给我们提供了两种方式来进行并发任务的执行,一种是进程,另一种是线程。 相对于前者来说,线程要更轻。所谓更轻,指的是线程的创建、销毁以及切换比起进程来要快速些。
这里介绍accept+thread的并发模型方式。
一、accept+thread 模型
1.1 基本介绍
说实话,从形式来看,这种并发模型的方式与前一篇的accept+fork的方式差不多。 这里是:主线程在一个大循环中阻塞等待连接,有新的连接到来只有,新建一个线程执行具体的业务逻辑处理。 所以上一篇的图修改修改拿来这里应该也是阔以的。
1.2 代码部分
服务器端的主要代码如下:
主要的框架代码:
#include "com.h"
#include "lib.h"
#include <pthread.h>
#include "str_echo.h"
#define LISTENQ 1024
int main(int argc, char**argv){
int listenFd,connFd;
pid_t childPid;
socklen_t cliLen;
struct sockaddr_in cliAddr,servAddr;
//socket
if((listenFd = socket(AF_INET,SOCK_STREAM,0))<0){
printf("socket error\n");
exit(-1);
}
bzero(&servAddr,sizeof(servAddr));
servAddr.sin_family=AF_INET;
servAddr.sin_addr.s_addr=htonl(INADDR_ANY);
servAddr.sin_port=htons(SERV_PORT);
//bind
if(bind(listenFd,(struct sockaddr*)&servAddr,sizeof(servAddr))<0){
printf("bind error\n");
exit(-1);
}
//listen
if((listen(listenFd,LISTENQ)) <0){
printf("listen error\n");
exit(-1);
}
pthread_t t_id;
while(1){
cliLen = sizeof(cliAddr);
if((connFd = accept(listenFd,(struct sockaddr*)&cliAddr,&cliLen))<0){
printf("accept error\n");
exit(-1);
}
printf(" in server main: conndfd is %d\n",connFd);
int ret = pthread_create(&t_id,NULL,(void*) doit,(void*) connFd); //创建线程 //???传递参数可能不安全
if(ret != 0){
printf("Create pthread error!\n");
exit(0);
}
// pthread_join(t_id,NULL); //阻塞等待线程结束
}
return 0;
}
具体的业务逻辑代码
#include"com.h"
#include "lib.h"
#include <pthread.h>
void str_echo(int sockFd){
ssize_t n;
char buf[MAXLINE];
while(1){
while((n=read(sockFd,buf,MAXLINE))>0){
Writen(sockFd,buf,n);
}
if(n<0 && errno == EINTR){
continue;
} else if(n<0){
printf("read error\n");
exit(-1);
}else if(0 == n){ //finish echo server
return ;
}
}
}
void *doit(void*arg){
int intArg = (int) arg;
pthread_detach(pthread_self()); //设置成自动分离的状态
str_echo(intArg);
close(intArg);
return NULL;
}
注意上述代码借用了UNP中的例子,有很多IO函数来自UNP,篇幅所限,这里没有全部贴出。
二、总结
同样,这里先解决几个遇到有关内核的小问题,最后在对这种并发模型做一个小总结。
2.1 几个小问题
(1)、有关信号处理的问题,即如果进程(或者)线程处在阻塞睡眠之中,信号发生了,这时候怎么办?
如果把线程或者进程看成正常的执行流的话,信号(有的也称为软件中断)是打断这种正常执行流的一种机制。它通常是异步的,也就是说进程预先不知道何时这种情况会发生。
如果进程正处在正常的执行状态,那么信号来了(未被mask),ok,进程会转去执行信号处理,处理完了之后再回来(和中断处理类似)。
如果进程正处于阻塞状态(比如说正阻塞等待accept上),这个时候发生了信号事件,那么这个时候怎么办呢?
你可能对这个不屑一顾,那就和上面的一样啊,先去进行信号处理,处理完了接着阻塞呗。
enen…
事是这么个事,但是如果要深究一下的话,还有有些东西的:
进程如果处在阻塞中,那么它是处于阻塞队列中的,一般情况下,阻塞队列里的进程是不会被系统调度到CPU上执行的(除非它等待的情况发生了,比如说阻塞在accept上的进程发现有连接到来就会重新加入就绪队列。 这是基本的操作系统常识,这里就不说了)。 现在信号来了,为了执行信号处理函数,操作系统不得不把原本应该阻塞的进程调度到CPU上进行执行。
现在,问题来了,执行完信号处理函数,进程该怎么办? 是继续回到阻塞队列,还是接着往下执行? 如果要是继续往下执行的话,原本阻塞的事件还没来呢(从某种程度来说,进程是被“非期待”的事件所唤醒的)?
现在很多操作系统内核,都采用了前者,又内核把进程继续放入到阻塞队列,就像什么事都没发生过一样(实际上执行了信号处理)。 当然也有一些内核会继续往下执行,这个时候如果想重新回到阻塞队列,就必须由上层用户进行信号判断,然后在一个循环里手动重新调用阻塞函数(重新加入阻塞队列中)。
【1】中信号处理这一节有比较详细的描述,可以参考。
(2)、有关线程僵尸的讨论。
在建立线程的过程中,有关于一个线程分离的概念。 新建立的线程,一般来说会使用pthread_detach() 函数,自行分离(或者主线程主动调用 pthread_join()也可以),这么做的目的是为了让线程退出的时候能够“完整的dead”,而不会形成一种“半死不活”的“僵尸”状态。,有的说法叫做“僵尸线程”。
上述操作和进程中所做的差不多, 但是我在做实验的时候,没有像进程那样,确切的观察到“僵尸线程”。
如上文所示,使用命令
top -Hp {服务端进程pid}
然后在客户端终止一个连接,按理说如果在程序中没有detach, 会产生僵尸线程,但是上面没有显示出来。 这个我也没搞太清楚,有小伙伴了解的,希望可以不吝赐教。
2.2 accept+thread 模型小总结
有些说法把这种模型称作thread-per-connection, 相对于上一个accept+fork 模型来说,线程的开销比较小。但是相对来说还是不太适合于短连接(创建线程的开销和短连接开销差不多)。 然而和上篇类似, 这两种方案都受到线程数量(或者进程数量)的影响,如果太多的话,操作系统吃不消的。
参考
【1】、Unix 网络编程(卷1)
【2】、僵尸线程
【3】、 Linux多线程模型
【4】、Linux进程、线程模型,LWP,pthread_self()