理解线程的概念
多进程模型的缺点:
--创建进程的过程会带来一定的开销
--为了完成进程间数据交换,需要特殊的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:
完结~