理解线程的概念
多进程模型的缺点:
--创建进程的过程会带来一定的开销
--为了完成进程间数据交换,需要特殊的IPC技术
最大的缺点:
--每秒少则数十次,多则数前次的“上下文切换"是创建进程时最大的开销。
线程相比进程具有如下优点:
--线程的创建和上下文切换比进程的创建和上下文切换更快。
--线程间交换数据时无需特殊技术。
线程和进程的差异
数据区保存全局变量,堆区域向malloc等函数的动态分配提供空间,函数运行时使用的栈区域。每个进程都有独立空间。
如果以获得多个代码执行流为目的,那么不应该像上图那样完全分离内存结构,而只需分离栈区域。
这种方法的优势:
--上下文切换时不需要切换数据区和堆
--可以利用数据区和堆交换数据
实际上这就是线程!线程为了保持多条代码执行流而隔开了栈区域:
多个线程共享数据区和堆。
定义:
进程:在操作系统构成单独执行流的单位
线程:在进程构成单独执行流的单位ie
关系图:
线程的创建及运行
线程的创建和执行流程
示例:thread1.c
#include<stdio.h> #include<unistd.h> #include<pthread.h> void* thread_main(void *arg); int main(int argc, char *argv[]) { pthread_t t_id; int thread_param = 5; /* 创建一个线程,从thread_main函数调用开始,在单独执行流运行。同时向其传递thread_param变量的地址值 */ if (pthread_create(&t_id,NULL,thread_main,(void*)&thread_param) != 0) { puts("pthread_create() error"); return -1; }; sleep(10); //延迟进程的终止时间。保证线程的正常执行。 puts("end of main"); return 0; } void* thread_main(void *arg) //传入arg参数的是第四个参数thread_param { int i; int cnt = *((int*)arg); //arg值为5 for (i=0; i<cnt; i++) { sleep(1); puts("running thread"); } return NULL; }
编译命令:gcc -o thread1 thread1.c -lpthread
运行结果:
执行流程:
线程相关的程序中必须保证线程在进程销毁前执行完毕。
用sleep函数可能会干扰程序的正常执行流。因此,我们不用sleep函数。
使用下列函数:
调用该函数的进程或线程将进入等待状态,直到第一个参数为ID的线程终止为止。而且可以得到线程的返回值保存到status中
示例:thread2.c
#include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<string.h> #include<pthread.h> void* thread_main(void *arg); int main(int argc,char *argv[]) { pthread_t t_id; int thread_param = 5; void * thr_ret; if (pthread_create(&t_id, NULL, thread_main, (void*)&thread_param) != 0) { puts("pthread_create() error!"); return -1; }; if(pthread_join(t_id,&thr_ret) != 0) /*main函数将等待t_id中的进程终止 并将返回值保存到thr_ret中 */ { puts("pthread_join() error!"); return -1; }; printf("Thread return message: %s \n",(char*)thr_ret); free(thr_ret); return 0; } void* thread_main(void *arg) { int i; int cnt = *((int*)arg); //cnt值为5 char *msg = (char*)malloc(sizeof(char) * 50); strcpy(msg,"Hello,I'am thread~ \n"); for (i=0; i<cnt; i++) { sleep(1); puts("running thread"); } return (void*)msg; }
运行结果:
执行流程:
可在临界区内调用的函数
关于线程的运行需要考虑:多个线程同时调用函数时可能产生问题。
这类函数内部存在临界区,多个线程同时执行这部分代码时,可能会引起问题。
函数可分为2类:
--线程安全函数
--非线程安全函数
一般非线程安全函数都有相同功能的线程安全的函数。
通过在声明头文件前定义_REENTRANT宏自动将非线程安全函数改为线程安全函数。
也可以在编译时添加 -D-REENTRANT 选项定义宏。
工作(Worker)线程模型
示例计算1到10的和,创建两个线程,一个计算1-5的和,另一个计算6-10的和,main函数输出结果。
这种方式的编程模型称为“工作线程(Worker thread)模型。
执行流程图:
程序:thread3.c
#include<stdio.h> #include<pthread.h> void * thread_summation(void * arg); int sum = 0; int main(int argc, char *argv[]) { pthread_t id_t1, id_t2; int range1[] = {1,5}; int range2[] = {6,10}; pthread_create(&id_t1,NULL, thread_summation, (void*)range1); pthread_create(&id_t2,NULL, thread_summation, (void*)range2); pthread_join(id_t1,NULL); pthread_join(id_t2,NULL); printf("result: %d \n",sum); } void * thread_summation(void *arg) //此处*arg为数组 { int start = ((int*)arg)[0]; int end = ((int*)arg)[1]; while(start <= end) { sum += start; start++; } return NULL; }
两个线程直接访问全局变量sum。
运行结果:
结果正确,但存在临界区相关问题。
介绍另一示例。该示例与上述示例相似,只是增加了发生临界区相关错误的可能性。
示例:thread4.c
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<pthread.h> #define NUM_THREAD 100 void * thread_inc(void *arg); void * thread_des(void *arg); long long num=0; int main(int argc,char *argv[]) { pthread_t thread_id[NUM_THREAD]; int i; printf("sizeof long long: %ld \n",sizeof(long long)); for (i=0; i<NUM_THREAD; i++) { if(i%2) pthread_create(&(thread_id[i]),NULL,thread_inc,NULL); else pthread_create(&(thread_id[i]),NULL,thread_des,NULL); } for (i=0; i<NUM_THREAD; i++) pthread_join(thread_id[i],NULL); printf("result: %lld \n",num); return 0; } void * thread_inc(void * arg) { int i; for (i=0; i<50000000; i++) num += 1; return NULL; } void * thread_des(void * arg) { int i; for (i=0; i<50000000; i++) num -= 1; return NULL; }
运行结果:
运行结果不是0!说明出现了问题。
线程存在的问题和临界区
多个线程访问同一变量是问题
多个线程同时访问全局变量时,会发生问题。任何内存空间---只要被同时访问---都可能发生问题。
下面通过示例解释“同时访问”的含义,并说明为何会引起问题。假设2个线程要执行将变量值逐次加1的工作。
正常流程:
这是理想的情况。
特殊情况:
如果在线程1读取num值并完成加1运算时,只是加1的结果尚未写入变量num,此时执行流程跳转到线程2,完成加1动作写入,线程2将num值改成100,然后线程1将运算后的值写入变量num。此时会发现num的值还是100;
因此,线程访问变量num时应阻止其他线程访问,直到线程1完成运算。这就是同步。
临界区位置
临界区:函数内同时运行多个线程时引起问题的多条语句构成的语句块。
观察示例thread4.c中的2个main函数:
产生的问题整理为如下:
--2个线程同时执行thread_inc函数
--2个线程同时执行thread_des函数
--2个线程分别执行thread_inc函数和thread_des函数
线程同步
同步的两面性
线程同步解决线程访问顺序引发的问题,分为两个方面考虑:
--同时访问同一内存空间时发生的情况。
--需要指定访问同一内存空间的线程执行顺序的情况
互斥量
互斥量是"Mutual Exclusion"的简写,表示不允许多个线程同时访问。用于解决线程同步访问的问题。
可以把洗手间比作临界区,把这些事情套用到保护临界区的同步过程:
线程同步需要锁,互斥量就是一把优秀的锁。
互斥量的创建和销毁函数:
为了创建相当于锁系统的互斥量,需要声明如下pthread_mutex_t型变量:
pthread_mutex_t mutex;
该变量地址传给init函数,用来保存操作系统创建的互斥量(锁系统)。
若不需要配置特殊的互斥量属性,第二个参数为NULL,可以利用宏声明来替换init函数初始化:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIIALIZER;
互斥量锁住或释放临界区时使用的函数:
进入临界区前调用的函数就是pthread_mutex_lock.调用该函数时,发现其他线程已进入临界区,则pthread_mutex_lock函数不会返回,直到里面的线程调用pthread_mutex_unlock函数退出临界区为止。其它线程退出前,当前线程将一直处于阻塞状态。
创建好互斥量后,通过如下代码结构保护临界区:
pthread_mutex_lock(&mutex); //临界区的开始 //... //临界区结束 pthread_mutex_unlock(&mutex);
此时互斥量就相当于一把锁,阻止多个线程同时访问。
若线程退出临界区时,忘了调用pthread_mutex_lock函数,那么其它为了进入临界区而调用pthread_mutex_lock函数的线程就无法摆脱阻塞状态。这种情况称为死锁(Dead-lock)。
下面通过互斥量解决thread4.c的问题:mutex.c
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<pthread.h> #define NUM_THREAD 100 void * thread_inc(void *arg); void * thread_des(void *arg); long long num=0; pthread_mutex_t mutex; //保存互斥量读取值的变量。 int main(int argc,char *argv[]) { pthread_t thread_id[NUM_THREAD]; int i; pthread_mutex_init(&mutex,NULL); printf("sizeof long long: %ld \n",sizeof(long long)); for (i=0; i<NUM_THREAD; i++) { if(i%2) pthread_create(&(thread_id[i]),NULL,thread_inc,NULL); else pthread_create(&(thread_id[i]),NULL,thread_des,NULL); } for (i=0; i<NUM_THREAD; i++) pthread_join(thread_id[i],NULL); printf("result: %lld \n",num); pthread_mutex_destroy(&mutex); return 0; } void * thread_inc(void * arg) { int i; pthread_mutex_lock(&mutex); for (i=0; i<50000000; i++) num += 1; pthread_mutex_unlock(&mutex); return NULL; } void * thread_des(void * arg) { int i; for (i=0; i<50000000; i++) //调用50000000次互斥量lock,unlock函数 { pthread_mutex_lock(&mutex); num -= 1; pthread_mutex_unlock(&mutex); } return NULL; }
运行结果:
问题解决了,但是确认运行结果需要等待较长时间。
因为以上程序中,两个线程main函数的临界区划分范围不同,thread_inc临界区较大,最大限度减少了互斥量lock,unlock函数调用次数。而thread_des函数临界区临界区太小,调用了很多次lock,unlock。因此thread_inc的运算很快,thread_des运算很慢。
但若是临界区划分大,临界区运行完之前不允许其它线程访问(上例中是变量num的值增加到50000000前不允许其它线程访问),这反而又是缺点。
所以,需要根据不同程序酌情考虑究竟扩大还是缩小临界区。
信号量
此处只涉及利用“二进制信号量”(只用0和1)完成“控制线程顺序”为中心的同步方法。
信号量的创建和销毁函数:
pshared参数超出我们关注的范围,默认向其传递0.
信号量相当于互斥量lock,unlock的函数:
调用sem_init函数时,操作系统将创建信号量对象,此对象中记录着“信号量值”整数。该值在调用sem_post函数时增1,调用sem_wait函数时减1。信号量的值不能小于0,当信号量为0的情况下调用sem_wait函数时,调用函数的线程将进入阻塞状态。此时如果有其它线程调用sem_post函数,信号量的值将变为1,而原来阻塞的线程可以将该信号量重新减为0并跳出阻塞状态。
实际上就是通过这种特性完成临界区的同步操作,通过如下形式同步临界区:
sem_wait(&sem); //信号量变为0... //临界区的开始 //......... //临界区的结束 sem_post(&sem); //信号量变为1
上述代码中,调用sem_wait函数进入临界区的线程在调用sem_post函数前不允许其它线程进入临界区。
信号量的值在0和1之间跳转,因此,具有这种特性的机制称为”二进制信号量“。
示例:线程A从输入得到值后存入全局变量num,此时线程B将取走该值并累加。该过程共进行5次,完成后输出总和并退出
/* 控制访问顺序的线程同步 */ #include<stdio.h> #include<pthread.h> #include<semaphore.h> void * read(void * arg); void * accu(void * arg); static sem_t sem_one; static sem_t sem_two; static int num; int main(int argc, char *argv[]) { pthread_t id_t1,id_t2; sem_init(&sem_one,0,0); //sem_one初始值为0 sem_init(&sem_two,0,1); //sem_two初始值为1 pthread_create(&id_t1,NULL,read,NULL); pthread_create(&id_t2,NULL,accu,NULL); pthread_join(id_t1,NULL); pthread_join(id_t2,NULL); sem_destroy(&sem_one); sem_destroy(&sem_two); return 0; } void * read(void * arg) { int i; for(i=0; i<5; i++) { fputs("Input num: ",stdout); sem_wait(&sem_two); //sem_two变为0,阻塞,在accu中加1后跳出阻塞状态 scanf("%d",&num); sem_post(&sem_one); //sem_one变为1 } return NULL; } void * accu(void * arg) { int sum=0, i; for(i=0; i<5; i++) { sem_wait(&sem_one); //sem_one变为0,阻塞,在read中加1后跳出阻塞状态 sum+=num; sem_post(&sem_two); //sem_two变为1 } printf("Result: %d \n",sum); return NULL; }
15,16行生成两个信号量。掌握需要2个信号量的原因。
运行结果:
线程的销毁和多线程并发服务器端的实现
销毁线程的3中方法
线程并不是在调用线程main函数返回时自动销毁,用如下2中方法之一加以明确,否则线程创建的内存空间将一直存在:
调用pthread_join函数不仅会等待线程终止,还会引导线程销毁。问题是,线程终止前,调用该函数的进程将进入阻塞状态。
因此,通常通过如下函数调用引导线程销毁:
调用上述函数不会引起线程终止或进入阻塞状态,通过该函数引导销毁线程创建的内存空间。调用该函数后不能再针对相应线程调用pthread_join函数。
多线程并发服务器端的实现
介绍多个客户端之间可以交换信息的简单的聊天程序:
聊天服务器端:chat_server.c
/* 聊天服务器端chat_server.c */ #include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> #include<arpa/inet.h> #include<sys/socket.h> #include<pthread.h> #include<semaphore.h> #define BUF_SIZE 100 #define MAX_CLNT 256 void error_handling(char *message); void * handle_clnt(void * arg); void send_msg(char * msg,int len); int clnt_cnt = 0; //接入的客户端套接字的数量 int clnt_socks[MAX_CLNT]; //管理接入的客户端套接字的数组 pthread_mutex_t mutx; int main(int argc,char *argv[]) { int serv_sock, clnt_sock; struct sockaddr_in serv_adr, clnt_adr; socklen_t clnt_adr_sz; pthread_t t_id; if (argc != 2) { printf("Usage : %s <port> \n",argv[0]); exit(1); } pthread_mutex_init(&mutx,NULL); serv_sock = socket(PF_INET,SOCK_STREAM,0); if (serv_sock == -1) error_handling("socket() error!"); memset(&serv_adr,0,sizeof(serv_adr)); serv_adr.sin_family = AF_INET; serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); serv_adr.sin_port = htons(atoi(argv[1])); if (bind(serv_sock,(struct sockaddr*) &serv_adr,sizeof(serv_adr)) == -1) error_handling("bind() error!"); if (listen(serv_sock,5) == -1) error_handling("listen() error!"); while(1) { clnt_adr_sz = sizeof(clnt_adr); clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr,&clnt_adr_sz); pthread_mutex_lock(&mutx); clnt_socks[clnt_cnt++] = clnt_sock; //每当有新连接,将连接的套接字写入变量clnt_cnt和clnt_socks pthread_mutex_unlock(&mutx); pthread_create(&t_id,NULL,handle_clnt,(void*)&clnt_sock); //创建线程向接入的客户端提供服务 pthread_detach(t_id); //从内存中完全销毁已终止的线程 printf("Connected client IP: %s \n",inet_ntoa(clnt_adr.sin_addr)); } close(serv_sock); return 0; } void * handle_clnt(void * arg) { int clnt_sock = *((int*)arg); int str_len = 0, i; char msg[BUF_SIZE]; while((str_len = read(clnt_sock,msg,sizeof(msg))) != 0) send_msg(msg,str_len); pthread_mutex_lock(&mutx); for (i=0; i<clnt_cnt; i++) //remove disconnected client { if (clnt_sock == clnt_socks[i]) { while(i++ < clnt_cnt-1) clnt_socks[i] = clnt_socks[i+1]; break; } } clnt_cnt--; pthread_mutex_unlock(&mutx); close(clnt_sock); return NULL; } void send_msg(char * msg,int len) //send to all:向所有客户端发送消息 { int i; pthread_mutex_lock(&mutx); for ( i=0; i<clnt_cnt; i++) write(clnt_socks[i], msg, len); pthread_mutex_unlock(&mutx); } void error_handling(char *message) { fputs(message,stderr); fputc('\n',stderr); exit(1); }
上述示例的临界区有如下特点:
“访问全局变量clnt_cnt和数组clnt_socks的代码将构成临界区!"
添加或删除客户端时,变量clnt_cnt和数组clnt_socks同时发生变化。如下情形中会导致数据不一致,从而引发严重错误:
所以访问变量clnt_cnt和数组clnt_socks的代码组织在一起并构成临界区。
聊天客户端:客户端为了分离输入和输出过程而创建了线程。
/* 聊天程序客户端 */ #include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> #include<arpa/inet.h> #include<sys/socket.h> #include<pthread.h> #define BUF_SIZE 1024 #define NAME_SIZE 20 void * send_msg(void * arg); void * recv_msg(void * arg); void error_handling(char *message); char name[NAME_SIZE] = "[DEFAULT]"; char msg[BUF_SIZE]; int main(int argc,char *argv[]) { int sock; struct sockaddr_in serv_adr; pthread_t snd_thread, rcv_thread; void * thread_return; if(argc != 4) { printf("Usage : %s <IP> <port> <name> \n",argv[0]); exit(1); } sprintf(name,"[%s]", argv[3]); //第四个参数为客户端名字 sock = socket(PF_INET,SOCK_STREAM,0); if (sock == -1) error_handling("socket() error!"); memset(&serv_adr,0,sizeof(serv_adr)); serv_adr.sin_family = AF_INET; serv_adr.sin_addr.s_addr = inet_addr(argv[1]); serv_adr.sin_port = htons(atoi(argv[2])); if (connect(sock,(struct sockaddr*) &serv_adr,sizeof(serv_adr)) == -1) error_handling("connect() error!"); else puts("Connected........."); pthread_create(&snd_thread,NULL,send_msg,(void*)&sock); pthread_create(&rcv_thread,NULL,recv_msg,(void*)&sock); pthread_join(snd_thread,&thread_return); //返回值保存到thread_return pthread_join(rcv_thread,&thread_return); //返回值保存到thread_return close(sock); return 0; } void * send_msg(void * arg) //send thread main { int sock = *((int*)arg); char name_msg[NAME_SIZE+BUF_SIZE]; while(1) { fgets(msg,BUF_SIZE,stdin); //读取输入到msg if (!strcmp(msg,"q\n") || !strcmp(msg,"Q\n")) { close(sock); exit(0); } sprintf(name_msg,"%s %s",name,msg); //把名字(命令行参数)和消息写入name_msg数组 write(sock,name_msg,strlen(name_msg)); } return NULL; } void * recv_msg(void * arg) //read thread main { int sock= *((int*)arg); char name_msg[NAME_SIZE+BUF_SIZE]; int str_len; while(1) { str_len = read(sock,name_msg,NAME_SIZE+BUF_SIZE-1); if (str_len == -1) return (void*)-1; name_msg[str_len] = 0; fputs(name_msg,stdout); } return NULL; } void error_handling(char *msg) { fputs(msg,stderr); fputc('\n',stderr); exit(1); }
运行结果:
聊天服务器端:
客户端Caoyi:
客户端FanKL:
完结~