1.线程通信方式
- 通过全局变量进行数据交换
1.因为线程共享同一进程的地址空间,共享全局变量
2.线程同步互斥机制提出缘由
- 正是因为同一进程下的线程之间可以通过全局变量交换数据,假设线程1正在访问一个全局变量A,然而此时线程A的时间片用完,内核会调度线程2继续执行,此时如果线程2将全局变量A修改,那么当线程1再次访问变量A时就会发生错误,所以才需要同步互斥机制.
3.线程同步机制
- 同步(synchronization)指多个任务按照约定的先后顺序相互配合完成同一间事情,即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作
- 举个例子:有一个存储区,初始化为NULL;有一个读线程,负责读取存储器中的数据;有一个写线程,负责向存储器写入数据;由于存储器开始没有数据,所以读线程需要阻塞,当写线程向存储区写入数据之后,写线程通过同步机制(信号量)通知读线程读取存储区数据,读线程读取数据后再通知写线程写入新的数据
- Edsgar Dijkstra在1968年基于信号量的概念提出了一种同步机制,由信号量来决定线程是继续运行还是阻塞等待
4.信号量,PV操作(理论)
- 信号量代表某一类资源,其值表示系统中该资源的数量,值>=0
- 信号量是一种受保护的变量,只能通过3种操作来访问,分别为:
1.初始化
2.P操作(申请资源)
解释:
当线程需要访问某一资源时候,会对代表目标资源的信号量进行访问,如果该信号量大于0,代表目标资源存在,可以继续访问;当该信号量小于0时,代表该资源不存在,此时任务便进入阻塞状态,直到代表资源的信号量存在
伪代码:
if (信号量值大于0){
申请资源的任务(阻塞态任务)继续运行;
信号量的值--;
}else{
申请资源的任务阻塞;
}
3.V操作(释放资源)
解释:
当某任务(线程)产生了资源(即产生了变量或者数据),此时该任务会通知系统资源数增加了,让系统增加代表资源的信号量,进而让处于阻塞态的任务继续执行
伪代码:
信号量值加1;
if (有任务在等待资源){
唤醒等待的任务,让其继续运行;
}
5.信号量,PV操作(代码)
- posix信号量:Linux系统遵循posix(可移植操作系统接口)规范,其定义了2类信号量
1.无名信号量(基于内存的信号量),无名信号量主要用于进程内部线程之间的通信
2.有名信号量,有名信号量可用于进程和线程之间的通信 - 相关函数
1.信号量初始化:
#include
/*
功能:初始化信号量
参数:
参数1:sem指向要初始化的信号量对象
参数2:信号量使用范围,0为在线程间使用,1为在进程间使用
参数3:信号量初始值,0表示无资源,大于0代表有资源
返回值:成功返回0,失败返回-1并设置errno
*/
int sem_init(sem_t *sem, int pshared, unsigned int value);
注意:
sem_t
为信号量对象,即保存信号量特性的结构体,使用sem_init
之前需要定义一个sem_t
类型结构体
2.P操作:
#include
/*
功能:P操作,检查是否有信号量
参数:要操作的信号量对象
返回值:成功返回0,失败返回-1并设置errno
*/
int sem_wait(sem_t *sem);
3.V操作:
#include
/*
功能:V操作,增加信号量
参数:要操作的信号量对象
返回值:成功返回0,失败返回-1并设置errno
*/
int sem_post(sem_t *sem);
6.使用信号量实现线程同步示例
- 两个线程实现同步读写缓冲区(生产者/消费者问题)
即一个线程读取数据,一个线程写入数据,二者之间寻求平衡
#include
#include
#include
#include
#include
char buff[32];
sem_t sem;//定义信号量结构体
void *function(void *arg);
int main(int argc, char *argv[])
{
pthread_t a_thread;
//先初始化信号量
if((sem_init(&sem, 0, 0)) < 0){
perror("sem init");
exit(-1);
}
//再创建线程
if(pthread_create(&a_thread, NULL, function, NULL) != 0){
printf("fail to pthread_creatr");
exit(-1);
}
printf("input 'quit' to exit\n");
do{
fgets(buff, 32, stdin);
sem_post(&sem);//通知读线程可以读取数据
}while(strncmp(buff, "quit", 4) != 0);
return 0;
}
void *function(void *arg)
{
while(1){
sem_wait(&sem);
printf("your input messages is : %s\n", buff);
}
}
执行结果如下:
hanqi@hanqi-PC:~/C/thread$ gcc read_write.c -Wall -lpthread
hanqi@hanqi-PC:~/C/thread$ ./a.out
input 'quit' to exit
hello world
your input messages is : hello world
quit
hanqi@hanqi-PC:~/C/thread$
分析:在运行程序时候,我们去查看进程
ps aux -L |grep a.out
,查看线程需要加上-L
hanqi@hanqi-PC:~/C/thread$ ps aux -L |grep a.out
hanqi 4981 4981 0.0 2 0.0 14540 720 pts/1 Sl+ 09:41 0:00 ./a.out
hanqi 4981 4982 0.0 2 0.0 14540 720 pts/1 Sl+ 09:41 0:00 ./a.out
hanqi 5043 5043 0.0 1 0.0 14536 952 pts/2 S+ 09:44 0:00 grep a.out
可以发现一共出现了2个线程其二者进程号相同,均为4981.主线程线程号与进程号相同,副线程线程号为4982.当执行./a.out
后主线程会在fgets()
处等待用户输入,故主线程陷入阻塞状态;此时由于信号量为0,子线程无法获取数据,故也进入阻塞状态.
- 改进
1.上述例程并没有实现严格意义上的同步.
2.上述例程的读线程在读取数据之前通过P操作检查缓冲区是否有数据,如果没有数据则会阻塞,有数据会读取数据.然而对于写线程而言,在写入数据之前没有判断缓冲区内是否存在数据,实际上只有当缓冲区内没有数据的时候才能写入数据.
3.举个例子,当读线程执行时间比较长的时候,此时写线程又写入了数据,这样会造成原来数据丢失.故不论读线程还是写线程在执行操作之前都需要判断当前缓冲区内数据,读线程应该判断缓冲区内是否存在可读数据,写线程应该判断缓冲区内是否有空间写入数据.
4.为严格意义上实现同步,我们需要定义两个信号量,一个为读信号量sem_r
,判断当前缓冲区是否可读,另一个写信号量sem_w
,判断当前缓冲区是否可写.
5.在程序执行开始的时候,缓冲区内没有数据sem_r = 0
,即读线程不可读进入阻塞状态,而写写线程可写入数据sem_w = 1
.
6.更改后程序如下:
#include
#include
#include
#include
#include
char buff[32];
sem_t sem_r, sem_w;//定义信号量结构体
void *function(void *arg);
int main(int argc, char *argv[])
{
pthread_t a_thread;
//先初始化读信号量
if((sem_init(&sem_r, 0, 0)) < 0){
perror("sem_r init");
exit(-1);
}
//先初始化写信号量
if((sem_init(&sem_w, 0, 1)) < 0){
perror("sem_w init");
exit(-1);
}
//再创建线程
if(pthread_create(&a_thread, NULL, function, NULL) != 0){
printf("fail to pthread_creatr");
exit(-1);
}
printf("input 'quit' to exit\n");
do{
sem_wait(&sem_w);//判断写信号量(sem_w=1-1=0)
fgets(buff, 32, stdin);
sem_post(&sem_r);//通知读线程可以读取数据(sem_r=0+1=1)
}while(strncmp(buff, "quit", 4) != 0);
return 0;
}
void *function(void *arg)
{
while(1){
sem_wait(&sem_r);//(sem_wait=1-1=0)
printf("your input messages is : %s\n", buff);
sem_post(&sem_w);//(sem_w=0+1=1)
}
}