Linux 线程同步
创始人
2025-05-31 21:18:05

文章目录

  • 一、线程同步介绍
    • 同步与互斥概述
    • 线程同步问题
  • 二、互斥锁
    • 为什么需要互斥锁
    • 互斥锁 Mutex 介绍
    • 互斥锁相关 API
    • 死锁 DeadLock
  • 三、读写锁
      • 读写锁概述
      • 读写锁相关 API
  • 四、生产者与消费者模型
  • 五、条件变量
    • 条件变量概述
    • 条件变量相关 API
    • 生产者消费者条件变量模型
  • 六、信号量
    • 信号量概述
    • 信号量相关 API
    • 生产者消费者信号量模型

一、线程同步介绍

同步与互斥概述

现代操作系统基本都是多任务操作系统,即同时有大量可调度实体在运行。在多任务操作系统中,同时运行的多个任务可能:

  • 都需要访问/使用同一种资源。
  • 多个任务之间有依赖关系,某个任务的运行依赖于另一个任务。
    这两种情形是多任务编程中遇到的最基本的问题,也是多任务编程中的核心问题,同步和互斥就是用于解决这两个问题的。

互斥 :对于散布在不同任务之间的若干程序片断,当某个任务运行其中一个程序片段时,其它任务就不能运行它们之中的任一程序片段,只能等到该任务运行完这个程序片段后才可以运行。最基本的场景就是:一个公共资源同一时刻只能被一个线程使用,多个线程不能同时使用公共资源。

同步:对于散布在不同任务之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。最基本的场景就是:两个或两个以上的线程在运行过程中协同步调,按预定的先后次序运行。比如 A 任务的运行依赖于 B 任务产生的数据。

显然,同步是一种更为复杂的互斥,而互斥是一种特殊的同步。也就是说互斥是两个任务之间不可以同时运行,他们会相互排斥,必须等待一个线程运行完毕,另一个才能运行,而同步也是不能同时运行,但他是必须要按照某种次序来运行相应的线程(也是一种互斥)!因此互斥具有唯一性和排它性,但互斥并不限制任务的运行顺序,即任务是无序的,而同步的任务之间则有顺序关系。

线程同步问题

线程的主要优势在于,能够通过全局变量来共享信息。不过,这种便捷的共享是有代价的:必须确保多个线程不会同时修改同一变量,或者某一线程不会读取正在由其他线程修改的变量。如果多个线程同时对某一个共享资源做了操作,这样会产生线程安全问题(数据安全问题),或者称之为线程同步问题。

临界区是指访问某一共享资源的代码片段,并且这段代码的执行应为原子操作(不考虑其他因素,原子是不可再继续分割的粒子,类似的原子操作是指不会被线程调度机制打断或者分割的的最小操作),也就是同时访问同一共享资源的其他线程不应终端该片段的执行

线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作,而其他线程则处于等待状态。所以线程同步会一定程度上降低并发的效率,但是它是必须的,因为可以保证共享数据的安全性。

linux 中通常可以通过互斥锁、读写锁、信号量、条件变量等实现线程同步。

二、互斥锁

为什么需要互斥锁

在多任务操作系统中,同时运行的多个任务可能都需要使用同一种资源。这个过程有点类似于,公司部门里,我在使用着打印机打印东西的同时(还没有打印完),别人刚好也在此刻使用打印机打印东西,如果不做任何处理的话,打印出来的东西肯定是错乱的。

下面我们用程序模拟一下这个过程,线程一需要打印“ hello ”,线程二需要打印“ world ”,不加任何处理的话,打印出来的内容会错乱,测试程序:

#include 
#include 
#include 
#include 
#include // 打印机,公共资源
void printer(char *str) {while (*str != '\0') {putchar(*str);fflush(stdout);str++;sleep(1);}printf("\n");
}// 线程一
void *thread_fun_1(void *arg) {char *str = "hello";printer(str); //打印
}// 线程二
void *thread_fun_2(void *arg) {char *str = "world";printer(str); //打印
}int main() {pthread_t tid1, tid2;// 创建 2 个线程pthread_create(&tid1, NULL, thread_fun_1, NULL);pthread_create(&tid2, NULL, thread_fun_2, NULL);// 等待线程结束,回收其资源pthread_join(tid1, NULL);pthread_join(tid2, NULL);return 0;
}

运行结果如下:可以看出打印结果是混乱的,并不是想要的结果。

实际上,打印机是有做处理的,我在打印着的时候别人是不允许打印的,只有等我打印结束后别人才允许打印。这个过程有点类似于,把打印机放在一个房间里,给这个房间安把锁,这个锁默认是打开的。当 A 需要打印时,他先过来检查这把锁有没有锁着,没有的话就进去,同时上锁在房间里打印。而在这时,刚好 B 也需要打印,B 同样先检查锁,发现锁是锁住的,他就在门外等着。而当 A 打印结束后,他会开锁出来,这时候 B 才进去上锁打印。

互斥锁 Mutex 介绍

在线程里也有这么一把锁:互斥锁(mutex),也叫互斥量,互斥锁是一种简单的加锁的方法来控制对共享资源的访问,以此确保同时仅有一个线程可以访问某项共享资源(可以使用互斥量来保证对任意共享资源的原子访问)。

互斥锁有两种状态:已锁定(locked)和未锁定(unlocked)。任何时候,至多只有一个线程可以锁定该互斥锁。试图对已经锁定的某一互斥锁再次加锁,将可能阻塞线程或者报错失败,具体取决于加锁时使用的方法。

一旦线程锁定互斥锁,随即成为该互斥锁的所有者,只有所有者才能给互斥锁解锁。一般情况下,对每一共享资源(可能由多个相关变量组成)会使用不同的互斥锁,每一线程在访问同一资源时将采用如下协议:
1)访问共享资源时,在临界区域前使用互斥锁进行加锁。
2)在访问完成后释放互斥锁导上的锁。
3)对互斥锁进行加锁后,其他任何试图再次对互斥锁加锁的线程将会被阻塞,直到锁被释放。

如果多个线程试图执行这一块代码(一个临界区),那么事实上将只有一个线程能够持有该互斥量(其他线程将遭到阻塞),即同时只有一个线程能够进入这段代码区域,如下图所示:

互斥锁相关 API

1、互斥锁的数据类型是:pthread_mutex_t

2、初始化互斥锁:pthread_mutex_init 函数

#include int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
功能:初始化一个互斥锁。
参数:mutex:互斥锁地址,即需要初始化的互斥量变量。attr:设置互斥量的属性,通常可采用默认属性,即可将 attr 设为 NULL。也可以使用宏 PTHREAD_MUTEX_INITIALIZER 静态初始化互斥锁,比如:pthread_mutex_t  mutex = PTHREAD_MUTEX_INITIALIZER; 这种方法等价于使用 NULL 指定的 attr 参数调用 pthread_mutex_init() 来完成动态初始化,不同之处在于 PTHREAD_MUTEX_INITIALIZER 宏不进行错误检查。返回值:成功:0,成功申请的锁默认是打开的。失败:非 0 错误码

【补充】restrict 是 c 语言中的一种类型限定符(Type Qualifiers),用于告诉编译器,对象已经被指针所引用,不能通过除该指针外所有其他直接或间接的方式修改该对象的内容。

3、释放互斥锁资源:pthread_mutex_destroy 函数

#include int pthread_mutex_destroy(pthread_mutex_t *mutex);
功能:销毁指定的一个互斥锁。互斥锁在使用完毕后,必须要对互斥锁进行销毁,以释放资源。
参数:mutex:互斥锁地址。
返回值:成功:0失败:非 0 错误码

4、加锁:pthread_mutex_lock 函数与 pthread_mutex_trylock 函数

#include int pthread_mutex_lock(pthread_mutex_t *mutex);
功能:对互斥锁上锁,若互斥锁已经上锁,则调用者阻塞,直到互斥锁解锁后再上锁。
参数:mutex:互斥锁地址。
返回值:成功:0失败:非 0 错误码int pthread_mutex_trylock(pthread_mutex_t *mutex);
功能:尝试加锁:如果加锁失败,不会阻塞,会直接返回。调用该函数时,若互斥锁未加锁,则上锁,返回 0;若互斥锁已加锁,则函数直接返回失败,即 EBUSY

pthread_mutex_lock() 将锁住该互斥量。线程调用该函数会发生下面 3 种情况:

  • 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock 之前,该线程一直拥有该锁。
  • 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。
  • 如果当前互斥量被当前调用线程锁住,则会产生死锁(死锁详细见下文)。

pthread_mutex_trylock(),尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。线程调用该函数也会出现下面 3 种情况

  • 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。
  • 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉。
  • 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。

5、解锁:pthread_mutex_unlock函数

#include int pthread_mutex_unlock(pthread_mutex_t *mutex);
功能:对指定的互斥锁解锁。
参数:mutex:互斥锁地址。
返回值:成功:0失败:非0错误码

6、 测试程序

#include 
#include 
#include 
#include 
#include pthread_mutex_t mutex; //互斥锁
// 打印机
void printer(char *str) {pthread_mutex_lock(&mutex); //上锁while (*str != '\0') {putchar(*str);fflush(stdout);str++;sleep(1);}printf("\n");pthread_mutex_unlock(&mutex); //解锁
}// 线程一
void *thread_fun_1(void *arg) {char *str = "hello";printer(str); //打印
}// 线程二
void *thread_fun_2(void *arg) {char *str = "world";printer(str); //打印
}int main(void) {pthread_t tid1, tid2;pthread_mutex_init(&mutex, NULL); //初始化互斥锁// 创建 2 个线程pthread_create(&tid1, NULL, thread_fun_1, NULL);pthread_create(&tid2, NULL, thread_fun_2, NULL);// 等待线程结束,回收其资源pthread_join(tid1, NULL);pthread_join(tid2, NULL);pthread_mutex_destroy(&mutex); //销毁互斥锁return 0;
}

运行结果:

死锁 DeadLock

1)什么是死锁

两个或两个以上的进程在执行过程中,因争夺共享资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

