1. 线程
传统Unix模型中,当一个进程需要另一个实体来完成某事,它就fork一个子进程来处理。Unix上大多数网络服务器程序便是以创建多个子进程的方式实现的:父进程accept一个连接,fork一个子进程,该子进程处理与该连接对端的客户之间的通信。
尽管,这种范式多年来一直用的不错,但是fork调用依然存在一些问题:
(1)fork是昂贵的,fork要把父进程的内存影像复制到子进程,并在子进程中复制所有描述符,等等。当今的实现使用写时复制技术,用于避免在子进程切实需要自己的副本之前把父进程的数据空间复制到子进程。然而,即使有这样的优势,fork仍然是昂贵的;
(2)fork返回之后,父子进程之间信息的传递需要进程间通信(IPC)机制。调用fork之前父进程向尚未存在的子进程传递信息相当容易,因为子进程将从父进程数据空间及所有描述符的一个副本开始进程,然而从子进程向父进程返回信息缺非常吃力;
针对以上的问题,线程有助于解决,线程也称为轻量级进程。
1.1 线程共享信息
同一进程内的所有线程共享以下信息:
- 相同的全局内存,包含全局变量
- 进程指令
- 大多数数据
- 打开的文件(即描述符)
- 信号处理函数和信号处置
- 当前工作目录
- 用户ID和组ID
1.2 线程独立信息
- 线程ID
- 寄存器集合,包括程序计数器和栈指针
- 栈(用于存放局部变量和返回地址)
- errno
- 信号掩码
- 优先级
本文总结于《Unix网络编程 -- 卷一》第26章,讲解的为POSIX线程也称为pthread。
2. 基本线程函数
Pthreads API在ANSI/IEEE POSIX 1003.1 – 1995标准中定义。不像MPI,该标准不是免费的,必须向IEEE购买。
Pthreads API中的函数可以非正式的划分为三大类:
- 线程管理(Thread management): 第一类函数直接用于线程:创建(creating),分离(detaching),连接(joining)等等。包含了用于设置和查询线程属性(可连接,调度属性等)的函数。
- 互斥量(Mutexes): 第二类函数是用于线程同步的,称为互斥量(mutexes),是"mutual exclusion"的缩写。Mutex函数提供了创建,销毁,锁定和解锁互斥量的功能。同时还包括了一些用于设定或修改互斥量属性的函数。
- 条件变量(Condition variables):第三类函数处理共享一个互斥量的线程间的通信,基于程序员指定的条件。这类函数包括指定的条件变量的创建,销毁,等待和受信(signal)。设置查询条件变量属性的函数也包含其中。
2.1 线程创建 -- pthread_create函数
最初,main函数包含了一个缺省的线程。其它线程则需要程序员显式地创建。 pthread_create 创建一个新线程并使之运行起来。该函数可以在程序的任何地方调用。
#include <pthread.h> int pthread_create(pthread_t *tid , const pthread_attr_t *attr , void *(*func)(void *) , void *arg); <span style="white-space:pre"> </span>返回:若成功则为0 , 若出错则为正的Exxx值
pthread_create参数:
- tid:返回一个不透明的,唯一的新线程标识符。
- attr:不透明的线程属性对象。可以指定一个线程属性对象,或者NULL为缺省值。
- func:线程将会执行一次的C函数。
- arg: 传递给func单个参数,传递时必须转换成指向void的指针类型。没有参数传递时,可设置为NULL。
注:
Q:一个线程被创建后,怎么知道操作系统何时调度该线程使之运行?
A:除非使用了Pthreads的调度机制,否则线程何时何地被执行取决于操作系统的实现。强壮的程序应该不依赖于线程执行的顺序。
A:除非使用了Pthreads的调度机制,否则线程何时何地被执行取决于操作系统的实现。强壮的程序应该不依赖于线程执行的顺序。
2.2 线程汇合 -- pthread_join函数
我们可以通过调用pthread_join等待一个给定线程终止。对比线程和Unix进程,pthread_create类似于fork,pthread_join类似于waitpid。
#include <pthread.h> int pthread_join(pthead_t *tid , void **status); 返回:若成功则为0,若出错则为正的Exxx值
我们必须指定要等待线程的tid,Pthread没办法等待任意一个线程(类似指定waitpid进程Id参数-1)。
如果status指针非空,来自所等待线程的返回值(一个指向某个对象的指针)将存入由status指向的位置。
2.3 线程分离 -- pthread_detach函数
一个线程是可汇合的(joinable默认值)或者是脱离的(detached)。当一个可汇合的线程终止时,它的线程ID和退出状态将留存到另一个线程对它调用pthread_join。脱离的线程却像守护进程,当它们终止时,所有相关资源都被释放,我们不能等待它们终止。如果一个线程需要知道另一个线程什么时候终止,那就最好保持第二个线程的可汇合状态。
pthread_detach函数将指定的线程转变为脱离状态。
#include <pthread.h> int pthread_detach(pthead_t *tid); 返回:若成功则为0,若出错则为正的Exxx值
如果一个线程想让自身脱离,则调用:pthread_detach(pthread_self());
注:线程汇合与分离
(1)连接: “连接”是一种在线程间完成同步的方法。
- pthread_join()函数阻赛调用线程知道threadid所指定的线程终止。
- 如果在目标线程中调用pthread_exit(),程序员可以在主线程中获得目标线程的终止状态。
- 连接线程只能用pthread_join()连接一次。若多次调用就会发生逻辑错误。
- 两种同步方法,互斥量(mutexes)和条件变量(condition variables)。
当一个线程被创建,它有一个属性定义了它是可连接的(joinable)还是分离的(detached)。只有是可连接的线程才能被连接(joined),若果创建的线程是分离的,则不能连接。 POSIX标准的最终草案指定了线程必须创建成可连接的。然而,并非所有实现都遵循此约定。
使用pthread_create()的attr参数可以显式的创建可连接或分离的线程,典型四步如下:
- 声明一个pthread_attr_t数据类型的线程属性变量
- 用 pthread_attr_init()初始化改属性变量
- 用pthread_attr_setdetachstate()设置可分离状态属性
- 完了后,用pthread_attr_destroy()释放属性所占用的库资源
- pthread_detach()可以显式用于分离线程,尽管创建时是可连接的。
- 没有与pthread_detach()功能相反的函数 。
(4)建议:
- 若线程需要连接,考虑创建时显式设置为可连接的。因为并非所有创建线程的实现都是将线程创建为可连接的。
- 若事先知道线程从不需要连接,考虑创建线程时将其设置为可分离状态。一些系统资源可能需要释放。
2.4 线程ID -- pthread_self函数
每一个线程都有一个所属进程内标识自身的ID。线程ID由pthread_create返回,pthread_join使用它,而函数pthread_self则是获取自身的线程ID。
#include <pthread.h> int pthread_self(void); 返回:调用线程的ID
2.5 线程退出 -- pthread_exit函数
让一个线程终止的方法之一是调用pthread_exit函数:
#include <pthread.h> void pthread_self(void *status); 不返回到调用者
如果本线程未曾脱离,它的线程ID和退出状态将一直留存到调用进程内的某个其它线程对它调用pthread_join。
指针status不能指向局部于调用线程的对象,因为线程终止时这样的对象也消失。
让一个线程终止的另外两个方法:
- 启动线程的函数(即pthread_create()的第三个参数)可以返回。既然该函数必须声明成返回一个void指针,它的返回值就是相应线程的终止状态。
- 如果进程的main函数返回或者任何线程调用了exit,整个进程就终止,其中包括它的任何线程。
3. 多线程TCP客户/服务器聊天程序实例
上面实例介绍了利用多线程技术实现的TCP客户/服务器回射程序实例,下面介绍利用多线程技术实现TCP客户端/服务器聊天小程序。
该程序实现客户端与服务器端通信,当任意一端发送退出指令exit时,程序结束。
3.1 config.h
1 /* 2 * config.h 包含该tcp/ip套接字编程所需要的基本头文件,与server.c client.c位于同一目录下 3 */ 4 5 #include <stdio.h> 6 #include <stdlib.h> 7 #include <string.h> 8 #include <sys/socket.h> 9 #include <sys/types.h> 10 #include <unistd.h> 11 #include <errno.h> 12 #include <netinet/in.h> 13 #include <netdb.h> 14 #include <arpa/inet.h> 15 #include <pthread.h> 16 17 const int MAX_LINE = 2048; 18 const int PORT = 6001; 19 const int BACKLOG = 10; 20 const int LISTENQ = 6666; 21 const int MAX_CONNECT = 20;
3.2 server.c
1 /* 2 * 服务器端代码实现 3 */ 4 5 #include "config.h" 6 7 /*处理接收客户端消息函数*/ 8 void *recv_message(void *fd) 9 { 10 int sockfd = *(int *)fd; 11 while(1) 12 { 13 char buf[MAX_LINE]; 14 memset(buf , 0 , MAX_LINE); 15 int n; 16 if((n = recv(sockfd , buf , MAX_LINE , 0)) == -1) 17 { 18 perror("recv error.\n"); 19 exit(1); 20 }//if 21 buf[n] = '\0'; 22 //若收到的是exit字符,则代表退出通信 23 if(strcmp(buf , "byebye.") == 0) 24 { 25 printf("Client closed.\n"); 26 close(sockfd); 27 exit(1); 28 }//if 29 30 printf("\nClient: %s\n", buf); 31 }//while 32 } 33 34 int main() 35 { 36 37 //声明套接字 38 int listenfd , connfd; 39 socklen_t clilen; 40 //声明线程ID 41 pthread_t recv_tid , send_tid; 42 43 //定义地址结构 44 struct sockaddr_in servaddr , cliaddr; 45 46 /*(1) 创建套接字*/ 47 if((listenfd = socket(AF_INET , SOCK_STREAM , 0)) == -1) 48 { 49 perror("socket error.\n"); 50 exit(1); 51 }//if 52 53 /*(2) 初始化地址结构*/ 54 bzero(&servaddr , sizeof(servaddr)); 55 servaddr.sin_family = AF_INET; 56 servaddr.sin_addr.s_addr = htonl(INADDR_ANY); 57 servaddr.sin_port = htons(PORT); 58 59 /*(3) 绑定套接字和端口*/ 60 if(bind(listenfd , (struct sockaddr *)&servaddr , sizeof(servaddr)) < 0) 61 { 62 perror("bind error.\n"); 63 exit(1); 64 }//if 65 66 /*(4) 监听*/ 67 if(listen(listenfd , LISTENQ) < 0) 68 { 69 perror("listen error.\n"); 70 exit(1); 71 }//if 72 73 /*(5) 接受客户请求,并创建线程处理*/ 74 75 clilen = sizeof(cliaddr); 76 if((connfd = accept(listenfd , (struct sockaddr *)&cliaddr , &clilen)) < 0) 77 { 78 perror("accept error.\n"); 79 exit(1); 80 }//if 81 82 printf("server: got connection from %s\n", inet_ntoa(cliaddr.sin_addr)); 83 84 /*创建子线程处理该客户链接接收消息*/ 85 if(pthread_create(&recv_tid , NULL , recv_message, &connfd) == -1) 86 { 87 perror("pthread create error.\n"); 88 exit(1); 89 }//if 90 91 /*处理服务器发送消息*/ 92 char msg[MAX_LINE]; 93 memset(msg , 0 , MAX_LINE); 94 while(fgets(msg , MAX_LINE , stdin) != NULL) 95 { 96 if(strcmp(msg , "exit\n") == 0) 97 { 98 printf("byebye.\n"); 99 memset(msg , 0 , MAX_LINE); 100 strcpy(msg , "byebye."); 101 send(connfd , msg , strlen(msg) , 0); 102 close(connfd); 103 exit(0); 104 }//if 105 106 if(send(connfd , msg , strlen(msg) , 0) == -1) 107 { 108 perror("send error.\n"); 109 exit(1); 110 }//if 111 }//while 112 }
3.3 client.c
1 /* 2 * 客户端代码 3 */ 4 #include "config.h" 5 6 /*处理接收服务器消息函数*/ 7 void *recv_message(void *fd) 8 { 9 int sockfd = *(int *)fd; 10 while(1) 11 { 12 char buf[MAX_LINE]; 13 memset(buf , 0 , MAX_LINE); 14 int n; 15 if((n = recv(sockfd , buf , MAX_LINE , 0)) == -1) 16 { 17 perror("recv error.\n"); 18 exit(1); 19 }//if 20 buf[n] = '\0'; 21 22 //若收到的是exit字符,则代表退出通信 23 if(strcmp(buf , "byebye.") == 0) 24 { 25 printf("Server is closed.\n"); 26 close(sockfd); 27 exit(0); 28 }//if 29 30 printf("\nServer: %s\n", buf); 31 }//while 32 } 33 34 35 int main(int argc , char **argv) 36 { 37 /*声明套接字和链接服务器地址*/ 38 int sockfd; 39 pthread_t recv_tid , send_tid; 40 struct sockaddr_in servaddr; 41 42 /*判断是否为合法输入*/ 43 if(argc != 2) 44 { 45 perror("usage:tcpcli <IPaddress>"); 46 exit(1); 47 }//if 48 49 /*(1) 创建套接字*/ 50 if((sockfd = socket(AF_INET , SOCK_STREAM , 0)) == -1) 51 { 52 perror("socket error"); 53 exit(1); 54 }//if 55 56 /*(2) 设置链接服务器地址结构*/ 57 bzero(&servaddr , sizeof(servaddr)); 58 servaddr.sin_family = AF_INET; 59 servaddr.sin_port = htons(PORT); 60 if(inet_pton(AF_INET , argv[1] , &servaddr.sin_addr) < 0) 61 { 62 printf("inet_pton error for %s\n",argv[1]); 63 exit(1); 64 }//if 65 66 /*(3) 发送链接服务器请求*/ 67 if( connect(sockfd , (struct sockaddr *)&servaddr , sizeof(servaddr)) < 0) 68 { 69 perror("connect error"); 70 exit(1); 71 }//if 72 73 /*创建子线程处理该客户链接接收消息*/ 74 if(pthread_create(&recv_tid , NULL , recv_message, &sockfd) == -1) 75 { 76 perror("pthread create error.\n"); 77 exit(1); 78 }//if 79 80 /*处理客户端发送消息*/ 81 char msg[MAX_LINE]; 82 memset(msg , 0 , MAX_LINE); 83 while(fgets(msg , MAX_LINE , stdin) != NULL) 84 { 85 if(strcmp(msg , "exit\n") == 0) 86 { 87 printf("byebye.\n"); 88 memset(msg , 0 , MAX_LINE); 89 strcpy(msg , "byebye."); 90 send(sockfd , msg , strlen(msg) , 0); 91 close(sockfd); 92 exit(0); 93 }//if 94 if(send(sockfd , msg , strlen(msg) , 0) == -1) 95 { 96 perror("send error.\n"); 97 exit(1); 98 }//if 99 100 101 }//while 102 }
4.4 运行结果
服务器端: