目录
- 一、同步的概念
- 二、同步机制
- 2.1 信号量
- 2.1.1基础概念
- 2.1.2 函数接口
- 2.1.3 例子
- 2.2 互斥锁
- 2.2.1 几个概念
- 2.2.2 函数接口
- 2.2.3 练习
- 2.3 条件变量
- 2.3.1 步骤
- 2.3.2 函数
- 2.3.3 练习
我们知道,一个进中的所有线程共享进程的资源,所以可以通过在进程中定义全局变量来完成进程中线程间的通信,但是,当在同一内存空间运行多个线程时,要注意一个基本的问题,就是不要让线程之间互相破坏。例如,我们要实现两个线程要更新两个变量的值,一个线程要把两个变量的值都设成0,另一个线程要把两个变量的值都设成1。 如果两个线程同时要做这件事情,结果可能是,一个变量的值是0;另一个变量的值是1。这是因为正好在第1个线程把第1个变量设为0后,时间片到,CPU切换第2个线程,第2个线程将把两个变量都设成1,然后CPU再切换线程,第1个线程恢复运行,把第2个变量设成0。结果就是,一个变量的值是0,另一个变量的值是1。
因此需要同步机制来进行制约。
在System V IPC机制中提供了信号量来实现进程或线程之间的通信。此外按照POSIX标准,POSIX提供了两种类型的同步机制,它们是互斥锁(Mutex) 和 条件变量(condition Variable)。
一、同步的概念
同步是指多个 任务(线程)按照约定的顺序相互配合完成一件事。
二、同步机制
2.1 信号量
2.1.1基础概念
- 通过信号量实现同步操作;由信号量来决定线程是继续运行还是阻塞等待
- 信号量代表某一类资源,其值表示系统中该资源的数量,信号量值>0,表示有资源可以用,可以申请到资源,继续执行程序;信号量值<=0,表示没有资源可以用,无法申请到资源,阻塞。
- 信号量是一个受保护的变量,只能通过三种操作来访问:初始化sem_init、P操作(申请资源)sem_wait、V操作(释放资源)sem_post
2.1.2 函数接口
-
初始化信号量:sem_init
int sem_init(sem_t *sem, int pshared, unsigned int value)- 功能:初始化信号量
- 参数:
- sem:初始化的信号量对象
- pshared:信号量共享的范围(0:线程间使用 ,非0:进程间使用)
- value:信号量初值
- 返回值:成功 0;失败 -1
-
申请资源:sem_wait
int sem_wait(sem_t *sem)- 功能:申请资源 P操作
- 参数:sem:信号量对象
- 返回值:成功;失败 -1
此函数执行过程,当信号量的值大于0时,表示有资源可以用,则继续执行,同时对信号量减1;当信号量的值等于0时,表示没有资源可以使用,函数阻塞。
-
释放资源:sem_post
int sem_post(sem_t *sem)- 功能:释放资源 V操作
- 参数:sem:信号量对象
- 返回值:成功 0;失败 -1
注:释放一次信号量的值加1,函数不阻塞
2.1.3 例子
- 测试信号量<0时,进程(线程)阻塞
- 通过线程实现数据的交互,主线程循环从终端输入,线程函数将数据循环输出,输入一行数据打印一行数据,当输入quit结束程序。
/*
练习:使用信号量实现同步,即通过线程实现数据的交互,主线程循环从终端输入,
线程函数将数据循环输出,当输入quit结束程序。
要点:
信号量初值的设定:初始化信号量为0,是为了让打印线程开始申请不到资源
*/
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <semaphore.h>
char buf[32] = {0};
sem_t sem;
void *handler(void *arg) //线程函数循环输出
{
while (1)
{
sem_wait(&sem); //P 申请资源
if(strcmp(buf,"quit")==0)
{
pthread_exit(NULL);
}
printf("buf:%s\n",buf);
}
return NULL;
}
int main(int argc, char const *argv[])
{
pthread_t tid; //创建线程
if(pthread_create(&tid,NULL,handler,NULL) != 0)
{
perror("pthread_create err");
return -1;
}
//初始化信号量为0,是为了让打印线程开始申请不到资源
if(sem_init(&sem,0,0)<0)
{
perror("sem_init err\n");
return -1;
}
while(1)
{
// scanf("%s",buf);
fgets(buf,32,stdin);
if(buf[strlen(buf)-1]=='\n')
buf[strlen(buf)-1] = '\0';
sem_post(&sem); // V 释放资源
if(strcmp(buf,"quit")==0)
{
break;
}
}
pthread_join(tid, NULL);
sem_destroy(&sem);
return 0;
}
注意:
(1)sem_t sem定义的信号量在主线程和新线程都要使用,故要定义为全局变量。
(2)信号量初值的设定:初始化信号量为0,是为了让打印线程开始申请不到资源,使得主线程获取到数据后先执行。
(3)在终端获取数据可以用scanf或者fgets,但是要注意一点就是使用fgets时,若不加处理会出现输入quit不能终止程序执行,原因是fgets会将换行作为字符捕获,这时的buf内容为quit\n\0,在使用strcmp(buf,“quit”)进行比较时,quit\n\0和quit\0并不相等。处理方法可以是将buf中quit\n\0的倒数第二个字符’\n’给替换掉,使得strcmp(buf,“quit”)可以比较成功。
2.2 互斥锁
2.2.1 几个概念
- 临界资源:一次仅允许一个进程所使用的资源
- 临界区:指的是一个访问共享资源的程序片段
- 互斥:多个线程在访问临界资源时,同一时间只能一个线程访问
- 互斥锁:通过互斥锁可以实现互斥机制,主要用来保护临界资源,每个临界资源都由一个互斥锁来保护,线程必须先获得互斥锁才能访问临界资源,访问完资源后释放该锁。如果无法获得锁,线程会阻塞直到获得锁为止。
2.2.2 函数接口
-
初始化互斥锁:pthread_mutex_init
int pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr)- 功能:初始化互斥锁
- 参数
- mutex:互斥锁
- attr: 互斥锁属性 // NULL表示缺省属性
- 返回值:成功 0;失败 -1
-
申请互斥锁:pthread_mutex_lock
int pthread_mutex_lock(pthread_mutex_t *mutex)- 功能:申请互斥锁
- 参数:mutex:互斥锁
- 返回值:成功 0;失败 -1
注:和pthread_mutex_trylock区别:pthread_mutex_lock是阻塞的;pthread_mutex_trylock不阻塞,如果申请不到锁会立刻返回
-
释放互斥锁:pthread_mutex_unlock
int pthread_mutex_unlock(pthread_mutex_t *mutex)- 功能:释放互斥锁
- 参数:mutex:互斥锁
- 返回值:成功 0;失败 -1
-
销毁互斥锁:pthread_mutex_destroy
int pthread_mutex_destroy(pthread_mutex_t *mutex)- 功能:销毁互斥锁
- 参数:mutex:互斥锁
2.2.3 练习
通过两个线程实现数组倒置,线程1用于循环倒置,线程2用于循环打印。用互斥锁实现同步
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
int a[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
pthread_mutex_t lock;
void *reverse(void *arg)
{
int temp;
while (1)
{
pthread_mutex_lock(&lock);
for (int i = 0; i < 10/2; i++)
{
temp = a[i];
a[i] = a[9-i];
a[9-i] = temp;
}
pthread_mutex_unlock(&lock);
}
}
void *print(void *arg)
{
while (1)
{
pthread_mutex_lock(&lock);
for (int i = 0; i < 10; i++)
{
printf("%d ", a[i]);
}
printf("\n");
pthread_mutex_unlock(&lock);
sleep(1);
}
}
int main(int argc, char const *argv[])
{
pthread_t tid1, tid2;
if (pthread_create(&tid1, NULL, reverse, NULL) != 0)
{
perror("pthread_create tid1 error");
return -1;
}
if (pthread_create(&tid2, NULL, print, NULL) != 0)
{
perror("pthread_create tid2 error");
return -1;
}
if (pthread_mutex_init(&lock, NULL))
{
perror("mutex err");
}
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_mutex_destroy(&lock);
return 0;
}
这个程序可以实现循环倒置与打印,但是不能实现倒置一次打印一次(下面的条件变量可以实现)。因为两个线程并不是交替执行,而是谁抢到时间片谁执行。比如有可能倒置线程先抢到时间片先执行,然后打印线程抢到时间片执行两次,等等其他无次序交替执行
2.3 条件变量
2.3.1 步骤
- pthread_cond_init:初始化
- pthread_cond_wait:阻塞等待条件产生,若没有条件产生会阻塞并且解锁,当有条件产生,再次上锁。所以在使用时先上锁再调用pthread_cond_wait
- pthread_cond_signal:产生条件,不阻塞
- pthread_cond_destory:销毁条件变量
2.3.2 函数
-
初始化条件变量:pthread_cond_init
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);- 功能:初始化条件变量
- 参数:
- cond:是一个指向结构pthread_cond_t 的指针
- restrict attr:是一个指向结构pthread_condattr_t的指针,一般设为NULL
- 返回值:成功:0 ;失败:非0
-
等待信号的产生:pthread_cond_wait
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);- 功能:等待信号的产生
- 参数
- restrict cond:要等待的条件
- restrict mutex:对应的锁
- 返回值:成功:0,失败:不为0
注:当没有条件产生时函数会阻塞,同时会将锁解开;如果等待到条件产生,函数会结束阻塞同时进行上锁。
-
产生条件变量:pthread_cond_signal
int pthread_cond_signal(pthread_cond_t *cond);- 功能:给条件变量发送信号
- 参数:cond:条件变量值
- 返回值:成功:0,失败:非0
注:必须等待pthread_cond_wait函数先执行,再产生条件
-
销毁条件变量:pthread_cond_destroy
int pthread_cond_destroy(pthread_cond_t *cond);- 功能:将条件变量销毁
- 参数:cond:条件变量值
- 返回值:成功:0, 失败:非0
2.3.3 练习
通过两个线程实现数组倒置,线程1用于循环倒置,线程2用于循环打印,实现终端间隔1秒交替循环输出,先输出倒置的数组。用互斥锁+条件变量实现此同步
/*练习:.通过两个线程实现数组倒置,线程一用于循环倒置,线程二用于循环打印。
用互斥锁 + 条件变量实现同步
int a[10] = {0,1,2,3,4,5,6,7,8,9};
*/
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
int a[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
pthread_mutex_t lock;
pthread_cond_t cond;
void *reverse(void *arg)
{
int temp;
while (1)
{
sleep(1); //保证print先抢到锁,让其阻塞等待信号
pthread_mutex_lock(&lock);
for (int i = 0, j = 9; i < j; i++, j--)
{
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
pthread_cond_signal(&cond); //产生条件
pthread_mutex_unlock(&lock); //解锁
}
pthread_exit(NULL);
return NULL;
}
void *print(void *arg)
{
while (1)
{
pthread_mutex_lock(&lock);
// 阻塞等待条件产生,若没条件,则解锁,条件到来解除阻塞上锁
pthread_cond_wait(&cond, &lock);
for (int i = 0; i < 10; i++)
{
printf("%d ", a[i]);
}
printf("\n");
pthread_mutex_unlock(&lock);
}
pthread_exit(NULL);
return NULL;
}
int main(int argc, char const *argv[]) //主进程
{
pthread_t tid1;
pthread_t tid2;
if (pthread_mutex_init(&lock, NULL)) //初始化互斥锁
{
perror("mutex_init err");
return -1;
}
if (pthread_cond_init(&cond, NULL)) //初始化条件变量
{
perror("cond_init err");
return -1;
}
// 创建线程
if (pthread_create(&tid1, NULL, reverse, NULL) != 0)
{
perror("pthread_create tid1 error");
return -1;
}
if (pthread_create(&tid2, NULL, print, NULL) != 0)
{
perror("pthread_create tid2 error");
return -1;
}
// 阻塞回收线程
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&cond);
return 0;
}
注:reverse线程函数里的sleep(1)放在加锁的上面,是为了保证print线程先抢到锁,让其在pthread_cond_wait处阻塞等待条件信号(pthread_cond_signal)产生,然后再reverse线程得到执行,实现数组的倒置,保证第一次的输出结果是倒置后的数据。