一个线程需要同时访问两个或更多不同的共享资源,而每个资源又都由不同的互斥量管理。当超过一个线程加锁同一组互斥量时,就有可能发生死锁。

2)死锁场景

  1. 忘记释放锁
  2. 重复加锁(重复加锁是指重复加同一个锁,即同一个 mutex 变量)
  3. 多线程多锁,抢占锁资源:都在等待对方占有的不可抢占的资源。
    示例:多线程多锁,抢占锁资源
#include 
#include 
#include // 创建2个互斥量pthread_mutex_t mutex1, mutex2;void * workA(void * arg) {pthread_mutex_lock(&mutex1);sleep(1);pthread_mutex_lock(&mutex2);printf("workA....\n");pthread_mutex_unlock(&mutex2);pthread_mutex_unlock(&mutex1);return NULL;
}void * workB(void * arg) {pthread_mutex_lock(&mutex2);sleep(1);pthread_mutex_lock(&mutex1);printf("workB....\n");pthread_mutex_unlock(&mutex1);pthread_mutex_unlock(&mutex2);return NULL;
}int main() {// 初始化互斥量pthread_mutex_init(&mutex1, NULL);pthread_mutex_init(&mutex2, NULL);// 创建2个子线程pthread_t tid1, tid2;pthread_create(&tid1, NULL, workA, NULL);pthread_create(&tid2, NULL, workB, NULL);// 回收子线程资源pthread_join(tid1, NULL);pthread_join(tid2, NULL);// 释放互斥量资源pthread_mutex_destroy(&mutex1);pthread_mutex_destroy(&mutex2);return 0;
}
// 运行结果是:进程无法继续运行,被一直阻塞。

三、读写锁

读写锁概述

当有一个线程已经持有互斥锁时,互斥锁将所有试图进入临界区的线程都阻塞住。但是考虑一种情形,当前持有互斥锁的线程只是要读访问共享资源,而同时有其它几个线程也想读取这个共享资源,但是由于互斥锁的排它性,所有其它线程都无法获取锁,也就无法读访问共享资源了,但是实际上多个线程同时读访问共享资源并不会导致数据安全问题。

在对数据的读写操作中,更多的是读操作,写操作较少,例如对数据库数据的读写应用。为了满足当前能够允许多个读出,但只允许一个写入的需求,线程提供了读写锁来实现。

读写锁的特点如下:

  • 如果有其它线程读数据,则允许其它线程执行读操作,但不允许写操作。
  • 如果有其它线程写数据,则其它线程都不允许读、写操作。
  • 写是独占的,写的优先级高。

读写锁分为读锁和写锁,规则如下:
1)如果某线程申请了读锁,其它线程可以再申请读锁,但不能申请写锁。
2)如果某线程申请了写锁,其它线程不能申请读锁,也不能申请写锁。

读写锁应用场景:在对数据的读写操作中,更多的是读操作,写操作较少

读写锁相关 API

1、读写锁的数据类型是: pthread_rwlock_t

2、读写锁初始化:pthread_rwlock_init 函数

#include int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);
功能:用来初始化 rwlock 所指向的读写锁。参数:rwlock:指向要初始化的读写锁指针。attr:读写锁的属性指针。如果 attr 为 NULL 则会使用默认的属性初始化读写锁,否则使用指定的 attr 初始化读写锁。可以使用宏 PTHREAD_RWLOCK_INITIALIZER 静态初始化读写锁,比如:pthread_rwlock_t my_rwlock = PTHREAD_RWLOCK_INITIALIZER; 这种方法等价于使用 NULL 指定的 attr 参数调用 pthread_rwlock_init() 来完成动态初始化,不同之处在于PTHREAD_RWLOCK_INITIALIZER 宏不进行错误检查。返回值:成功:0,读写锁的状态将成为已初始化和已解锁。失败:非 0 错误码。

3、释放读写锁资源:pthread_rwlock_destroy 函数

#include int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
功能:用于销毁一个读写锁,并释放所有相关联的资源(所谓的所有指的是由 pthread_rwlock_init() 自动申请的资源) 。
参数:rwlock:读写锁指针。
返回值:成功:0失败:非 0 错误码

4、读加锁:pthread_rwlock_rdlock 函数和 pthread_rwlock_tryrdlock 函数

#include int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
功能:以阻塞方式在读写锁上获取读锁(读锁定)。如果没有写者持有该锁,并且没有写者阻塞在该锁上,则调用线程会获取读锁。如果调用线程未获取读锁,则它将阻塞直到它获取了该锁。一个线程可以在一个读写锁上多次执行读锁定。线程可以成功调用 pthread_rwlock_rdlock() 函数 n 次,但是之后该线程必须调用 pthread_rwlock_unlock() 函数 n 次才能解除锁定。
参数:rwlock:读写锁指针。
返回值:成功:0失败:非 0 错误码int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
功能:尝试加锁:如果加锁失败,不会阻塞,会直接返回,即用于尝试以非阻塞的方式来在读写锁上获取读锁。如果有任何的写者持有该锁或有写者阻塞在该读写锁上,则立即失败返回。

5、写加锁:pthread_rwlock_wrlock 函数和 pthread_rwlock_trywrlock 函数

