TCP/IP网络编程 (十八):多线程服务器端的实现(线程,互斥量,信号量)

理解线程的概念

 

多进程模型的缺点:

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

--为了完成进程间数据交换,需要特殊的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:

 

 

 

 

 

完结~

猜你喜欢

转载自blog.csdn.net/amoscykl/article/details/80322394