## 多线程并发服务器 ### 进程和线程的基本概念 #### 进程 进程是程序的一次执行过程,是操作系统资源分配的基本单位。 比如我们在上一个实验中用 `./server` 命令运行服务器程序,就会产生一个进程,可以使用 `ps -ef|grep ./server` 命令查看相关进程快照,如下: ![图片描述](https://typoraflykhan.oss-cn-beijing.aliyuncs.com/202304011939279.png) PID 为 120 即该进程的进程号为 120。 #### 线程 线程是任务调度和执行的基本单位,一个进程中可以有多个线程独立运行。线程没有自己独立的地址空间,会与其它属于同一进程的线程一起共享进程的资源,但是每个线程也会有自己的独立的栈和一组寄存器。在 Linux 当中,线程的实现比较特别,会把线程当做进程来实现,即将线程视为一个与其它进程共享资源的进程。 我们可以使用 ps -T -p 命令来查看一个进程的所有线程,如下( 359 是进程号): ![图片描述](https://typoraflykhan.oss-cn-beijing.aliyuncs.com/202304011939325.png) ##### 通俗理解线程进程和协程 ![image-20230401194319882](https://typoraflykhan.oss-cn-beijing.aliyuncs.com/202304011943949.png) ### C++ 11 的 thread 线程库 C++ 11 中提供了专门的线程库,可以很方便地进行调用。 #### 基本使用 需要引入头文件: ```cpp #include ``` 创建一个新线程来执行 run 函数: ```cpp thread t(run); //实例化一个线程对象t,让该线程执行run函数,构造对象后线程就开始执行了 ``` 假如说 run 函数需要传入参数 a 和 b,我们可以这样构造: ```cpp thread t(run,a,b); //实例化一个线程对象t,让该线程执行run函数,传入a和b作为run的参数 ``` 需要注意的是,传入的函数必须是**全局函数或者静态函数**,不能是类的普通成员函数。 join 函数会阻塞主线程,直到 join 函数的 thread 对象标识的线程执行完毕为止,join 函数使用方法如下: ```cpp t.join(); //调用join后,主线程会一直阻塞,直到子线程的run函数执行完毕 ``` 但有时候我们需要主线程在继续完成其它的任务,而不是一直等待子线程结束,这时候我们可以使用 detach 函数。 detach 函数会让子线程变为分离状态,主线程不会再阻塞等待子线程结束,而是让系统在子线程结束时自动回收资源。使用的方法如下: ```cpp t.detach(); ``` #### 代码示例 接下来用一段代码来示范如何进行简单的多线程编程,我们构建两个线程,同时输出 1-10,如下: 新建一个文件名为 `test_thread.cpp`: ```cpp #include #include #include using namespace std; void print(){ for(int i=1;i<=10;i++){ cout< #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; class server{ private: int server_port;//服务器端口号 int server_sockfd;//设为listen状态的套接字描述符 string server_ip;//服务器ip vector sock_arr;//保存所有套接字描述符 public: server(int port,string ip);//构造函数 ~server();//析构函数 void run();//服务器开始服务 static void RecvMsg(int conn);//子线程工作的静态函数 }; #endif ``` 接下来我们需要在 `server.cpp` 文件中给出函数具体的定义: ```cpp #include "server.h" //构造函数 server::server(int port,string ip):server_port(port),server_ip(ip){} //析构函数 server::~server(){ for(auto conn:sock_arr) close(conn); close(server_sockfd); } //服务器开始服务 void server::run(){ //定义sockfd server_sockfd = socket(AF_INET,SOCK_STREAM, 0); //定义sockaddr_in struct sockaddr_in server_sockaddr; server_sockaddr.sin_family = AF_INET;//TCP/IP协议族 server_sockaddr.sin_port = htons(server_port);//server_port;//端口号 server_sockaddr.sin_addr.s_addr = inet_addr(server_ip.c_str());//ip地址,127.0.0.1是环回地址,相当于本机ip //bind,成功返回0,出错返回-1 if(bind(server_sockfd,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr))==-1) { perror("bind");//输出错误原因 exit(1);//结束程序 } //listen,成功返回0,出错返回-1 if(listen(server_sockfd,20) == -1) { perror("listen");//输出错误原因 exit(1);//结束程序 } //客户端套接字 struct sockaddr_in client_addr; socklen_t length = sizeof(client_addr); //不断取出新连接并创建子线程为其服务 while(1){ int conn = accept(server_sockfd, (struct sockaddr*)&client_addr, &length); if(conn<0) { perror("connect");//输出错误原因 exit(1);//结束程序 } cout<<"文件描述符为"< 的探究 在 C++ 中,用于包含头文件的指令是 `#include`。在使用 `#include` 指令时,可以使用尖括号 `< >` 或者双引号 `" "` 来引用头文件。 当使用尖括号 `< >` 时,编译器会在系统默认的头文件目录中查找该头文件;而当使用双引号 `" "` 时,编译器会先在当前源代码文件所在目录中查找该头文件,如果没有找到再到系统默认的头文件目录中查找。 因此,如果要引用系统库中的头文件,应该使用尖括号 `< >`;如果要引用自己编写的头文件,应该使用双引号 `" "`。 需要注意的是,如果在使用 `#include` 指令时使用了错误的符号,可能会导致编译器无法正确查找头文件,从而导致编译失败。因此,在编写代码时要注意使用正确的符号来引用头文件。 ## server代码解释 ### accept() ![image-20230401223027473](https://typoraflykhan.oss-cn-beijing.aliyuncs.com/202304012230588.png) ### 编译警告信息说明 ![image-20230401234038072](https://typoraflykhan.oss-cn-beijing.aliyuncs.com/202304012340170.png) ### makefile 编译链接命令解释 ![image-20230401234152788](https://typoraflykhan.oss-cn-beijing.aliyuncs.com/202304012341878.png) ![image-20230401234450436](https://typoraflykhan.oss-cn-beijing.aliyuncs.com/202304012344501.png)