#include int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
功能:在读写锁上获取写锁(写锁定)。如果没有写者持有该锁,并且没有写者读者持有该锁,则调用线程会获取写锁。如果调用线程未获取写锁,则它将阻塞直到它获取了该锁。
参数:rwlock:读写锁指针。
返回值:成功:0失败:非 0 错误码int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
功能:尝试加锁:如果加锁失败,不会阻塞,会直接返回,即用于尝试以非阻塞的方式来在读写锁上获取写锁。如果有任何的读者或写者持有该锁,则立即失败返回。

6、解锁:pthread_rwlock_unlock 函数

#include int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
功能:无论是读锁或写锁,都可以通过此函数解锁。
参数:rwlock:读写锁指针。
返回值:成功:0失败:非 0 错误码

7、测试程序

下面是一个使用读写锁来实现 4 个线程读写一段数据是实例:在此示例程序中,共创建了 4 个线程,其中两个线程用来写入数据,两个线程用来读取数据。当某个线程读操作时,其他线程允许读操作,却不允许写操作;当某个线程写操作时,其它线程都不允许读或写操作。

#include 
#include 
#include 
#include 
#include pthread_rwlock_t rwlock; //读写锁
int num = 1;//读操作,其他线程允许读操作,却不允许写操作
void *fun1(void *arg) {while (1) {pthread_rwlock_rdlock(&rwlock);printf("read num first===%d\n", num);pthread_rwlock_unlock(&rwlock);sleep(1);}
}//读操作,其他线程允许读操作,却不允许写操作
void *fun2(void *arg) {while (1) {pthread_rwlock_rdlock(&rwlock);printf("read num second===%d\n", num);pthread_rwlock_unlock(&rwlock);sleep(2);}
}//写操作,其它线程都不允许读或写操作
void *fun3(void *arg) {while (1) {pthread_rwlock_wrlock(&rwlock);num++;printf("write thread first\n");pthread_rwlock_unlock(&rwlock);sleep(2);}
}//写操作,其它线程都不允许读或写操作
void *fun4(void *arg) {while (1) {pthread_rwlock_wrlock(&rwlock);num++;printf("write thread second\n");pthread_rwlock_unlock(&rwlock);sleep(1);}
}int main() {pthread_t ptd1, ptd2, ptd3, ptd4;pthread_rwlock_init(&rwlock, NULL);//初始化一个读写锁//创建线程pthread_create(&ptd1, NULL, fun1, NULL);pthread_create(&ptd2, NULL, fun2, NULL);pthread_create(&ptd3, NULL, fun3, NULL);pthread_create(&ptd4, NULL, fun4, NULL);//等待线程结束,回收其资源pthread_join(ptd1, NULL);pthread_join(ptd2, NULL);pthread_join(ptd3, NULL);pthread_join(ptd4, NULL);pthread_rwlock_destroy(&rwlock);//销毁读写锁return 0;
}

运行结果:

四、生产者与消费者模型

线程同步典型的案例为生产者消费者模型,仅仅使用互斥锁也可以实现生产者与消费者模型(可以实现,但是会有很多缺点,改进版本可以看下文条件变量和信号量)。

什么是生产者与消费者模型?

假定有两个线程,一个模拟生产者行为,不断生产产品,一个模拟消费者行为,不断消费产品。所有的产品都存放在一个容器中,这个容器就是一个共享资源,两个线程同时操作这个共享资源(一般称之为汇聚),生产向其中添加产品,消费者从中消费掉产品。

  • 生产者消费者模型中的对象:1、生产者;2、消费者;3、容器
  • 生产者消费者模型中生产者可能会有多个,同理消费者也可能有多个,容器有一个
  • 实现生产者消费者模型过程中会产生的问题:
    • 问题1:数据安全问题——可以使用互斥锁、读写锁解决;
    • 问题2:生产者生产的产品占满了容器,需要等待并通知消费者消费;消费者将容器中产品消费完了,需要等待并通知生产者生产——可以使用条件变量、信号量解决(可以实现,改进版本可以看下文条件变量和信号量)

示例:仅仅使用互斥锁的生产者消费者模型

