前置知识:
多线程知识
Socket编程
多线程并发服务器思路
- lfd=socket(),创建监听套接字 lfd。
- setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt, sizeof(opt)),设置端口复用。
- bind(),绑定监听套接字lfd与Strcut scokaddr_in srv_addr(服务器IP和端口)。
- listen(),把lfd转换成一个被动套接字,进入被动监听状态,用来被动接受来自其他主动套接字的连接请求,并设置监听上限。
- 主线程
while(1){ //死循环运行服务端
cfd=accept() //阻塞监听客户端的连接请求,接收客户端的连接
pthread_create() //创建线程,子线程去执行与客户端的通信
pthread_detach() //实现线程分离,回收子线程
//close(cfd) 不可以关闭cfd,这关了子线程的也没了
}
- 子线程
while(1){
//close(lfd) 不可以关闭lfd,主线程还要用
read() //读客户端数据
//处理数据
write() //写回数据
pthread_exit() //线程退出,可以指定返回值
}
10个问题
1 为什么设置线程分离?
主线程与子线程分离,子线程结束后,资源自动回收,防止僵尸线程。
2 为什么是pthread_detach(),而不是pthread_join()?
使用pthread_create创建的线程有两种状态:joinable和unjoinable。默认是joinable 状态。
- 如果是可结合的joinable状态,则该线程结束后,不会释放资源,等到pthread_join()函数调用后才会释放资源。pthread_join()会阻塞主线程,直到要回收的子线程结束。
- 如果是分离的unjoinable(detached)状态,则该进程结束后会自动释放占用资源。而这种状态就是通过设置线程分离获得的,主线程与子线程分离,子线程结束后,资源自动回收,防止僵尸线程。pthread_detach(),不会阻塞,调用它后,线程运行结束后会自动释放资源。
在服务器中,当主线程监听并连接到新的客户端,创建子线程通过cfd与客户端通信时,主线程并不希望因为调用pthread_join而阻塞,因为主线程还要继续去执行阻塞监听。你pthread_join把我阻塞在这里算怎么回事。
3 pthread_join和pthread_detach()的应用场景?
pthread_detach()和pthread_join()就是控制子线程回收资源的两种不同的方式。
pthread_join()函数是一个阻塞函数,调用方会阻塞到pthread_join所指定的tid的线程结束后才被回收 ,一般应用在主线程需要等待子线程结束后才继续执行的场景。(子线程合入主线程,主线程会一直阻塞,直到子线程执行结束,然后回收子线程资源,并继续执行。)
pthread_detach()函数不会阻塞,调用它后,使得主线程与子线程分离,两者相互不干涉,子线程结束同时子线程的资源自动回收释放资源,非常方便。
4 设置线程分离的3种方式?
- 在创建时指定属性
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&tid, &attr, childthread_work, (void *)cfd);
- 在子线程执行体的最开始处添加一行
也就是说,添加在do_work函数的第一行。
pthread_detach(pthread_self());
- 直接主线程主动调用,在pthread_create()后添加一行
pthread_t tid;
ret= pthread_create(&tid,NULL,childthread_work, (void *)cfd);
if(ret==-1){
sys_err("pthread_create error");
}
pthread_detach(tid); //非阻塞,可立即返回
5 设置线程分离的3种方式?
由于pthread库不是Linux系统默认的库,连接时需要使用库libpthread.a,所以在使用pthread_create创建线程时,在编译中要加-lpthread。
比如:gcc multhreadserver.c wrap.c -o multhreadserver -lpthread
6 pthread_exit()和return的区别?
return 的含义是返回,它不仅可以用于线程执行的函数,普通函数也可以使用;pthread_exit() 函数的含义是线程退出,它专门用于结束某个线程的执行。
在子线程中,当执行结束, return 和 pthread_exit() 都可以给返回值到主线程,主线程中的 pthread_join() 函数都可以接收到线程的返回值。
- pthread_exit()用于线程退出,只退出当前子线程,可以指定返回值,以便其他线程通过pthread_join()函数获取该线程的返回值。注意:如果在主线程中使用 pthread_exit()退出,可以达到主进程退出子线程继续运行的目的。
- return返回到调用者那里去。注意,在主线程退出时效果与exit,_exit一样。因为主进程执行完return之后,实际上经过编译器代码优化,会调用exit()函数,该函数除了执行关闭IO等操作之外,还会执行关掉其他子线程的操作。
- exit()是进程退出,无论哪个子线程调用,会导致该线程所在进程的其他线程也挂掉,则整个程序都将结束。
7 要想让子线程总能完整执行(不会中途退出)的三种方法?
- 在主线程中调用pthread_join对其等待,即pthread_create/pthread_join/pthread_exit或return;
- 在主线程退出时使用pthread_exit,这样子线程能继续执行,即pthread_create/pthread_detach/pthread_exit;
- 主线程是死循环,那么就要pthread_create/pthread_detach。
8 多线程错误返回值分析
所有线程的错误号返回都只能使用strerror这个函数判断,不能使用perror,因为perror是调用进程的全局错误号,不适合单独线程的错误分析,所以只能使用strerror。
比如:fprintf(stderr, “xxx error: %s\n”, strerror(ret));
9 pthreat_create()参数传递?
- 传值。(void *)arg
while(1){
cfd= Accept(lfd,(struct sockaddr*)&clt_addr,&clt_addr_len);
ret= pthread_create(&tid,NULL, childthread_work,(void *)cfd);
}
- 传动态指针,每次传的指针都是新malloc的。
for (int i=0; i<10; ++i)
{
int *p = malloc(sizeof(*p));
*p = i;
if ((ret=pthread_create(&pid[i],NULL,thread,(void*)p)) != 0)
{
fprintf(stderr,"pthread_create:%s\n",strerror(ret));
exit(1);
}
}
- 注意不可以直接传变化值的指针,多个线程之间存在竞争,线程函数中对arg的使用,其实多个arg指针都指向了同一片内存,如果修改,其他线程里的arg值也会改变。上述两种方法不存在竞争的原因是,一个指针指向一个变量。
总之: 不能在线程创建过程中,改变传递的参数,避免该问题产生的方法是传递值或者使用动态申请内存的方法。
10 为什么主线程不可以关闭cfd,子线程不可以关闭lfd?
同一进程间的线程具有共享和独立的资源,
其中共享的资源有进程代码段、进程的公有数据(利用这些数据,线程很容易实现相互之间的通讯),进程的所拥有资源。详细说:
0、进程代码段
1、进程申请的堆内存
2、进程打开的文件描述符
3、进程的全局数据(可用于线程之间通信)
4、进程ID、进程组ID
5、进程目录
6、信号处理器。
而独占资源有:
1、线程ID
2、寄存器组的值
3、线程堆栈
4、错误返回码
5、信号屏蔽码
6、线程的优先级
因为多线程共享进程打开的文件描述符,与多进程对比,主线程是不可以关闭cfd的,因为子线程并没有把fd表复制过来。如果关闭了cfd,就相当于释放掉了套接字,那么后续子线程就不能进行读写了。同理,lfd也是这样。
代码实现:
提示:多返回值传出的方式,来自ChernoCppTutorial的笔记:1、传引用或者指针,即函数设置多个传出参数。2、直接返回一个数组。当然这不通用,因为必须要同一种类型。当然还能写为vector,不过array会在栈上创建,而vector会把它的底层存储在堆上,所以从技术上来讲返回std::array会更快。3、tuple或pair。4、定义一个结构体,然后返回。
//server.c,需要和wrap.c一起gcc
// Created on 2022/5/22.
//
#include <string.h>
#include <strings.h>
#include<netinet/in.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <pthread.h>
#include <signal.h>
#include <sys/wait.h>
#include <fcntl.h>
#include "wrap.h"
#define IP "127.44.44.44"
#define PORT 6266
struct s_info { //定义一个结构体, 将地址结构跟 cfd 捆绑
struct sockaddr_in cliaddr;
int connfd;
};
void *do_work(void *s_in) {
struct s_info *client_info = (struct s_info *) s_in;
int cfd = (*client_info).connfd;
int ret;
char buf[BUFSIZ], clie_ip[BUFSIZ];
while (1) {
ret = read(cfd, buf, sizeof(buf));
if (ret == 0) {
close(cfd);
break;
} else if (ret == -1) {
sys_err("read error");
} else {
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &(*client_info).cliaddr.sin_addr.s_addr, clie_ip, sizeof(clie_ip)),
ntohs((*client_info).cliaddr.sin_port));
write(STDOUT_FILENO, buf, ret);
for (int i = 0; i < ret; i++) {
buf[i] = toupper(buf[i]);
}
write(cfd, buf, ret);
}
}
close(cfd);
return (void*)0;
}
int main(int argc,char *argv[]){
int lfd,cfd;
int i=0;
int ret;
char buf[BUFSIZ];
pthread_t tid;
struct sockaddr_in srv_addr,clt_addr;
socklen_t clt_addr_len;
struct s_info s_info_array[256];
//memset(&saddr,0,sizeof(saddr));
bzero(&srv_addr,sizeof(srv_addr));
srv_addr.sin_family=AF_INET;
srv_addr.sin_port= htons(PORT);
srv_addr.sin_addr.s_addr= htonl(INADDR_ANY);
lfd= Socket(AF_INET,SOCK_STREAM,0);
Bind(lfd,(struct sockaddr *)&srv_addr, sizeof(srv_addr));
Listen(lfd,128);
while(1){
clt_addr_len = sizeof(clt_addr_len);
cfd= Accept(lfd,(struct sockaddr*)&clt_addr,&clt_addr_len);
s_info_array[i].cliaddr = clt_addr;
s_info_array[i].connfd = cfd;
ret= pthread_create(&tid,NULL,do_work,(void *)&s_info_array[i]);
if(ret==-1){
sys_err("pthread_create error");
}
pthread_detach(tid);
i++;
}
return 0;
}