并发程序设计4:多线程
上一节实现了基于epoll的IO复用并发程序控制,本节记录基于多线程的并发程序设计。
1. 线程和进程
进程是具有独立功能的程序关于某个数据集合的一次运行活动,是系统资源管理资源分配的基本单位,而线程是进程中代码的一个执行流,是系统调度的基本单位(虽然这句话很常见,但是就用户级线程和内核级线程而言似乎不同,后面会讲到)。同一进程内的线程除了有自己的空间外,还共享进程的资源。线程和进程的区别如图1.1所示。
(a) 单线程 (b) 多线程
图1.1 进程与线程
可以看到,进程创建时,系统为其分配地址空间,进程有自己的代码段,数据段,堆栈。而线程在创建时,共享进程地址空间,同时维护自己的堆栈。再细分一下,线程可以分为用户级线程(User Level Thread, ULT)和内核级线程(Kernel Level Thread, KLT)。
1.1 KLT和ULT(PS: 此部分仅是操作系统中的概念,用户编程时均是多线程处理)
不同的操作系统实现线程的方式略有不同,有的操作系统直接支持线程(KLT);而有的操作系统感知不到线程的存在,其线程实现是通过用户空间以库函数来管理实现的(ULT)。
KLT管理工作由OS来执行,此时OS直接调度线程,进程不再具有多状态(只有挂起和非挂起)。主要特点有:
(1) 进程中一个线程被阻塞,OS调用该进程中其他线程来执行;
(2) 多处理器环境下,内核调用多个线程并行执行(物理并行);
(3) 应用程序在用户模式下进行,线程调度在内核模式下进行。线程调度要模式切换,系统开销大。
ULT多线程中内核没有意识到线程存在。多线程通过用户空间内的线程库实现。主要特点有:
(1) 线程管理的数据结构在用户空间中,调度不需要模式切换;
(2) 线程调度算法可裁剪和选择(因为是用户库函数来实现的);
(3) 不能利用多处理器优势,OS调用进程,进程中只有一个线程执行。一个线程阻塞,导致整个进程阻塞。
为了解决ULT的线程阻塞导致进程阻塞问题,引入了jacketing技术,Jacketing技术可以在线程阻塞时是否进行进程切换或转移控制权给其他线程。
此外,还有第三混合式多线程。混合式多线程在用户空间创建多线程ULT(用户创建),同时内核空间创建多线程KLT。将多个ULT映射到(小于等于ULLT的数目)KLT当中。程序员可以调整KLT的数目。KLT的调度由OS负责,ULT的三态由用户调度,并将活跃态的ULT绑定到KLT上。三种方式的对比如图1.2所示。
图1.2 三种多线程的对比
(注:图引自https://www.cnblogs.com/Mered1th/p/10745137.html)
2. Linux下多线程的实现
Linux下多线程实现主要用到了pthread.h库函数,主要函数如下:
int pthread_create(pthread_t* restrict thread, attr, void* (*start_routine)(void *),void* restrict arg); thread:用于保存线程ID的变量; attr:属性,设为NULL; start_routine:线程开始函数,返回值和输入参数都是void指针; arg:传递给线程的参数 为了使创建的线程结束之后自动销毁,有两种方式: (1) 由创建线程的进程调用 int pthread_join(pthread_t thread, void **status); thread:等待的线程ID status:返回给进程的参数 (2) 线程结束时调用 int pthread_detach(pthread_t thread);
方式很简单,我们仍然以回声服务器服务器端的代码为例,将其变成多线程并发,代码如下:
1 #include <stdlib.h> 2 #include <stdio.h> 3 #include <string.h> 4 #include <sys/socket.h> 5 #include <arpa/inet.h> 6 #include <unistd.h> 7 #include <pthread.h> 8 9 void error_handle(const char* msg) 10 { 11 fputs(msg,stderr); 12 fputc('\n',stderr); 13 exit(1); 14 } 15 void* client_thread(void* arg); //线程处理函数 16 17 int main(int argc,char* argv[]) 18 { 19 //服务器建立连接 20 int servsock,clntsock; 21 struct sockaddr_in servaddr,clntaddr; 22 char message[50]; 23 socklen_t clntlen=sizeof(clntaddr);; 24 25 if(argc!=2) 26 error_handle("Please input port number"); 27 28 servsock=socket(PF_INET,SOCK_STREAM,0); //1.建立套接字 29 30 memset(&servaddr,0,sizeof(servaddr)); 31 servaddr.sin_family=AF_INET; 32 servaddr.sin_addr.s_addr=htonl(INADDR_ANY); //默认本机IP地址 33 servaddr.sin_port=htons(atoi(argv[1])); 34 35 if(bind(servsock,(struct sockaddr*)&servaddr,sizeof(servaddr))==-1) 36 error_handle("bind error"); //2.建立连接 37 38 if(listen(servsock,10)==-1) //3.监听建立 39 error_handle("listen() error"); 40 41 //到此的代码为socket创建过程,与前三节相同 42 pthread_t t_id; //保存线程ID 43 while(1) 44 { 45 clntsock=accept(servsock,(struct sockaddr*)&clntaddr,&clntlen); //建立客户端连接 46 if(clntsock==-1) 47 { 48 printf("accept() error"); 49 close(clntsock); 50 } 51 else{ 52 pthread_create(&t_id,NULL,client_thread,(void*)&clntsock); 53 pthread_detach(t_id); 54 printf("connecting"); 55 } 56 } 57 58 } 59 60 void* client_thread(void* arg) 61 { 62 char message[30]; 63 int sock=*((int*)arg); 64 int strlen; 65 while(1) 66 { 67 strlen=read(sock,message,sizeof(message)); 68 if(strlen==0) //断开连接 69 { 70 close(sock); 71 break; 72 } 73 if(strlen>0) 74 write(sock,message,strlen); 75 } 76 77 }
ps:Linux下在编译该代码时,应加上线程库,如上面的代码文件名为server.cpp。那么编译命令为:g++ server.cpp -g -o -lpthread