#include 
#include 
#include 
#include // 创建一个互斥量
pthread_mutex_t mutex;struct Node{int num;struct Node *next;
};// 头结点
struct Node * head = NULL;  //容器void * producer(void * arg) {// 不断的创建新的节点,添加到链表中(头插法)while(1) {pthread_mutex_lock(&mutex);struct Node * newNode = (struct Node *)malloc(sizeof(struct Node));newNode->next = head;head = newNode;newNode->num = rand() % 1000;printf("add node, num : %d, tid : %ld\n", newNode->num, pthread_self());pthread_mutex_unlock(&mutex);usleep(100);}return NULL;
}void * customer(void * arg) {while(1) {pthread_mutex_lock(&mutex);// 保存头结点的指针struct Node * tmp = head;// 判断是否有数据    if(head != NULL) {   // 如果容器里面没有数据了,消费者不停地循环和判断,这样浪费计算资源// 有数据head = head->next;printf("del node, num : %d, tid : %ld\n", tmp->num, pthread_self());free(tmp);pthread_mutex_unlock(&mutex);usleep(100);} else {// 没有数据pthread_mutex_unlock(&mutex);}}return  NULL;
}int main() {pthread_mutex_init(&mutex, NULL);// 创建5个生产者线程,和5个消费者线程pthread_t ptids[5], ctids[5];for(int i = 0; i < 5; i++) {pthread_create(&ptids[i], NULL, producer, NULL);pthread_create(&ctids[i], NULL, customer, NULL);}// 情况一:主线程先退出,那么 pthread_mutex_destroy 将不会执行,所以这种写法错误。// for(int i = 0; i < 5; i++) {//     pthread_detach(ptids[i]);//     pthread_detach(ctids[i]);// }// pthread_exit(NULL);// pthread_mutex_destroy(&mutex);// 情况二:pthread_mutex_destroy 将互斥锁销毁,线程中无法继续使用互斥锁,所以这种写法错误。// for(int i = 0; i < 5; i++) {//     pthread_detach(ptids[i]);//     pthread_detach(ctids[i]);// }// pthread_mutex_destroy(&mutex);// pthread_exit(NULL);// 情况三,正确,使用for 循环不停的阻塞。for(int i = 0; i < 5; i++) {pthread_detach(ptids[i]);pthread_detach(ctids[i]);}while(1) {sleep(10);}pthread_mutex_destroy(&mutex);pthread_exit(NULL);// 情况四: 使用 prhread_join 回收资源,这种方法正确// for(int i = 0; i < 5; i++) {//     pthread_join(ptids[i]);//     pthread_join(ctids[i]);// }// pthread_mutex_destroy(&mutex);// pthread_exit(NULL);return 0;
}

运行结果:

总结:对于问题1,以上的生产者消费者模型可以很好解决,但是对于问题2,以上模型使用 if else 代替了条件变量和信号量,虽然也可以实现功能,但是如果容器里面没有数据了,消费者就需要不停地循环和判断,这样浪费计算资源。

五、条件变量

条件变量概述

与互斥锁不同,条件变量是用来等待而不是用来上锁的,条件变量本身不是锁。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。

通常条件变量和互斥锁同时使用:条件变量变量主要用于阻塞线程,但是它不能保证线程安全,如果要保证线程安全需要与互斥锁配合使用

条件变量的两个动作:

  • 条件不满,阻塞线程
  • 当条件满足,通知阻塞的线程开始工作(解除阻塞)

条件变量相关 API

1、条件变量的类型:pthread_cond_t

2、初始化条件变量:pthread_cond_init 函数

#include int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
功能:初始化一个条件变量
参数:cond:指向要初始化的条件变量指针。attr:条件变量属性,通常为默认值,传 NULL即可也可以使用静态初始化的方法,初始化条件变量:pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
返回值:成功:0失败:非0错误号

3、释放条件变量资源:pthread_cond_destroy 函数

#include int pthread_cond_destroy(pthread_cond_t *cond);
功能:销毁一个条件变量
参数:cond:指向要初始化的条件变量指针
返回值:成功:0失败:非0错误号

4、等待条件变量:pthread_cond_wait 函数

#include int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
功能:阻塞等待一个条件变量a) 阻塞等待条件变量cond(参1)满足b) 释放已掌握的互斥锁(解锁互斥量)相当于pthread_mutex_unlock(&mutex)。其中 a) b) 两步为一个原子操作。c) 当被唤醒,pthread_cond_wait函数返回时,解除阻塞并重新申请获取互斥锁pthread_mutex_lock(&mutex);参数:cond:指向要初始化的条件变量指针mutex:互斥锁返回值:成功:0失败:非0错误号int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex,const struct timespec *restrict abstime);
功能:限时阻塞等待一个条件变量:调用了这个函数,线程会阻塞,直到指定的时间结束
参数:cond:指向要初始化的条件变量指针mutex:互斥锁abstime:绝对时间返回值:成功:0失败:非0错误号

abstime 补充说明:

struct timespec {time_t tv_sec;      /* seconds */ // 秒long   tv_nsec; /* nanosecondes*/ // 纳秒
}time_t cur = time(NULL);        //获取当前时间。
struct timespec t;              //定义timespec 结构体变量t
t.tv_sec = cur + 1;             // 定时1秒
pthread_cond_timedwait(&cond, &t);

5、唤醒条件变量(唤醒阻塞在条件变量上的线程):pthread_cond_signal函数

