一起来写web server 03 -- 多线程版本
错误的代码和正确的代码总是非常相似的!
好吧,我们继续开干,这一次,我们来写一个多线程版本的web
服务器.
这次代码的思想十分简单,那就是一旦从客户端来了一个连接,就生成一个线程来处理这个连接.这种想法和之前的多进程版本非常类似.但是请注意进程和线程之间的差异.
进程和线程的区别
进程这种东西,一旦父进程调用fork
函数,生成了一个子进程,那么子进程基本上不和父进程共享任何东西了,当然,子进程保留了父进程打开文件的指针,复制了父进程的代码区,数据区,寄存器的值,几乎所有的东西,但是一旦fork
,两者之间除了父子关系,它们两者的关系就和普通的两个进程一样了.
线程不一样,父线程创建了子线程之后,父子线程之间共享很多东西,当然,子线程有自己的堆栈(堆栈这个玩意自然不能够共享,如果共享,会造成混乱.).寄存器的值也不会共享,应该还有一些我没有提到的东西不能共享吧,不过这些足够了.除此之外,全部共享,也就是说,如果在子线程里关闭了某个文件描述符,那么这个文件描述符在父线程里面一样被关闭了,事实上:
如果你在父(子)进程中能够得到子(父)线程的堆栈的指针的话,父(子)进程也能够访问子(父)进程的堆栈空间了.
这里有一个微小的错误,你能发现吗?
在推进代码的时候,我曾经写过这样一个主函数:
int main(int argc, char *argv[])
{
int listenfd = Open_listenfd(8080); /* 8080号端口监听 */
signal(SIGPIPE, SIG_IGN); /* 忽略SIGPIPE消息 */
while (true) /* 无限循环 */
{
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int *fdp = (int *)Malloc(sizeof(int));
int connfd = Accept(listenfd, (SA*)&clientaddr, &len);
pthread_t tid;
Pthread_create(&tid, NULL, handle, (void *)connfd);
//close(connfd);
}
return 0;
}
你能发现哪里出错了吗?对了这是处理函数:
void* handle(void* arg)
{
Pthread_detach(pthread_self()); // 脱离父线程
int fd = (*(int *)arg);
printf("%d: fd = %d\n", pthread_self(), fd);
doit(fd);
printf("%d: close fd = %d\n", pthread_self(), fd);
close(fd);
}
这个错误非常地隐蔽,下面是一次打印的结果:
-142670080: fd = 4
-142670080:GET /cpp/concep
t.html HTTP/1.1
-142670080: close fd = 4
-151062784: fd = 5
-151062784:GET /common/ext
.css HTTP/1.1
-151062784: close fd = 5
-142670080: fd = 4
-142670080:GET /common/sit
e_modules.css HTTP/1.1
-142670080: close fd = 4
-142670080: fd = 4
-142670080:GET /common/ski
n_scripts.js HTTP/1.1
-151062784: fd = 6
-142670080: close fd = 4
-151062784:GET /common/sit
e_scripts.js HTTP/1.1
-151062784: close fd = 6
-159455488: fd = 6
Rio_readlineb error: Bad f
ile descriptor
错误的原因
好了,我这里就不卖关子了,代码错在了这里:
Pthread_create(&tid, NULL, handle, (void *)connfd);
我们可以发现,这里的connfd
是栈上的一个变量,当while循环了一遍之后,这个connfd
的值我们就不再确定了,他有可能是原来的值,这样的话,线程运行不会出错,有可能是任何值,这个时候就会出现打印的bad file diesriptor
啦.
有一点我们是要注意的,共享的资源会导致竞争状态的产生,考虑这样一种极端情况,Pthread_create
函数之后,子进程并没有得到cpu
的运行时间,而主线程一直在运行,很快,主线程获得了一个新的连接,connfd
的值被新的文件描述符的值填充了,这个时候子线程才开始运行,子线程获得的值是新的connfd
的值吗?
所以,正确的代码示例是这样的:
/*-
* 多线程版本的web server.
*/
void* handle(void* arg)
{
Pthread_detach(pthread_self());
int fd = (*(int *)arg);
Free(arg); /* 防止内存泄露,释放 */
printf("%d: fd = %d\n", pthread_self(), fd);
doit(fd);
printf("%d: close fd = %d\n", pthread_self(), fd);
close(fd);
}
int main(int argc, char *argv[])
{
int listenfd = Open_listenfd(8080); /* 8080号端口监听 */
signal(SIGPIPE, SIG_IGN);
int connfd;
while (true) /* 无限循环 */
{
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int *fdp = (int *)Malloc(sizeof(int)); /* 重新分配一块地址 */
*fdp = Accept(listenfd, (SA*)&clientaddr, &len);
pthread_t tid;
Pthread_create(&tid, NULL, handle, (void *)fdp);
//close(connfd);
}
return 0;
}
存在的小bug
这里的signal(SIGPIPE, SIG_IGN);
其实对于线程来说并没有起作用,以后的版本会改进.
当然,这种模式效率依旧不是很高.