博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
线程 互斥量 信号量——多线程服务器端的实现
阅读量:2177 次
发布时间:2019-05-01

本文共 11016 字,大约阅读时间需要 36 分钟。

理解线程的概念

 

多进程模型的缺点:

--创建进程的过程会带来一定的开销

--为了完成进程间数据交换,需要特殊的IPC技术

 最大的缺点:

--每秒少则数十次,多则数前次的“上下文切换"是创建进程时最大的开销。

 

 线程相比进程具有如下优点:

--线程的创建和上下文切换比进程的创建和上下文切换更快。

--线程间交换数据时无需特殊技术。

 

线程和进程的差异

                                            

数据区保存全局变量,堆区域向malloc等函数的动态分配提供空间,函数运行时使用的栈区域。每个进程都有独立空间。

 

如果以获得多个代码执行流为目的,那么不应该像上图那样完全分离内存结构,而只需分离栈区域。

这种方法的优势:

--上下文切换时不需要切换数据区和堆

--可以利用数据区和堆交换数据

实际上这就是线程!线程为了保持多条代码执行流而隔开了栈区域:

                                                        

多个线程共享数据区和堆。

定义:

进程:在操作系统构成单独执行流的单位

线程:在进程构成单独执行流的单位ie

 

关系图:

                                                     

 


线程的创建及运行

线程的创建和执行流程

 

 

示例:thread1.c

#include
#include
#include
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

编译命令:gcc -o thread1 thread1.c -lpthread

运行结果:

执行流程:

 

线程相关的程序中必须保证线程在进程销毁前执行完毕。

用sleep函数可能会干扰程序的正常执行流。因此,我们不用sleep函数。

使用下列函数:

调用该函数的进程或线程将进入等待状态,直到第一个参数为ID的线程终止为止。而且可以得到线程的返回值保存到status中

 

示例:thread2.c

#include
#include
#include
#include
#include
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

运行结果:

执行流程:

                        

 

可在临界区内调用的函数

关于线程的运行需要考虑:多个线程同时调用函数时可能产生问题。

这类函数内部存在临界区,多个线程同时执行这部分代码时,可能会引起问题。

 

函数可分为2类:

--线程安全函数

--非线程安全函数

 

一般非线程安全函数都有相同功能的线程安全的函数。

通过在声明头文件前定义_REENTRANT宏自动将非线程安全函数改为线程安全函数。

也可以在编译时添加 -D-REENTRANT 选项定义宏。

 

工作(Worker)线程模型

示例计算1到10的和,创建两个线程,一个计算1-5的和,另一个计算6-10的和,main函数输出结果。

这种方式的编程模型称为“工作线程(Worker thread)模型。

 

执行流程图:

                                

 

程序:thread3.c

#include
#include
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
#include
#include
#include
#define NUM_THREAD 100void * 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

运行结果:

运行结果不是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
#include
#include
#include
#define NUM_THREAD 100void * 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

运行结果:

 

问题解决了,但是确认运行结果需要等待较长时间。

因为以上程序中,两个线程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
#include
#include
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
#include
#include
#include
#include
#include
#include
#include
#define BUF_SIZE 100#define MAX_CLNT 256void 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
\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和数组clnt_socks的代码将构成临界区!" 

 

 添加或删除客户端时,变量clnt_cnt和数组clnt_socks同时发生变化。如下情形中会导致数据不一致,从而引发严重错误:

 所以访问变量clnt_cnt和数组clnt_socks的代码组织在一起并构成临界区。

 

聊天客户端:客户端为了分离输入和输出过程而创建了线程。

/* 聊天程序客户端 */#include
#include
#include
#include
#include
#include
#include
#define BUF_SIZE 1024#define NAME_SIZE 20void * 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
\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:

 

 

 

 

 

完结~

你可能感兴趣的文章
composer安装YII
查看>>
Sublime text3快捷键演示
查看>>
sublime text3 快捷键修改
查看>>
关于PHP几点建议
查看>>
硬盘的接口、协议
查看>>
VLAN与子网划分区别
查看>>
Cisco Packet Tracer教程
查看>>
02. 交换机的基本配置和管理
查看>>
03. 交换机的Telnet远程登陆配置
查看>>
微信小程序-调用-腾讯视频-解决方案
查看>>
phpStudy安装yaf扩展
查看>>
密码 加密 加盐 常用操作记录
查看>>
TP 分页后,调用指定页。
查看>>
Oracle数据库中的(+)连接
查看>>
java-oracle中几十个实用的PL/SQL
查看>>
PLSQL常用方法汇总
查看>>
几个基本的 Sql Plus 命令 和 例子
查看>>
PLSQL单行函数和组函数详解
查看>>
Oracle PL/SQL语言初级教程之异常处理
查看>>
Oracle PL/SQL语言初级教程之游标
查看>>