#include int pthread_cond_signal(pthread_cond_t *cond);
功能:唤醒至少一个阻塞在条件变量上的线程
参数:cond:指向要初始化的条件变量指针
返回值:成功:0失败:非0错误号int pthread_cond_broadcast(pthread_cond_t *cond);
功能:唤醒全部阻塞在条件变量上的线程
参数:cond:指向要初始化的条件变量指针
返回值:成功:0失败:非0错误号
#include 
#include 
#include 
#include 
#include pthread_mutex_t mutex;  // 互斥量
pthread_cond_t cond;    // 条件变量int flag = 0;// 改变条件的线程
void *fun1(void *arg) {while (1) {   pthread_mutex_lock(&mutex); // 枷锁flag = 1;pthread_mutex_unlock(&mutex);  //解锁// 唤醒因为条件而阻塞线程pthread_cond_signal(&cond);printf("线程1已经改变条件\n");sleep(2);}return NULL;
}// 等待条件的线程,如果条件成立往下执行
void *fun2(void *arg) {while (1) {pthread_mutex_lock(&mutex);// 条件不满足 则阻塞等条件满足if (0 == flag) {pthread_cond_wait(&cond, &mutex);}printf("线程二因为条件满足 开始运作...\n");flag = 0;pthread_mutex_unlock(&mutex); // 解锁}return NULL;
}int main() {int ret = -1;pthread_t tid1, tid2;// 初始化条件变量ret = pthread_cond_init(&cond, NULL);if (0 != ret) {printf("pthread_cond_init failed...\n");return 1;}// 初始化互斥量ret = pthread_mutex_init(&mutex, NULL);if (0 != ret) {printf("pthread_mutex_init failed...\n");return 1;}// 创建两个线程pthread_create(&tid1, NULL, fun1, NULL);pthread_create(&tid2, NULL, fun2, NULL);// 回收线程资源ret = pthread_join(tid1, NULL); if (0 != ret) {printf("pthread_join failed...\n");return 1;}ret = pthread_join(tid2, NULL); if (0 != ret) {printf("pthread_join failed...\n");return 1;}// 销毁互斥量pthread_mutex_destroy(&mutex);// 销毁条件变量pthread_cond_destroy(&cond);return 0;
}

生产者消费者条件变量模型

线程同步典型的案例即为生产者消费者模型,而借助条件变量来实现这一模型,是比较常见的一种方法。

#include 
#include 
#include 
#include // 创建一个互斥量
pthread_mutex_t mutex;// 创建条件变量
pthread_cond_t cond;struct Node{int num;struct Node *next;
};
// 头结点
struct Node * head = NULL;void * producer(void * arg) {// 不断的创建新的节点,添加到链表中while(1) {pthread_mutex_lock(&mutex);struct Node * newNode = (struct Node *)malloc(sizeof(struct Node));newNode->next = head;head = newNode;newNode->num = rand() % 1000;printf("add node, num : %d, tid : %ld\n", newNode->num, pthread_self());// 只要生产了一个,就通知消费者消费pthread_cond_signal(&cond);pthread_mutex_unlock(&mutex);usleep(100);}return NULL;
}void * customer(void * arg) {while(1) {pthread_mutex_lock(&mutex);// 保存头结点的指针struct Node * tmp = head;// 判断是否有数据if(head != NULL) {// 有数据head = head->next;printf("del node, num : %d, tid : %ld\n", tmp->num, pthread_self());free(tmp);pthread_mutex_unlock(&mutex);usleep(100);} else {// 没有数据,需要等待// 当这个函数调用阻塞的时候,会对互斥锁进行解锁,当不阻塞的,继续向下执行,会重新加锁。pthread_cond_wait(&cond, &mutex);pthread_mutex_unlock(&mutex);}}return  NULL;
}int main() {pthread_mutex_init(&mutex, NULL);pthread_cond_init(&cond, NULL);// 创建5个生产者线程,和5个消费者线程pthread_t ptids[5], ctids[5];for(int i = 0; i < 5; i++) {pthread_create(&ptids[i], NULL, producer, NULL);pthread_create(&ctids[i], NULL, customer, NULL);}for(int i = 0; i < 5; i++) {pthread_detach(ptids[i]);pthread_detach(ctids[i]);}while(1) {sleep(10);}pthread_mutex_destroy(&mutex);pthread_cond_destroy(&cond);pthread_exit(NULL);return 0;
}


总结:相较于 mutex 而言,条件变量可以减少竞争。如直接使用 mutex,除了生产者、消费者之间要竞争互斥量以外,消费者之间也需要竞争互斥量,但如果汇聚(链表)中没有数据,消费者之间竞争互斥锁是无意义的。有了条件变量机制以后,只有生产者完成生产,才会引起消费者之间的竞争。提高了程序效率。

六、信号量

信号量概述

信号量也叫信号灯,广泛用于进程或线程间的同步和互斥,信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。编程时可根据操作信号量值的结果判断是否对公共资源具有访问的权限,当信号量值大于 0 时,则可以访问,否则将阻塞。

PV 原语是对信号量的操作,一次 P 操作使信号量减1(占用1个资源),一次 V 操作使信号量加1(释放1个资源)。

信号量主要用于进程或线程间的同步和互斥这两种典型情况。

  • 信号量用于互斥:
  • 信号量用于同步:

信号量和条件变量变量类似,也主要用于阻塞线程,但是它不能保证线程安全,如果要保证线程安全需要与互斥锁配合使用。

信号量相关 API

1、信号量数据类型:sem_t。

2、信号量初始化:sem_init 函数

#include int sem_init(sem_t *sem, int pshared, unsigned int value);
功能:创建一个信号量并初始化它的值。一个无名信号量在被使用前必须先初始化。
参数:sem:信号量的地址。pshared:等于 0,信号量在线程间共享(常用);不等于0,信号量在进程间共享。value:信号量的初始值。
返回值:成功:0失败: - 1

3、释放信号量资源:sem_destroy 函数

#include int sem_destroy(sem_t *sem);
功能:删除 sem 标识的信号量。
参数:sem:信号量地址。
返回值:成功:0失败: - 1

4、信号量 P 操作(减1

#include int sem_wait(sem_t *sem);
功能:将信号量的值减 1(占用1个资源)。操作前,先检查信号量(sem)的值是否为 0,若信号量为 0,此函数会阻塞,直到信号量大于 0 时才进行减 1 操作。
参数:sem:信号量的地址。
返回值:成功:0失败: - 1int sem_trywait(sem_t *sem);以非阻塞的方式来对信号量进行减 1 操作。若操作前,信号量的值等于 0,则对信号量的操作失败,函数立即返回。int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);限时尝试(非阻塞)将信号量的值减 1abs_timeout:绝对时间

abs_timeout补充说明:

struct timespec {time_t tv_sec;      /* seconds */ // 秒long   tv_nsec; /* nanosecondes*/ // 纳秒
}time_t cur = time(NULL);        //获取当前时间。
struct timespec t;              //定义timespec 结构体变量t
t.tv_sec = cur + 1;             // 定时1秒
sem_timedwait(&cond, &t)

5、信号量 V 操作(加1)

#include int sem_post(sem_t *sem);
功能:将信号量的值加 1(释放1个资源) 并发出信号唤醒等待线程(sem_wait())。
参数:sem:信号量的地址。
返回值:成功:0失败:-1

6、获取信号量的值:sem_getvalue

#include int sem_getvalue(sem_t *sem, int *sval);
功能:获取 sem 标识的信号量的值,保存在 sval 中。
参数:sem:信号量地址。sval:保存信号量值的地址。
返回值:成功:0失败:-1

7、程序示例

// 信号量用于互斥
#include 
#include 
#include 
#include 
#include 
#include sem_t sem; //信号量void printer(char *str) {sem_wait(&sem);//减一while (*str) {putchar(*str);fflush(stdout);str++;sleep(1);}printf("\n");sem_post(&sem);//加一
}void *thread_fun1(void *arg) {char *str1 = "hello";printer(str1);
}void *thread_fun2(void *arg) {char *str2 = "world";printer(str2);
}int main(void) {pthread_t tid1, tid2;sem_init(&sem, 0, 1); //初始化信号量,初始值为 1//创建 2 个线程pthread_create(&tid1, NULL, thread_fun1, NULL);pthread_create(&tid2, NULL, thread_fun2, NULL);//等待线程结束,回收其资源pthread_join(tid1, NULL);pthread_join(tid2, NULL);sem_destroy(&sem); //销毁信号量return 0;
}

生产者消费者信号量模型

// 信号量用于同步——生产者消费者模式
#include 
#include 
#include 
#include 
#include // 创建一个互斥量
pthread_mutex_t mutex;// 创建两个信号量
sem_t psem;  // 存放商品容器的个数
sem_t csem;   // 可以消费的商品的个数struct Node{int num;struct Node *next;
};// 头结点
struct Node * head = NULL;
void * producer(void * arg) {// 不断的创建新的节点,添加到链表中while(1) {sem_wait(&psem);pthread_mutex_lock(&mutex);struct Node * newNode = (struct Node *)malloc(sizeof(struct Node));newNode->next = head;head = newNode;newNode->num = rand() % 1000;printf("add node, num : %d, tid : %ld\n", newNode->num, pthread_self());pthread_mutex_unlock(&mutex);// 通知消费者消费,将可以卖的商品个数加1sem_post(&csem);}return NULL;
}void * customer(void * arg) {while(1) {// 申请资源  可以卖的的商品个数减1sem_wait(&csem);pthread_mutex_lock(&mutex);// 保存头结点的指针struct Node * tmp = head;head = head->next;printf("del node, num : %d, tid : %ld\n", tmp->num, pthread_self());free(tmp);pthread_mutex_unlock(&mutex);// 释放资源 将容器的个数加1sem_post(&psem);}return  NULL;
}int main() {pthread_mutex_init(&mutex, NULL);sem_init(&psem, 0, 8);  //初始化信号量,初始值为 8(最多可以生产8个商品)sem_init(&csem, 0, 0);  //初始化信号量,初始值为 0(开始可以买的商品为0)// 创建5个生产者线程,和5个消费者线程pthread_t ptids[5], ctids[5];for(int i = 0; i < 5; i++) {pthread_create(&ptids[i], NULL, producer, NULL);pthread_create(&ctids[i], NULL, customer, NULL);}for(int i = 0; i < 5; i++) {pthread_detach(ptids[i]);pthread_detach(ctids[i]);}while(1) {sleep(10);}pthread_mutex_destroy(&mutex);pthread_exit(NULL);return 0;
}

相关内容

热门资讯

最新!2025新势力5月销量出... 5月新势力表现如何?2025年5月的销售周期刚刚过去,不少造车新势力厂商的销量数据,已经新鲜出炉。数...
两家A股公司,收终止上市决定 又有两家A股上市公司收到股票终止上市决定,6月10日进入退市整理期。 上述自律监管决定书指出,因2...
重磅,事关教育强国,主力提前埋... 数据是个宝数据宝投资少烦恼这些产业的景气度处于上升期。《求是》杂志发表文章《加快建设教育强国》6月1...
阳光诺和“二刷”收购 80后富... 《投资者网》蔡俊时隔2年后,阳光诺和(688621.SH,下称“公司”)再拟收购同一个资产。实际上,...
买车,不安全了? 买车,不安全... 在新能源汽车市场竞争空前激烈的当下,车企、经销商习惯于采取更加激进的营销、市场策略,尤其在行业加速“...
欧佩克+同意7月再增产41.1... 为了增产惩罚超产国并争夺市场份额,欧佩克+连续第三个月大幅增产,美国页岩油生产商或首当其冲,美油一度...
经济学泰斗菲舍尔逝世:培育伯南... 当地时间6月1日,以色列央行发布声明称,世界著名经济学家、以色列央行前行长及美联储前副主席菲舍尔(S...
更名!“天府证券”来了 天府证... 【导读】宏信证券更名为天府证券中国基金报记者 吴君这家券商,历史上第二次更名。5月末,工商信息显示,...
两家A股公司,收终止上市决定 ... 又有两家A股上市公司收到股票终止上市决定,6月10日进入退市整理期。*ST鹏博(600804)公告称...
瑞幸降价迈入“6块9”时代?瑞... 说起最近几年的咖啡茶饮市场,相信每个人都不会陌生,各家咖啡茶饮企业的各种降价消息是此起彼伏,就在最近...
主次节奏:6.1黄金 - 每周... 本文每周初更新发布梳理各级别走势分析和预期主次节奏:做有品质的三方服务黄金月线图(超长线) 月线图...
超400亿资金狂涌!这类ETF... 债券ETF市场持续扩容。今年以来,债券市场表现震荡,债券类基金回报远不及预期,但这并未妨碍债券型ET...
坚定信心 行稳致远(记者手记) 侯琳良 最近一段时间,海尔集团上世纪90年代投资制作的《海尔兄弟》动画片,在多个视频平台上线高清重制...
世纪大辩论2——哈耶克与凯恩斯... 本来节后决定启动一个项目,但家里临时有事,需要陪家人去一趟北京,节后拉群的事,因此要推迟一周左右(具...
4月广州消费品市场表现强劲 1-4月,随着消费品以旧换新等促消费政策持续发力和各类会展活动陆续开展,政策相关消费快速增长,升级类...
金价,又跌了! 人民财讯5月31日电,5月30日,COMEX黄金期货收跌0.92%,报3313.1美元/盎司。 从高...
10万吨改性项目!巴斯夫、金发... 【DT新材料】获悉,6月3日,沪市主板新股海阳科技将启动申购,上市在即! 资料显示,海阳科技前身为南...
湾财周报|大事记 比亚迪驳斥“... 一周大事记(5月26日-6月1日) 头条 比亚迪驳斥! 长城“车圈恒大论”是行业警示还是危言耸听?...
通源石油跌1.96%,成交额1... 5月30日,通源石油跌1.96%,成交额1.03亿元,换手率4.40%,总市值23.54亿元。 异动...
中国邮储银行浙江分行2025校... 点这里 ↑ 老满说高考 作者 l 老满 生涯规划师l 升学顾问l 拆书家 这是 老满说高考公众号 的...
公募基金规模首次突破33万亿元... 每经记者:肖芮冬 每经编辑:叶峰 天赐良基日报第654期 一、今日基金新闻速览 1、华润元大基金贾...
湾财周报 大事记 比亚迪驳斥“... 一周大事记(5月26日-6月1日)头条比亚迪驳斥!长城“车圈恒大论”是行业警示还是危言耸听?近日,关...
EL表达式JSTL标签库 EL表达式     EL:Expression Language 表达式语言     ...
关于测试,我发现了哪些新大陆 关于测试 平常也只是听说过一些关于测试的术语,但并没有使用过测试工具。偶然看到编程老师...
工信部、中汽协紧急发声!汽车“... 文/刘育英新一轮汽车价格战再起。近日,工信部、中汽协纷纷发声表示反对。工业和信息化部表示,将加大对汽...
3 ROS1通讯编程提高(1) 3 ROS1通讯编程提高3.1 使用VS Code编译ROS13.1.1 VS Code的安装和配置...
募资39亿,全亏光了,账上不到... 关于天然气,用户的感觉是价格一直在上涨,但很奇怪,不管怎么涨,天然气企业仍然亏,还亏得一塌糊涂。这是...
资阳房产评估公司 这是(tel-15828298733)整理的信息,希望能帮助到大家 在当今社会,随着经济的发展和城...
华桥汇利(中国)投资基金管理有... 今年第一季度,美国企业利润出现大幅下降,且面临着来自关税上升的持续压力,这一局面可能会在今年进一步加...
ESG 报告合规与鉴证:全球政... 在当下全球经济格局里,ESG(环境、社会和公司治理)已然成为衡量企业可持续发展能力的关键指标。随着全...