【Linux】系统编程线程互斥与同步(C++)

目录

【1】线程互斥

【1.1】进程线程间的互斥相关背景概念

【1.2】互斥量mutex

【1.3】互斥量实现原理探究

【1.4】RAII的加锁风格

【2】可重入VS线程安全

【2.1】概念

【2.2】常见的线程不安全的情况

【2.3】常见的线程安全的情况

【2.4】常见不可重入的情况

【2.5】常见可重入的情况

【2.6】可重入与线程安全联系

【2.7】可重入与线程安全区别

【3】死锁

【3.1】死锁的概念

【3.2】死锁四个必要条件

【3.3】避免死锁

【3.4】避免死锁算法

【4】线程同步

【4.1】条件变量

【4.2】同步概念与竞态条件

【4.3】条件变量函数


【1】线程互斥

【1.1】进程线程间的互斥相关背景概念

  • 【临界资源】多线程执行流共享的资源就叫做临界资源。

  • 【临界区】每个线程内部,访问临界资源的代码,就叫做临界区。

  • 【互斥】任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。

  • 【原子性】(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。

【1.2】互斥量mutex

        大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。

        但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。

        多个线程并发的操作共享变量,会带来一些问题。

#include <memory>
#include <cstring>
#include <unistd.h>
#include "Thread.hpp" 
using namespace std;
// 以下演示结果的解释:
// 需要尽可能的让多个线程交叉执行。
// 多个线程交叉执行本质就是让调度器尽可能的频繁发生线程调度与切换。
// 线程一般在什么时候发生切换呢?时间片到了,来了更高优先级的线程,线程等待的时候。
// 线程是在什么时候检测上面的问题?在内核态返回用户态的时候,线程要对调度状态进行检测,如果可以,就直接发生线程切换。/* 概念:火车票(共享资源) */
int g_ticket = 10000;/* 线程函数 */
void* StartRoutine(void* args) {const char* userName = static_cast<const char*>(args);char buffer[64];while(true) {if(g_ticket > 0) {// 模拟真实抢票要花费的时间usleep(1);snprintf(buffer, sizeof(buffer), "%s正在进行抢票, 已抢到,还剩余:%d张!\n", userName, g_ticket--);cout << buffer << endl; }else {// snprintf(buffer, sizeof(buffer), "%s说没有没有票了,现在资源为:%d张!\n", userName, g_ticket);//cout << buffer << endl;break;}}return nullptr;
}/* 入口函数 */
int main() {unique_ptr<Thread> thread1(new Thread(StartRoutine, (void*)"用户1", 1));unique_ptr<Thread> thread2(new Thread(StartRoutine, (void*)"用户2", 2));unique_ptr<Thread> thread3(new Thread(StartRoutine, (void*)"用户3", 3));unique_ptr<Thread> thread4(new Thread(StartRoutine, (void*)"用户4", 4));unique_ptr<Thread> thread5(new Thread(StartRoutine, (void*)"用户5", 5));unique_ptr<Thread> thread6(new Thread(StartRoutine, (void*)"用户6", 6));unique_ptr<Thread> thread7(new Thread(StartRoutine, (void*)"用户7", 7));unique_ptr<Thread> thread8(new Thread(StartRoutine, (void*)"用户8", 8));thread1->Join();thread2->Join();thread3->Join();thread4->Join();thread5->Join();thread6->Join();thread7->Join();thread8->Join();return 0;
}// 打印结果:出问题啦!
用户2正在进行抢票, 已抢到,还剩余:3张!
用户3正在进行抢票, 已抢到,还剩余:4张!
用户6正在进行抢票, 已抢到,还剩余:9张!
用户7正在进行抢票, 已抢到,还剩余:2张!
用户6正在进行抢票, 已抢到,还剩余:1张!
用户5正在进行抢票, 已抢到,还剩余:0张!
用户8正在进行抢票, 已抢到,还剩余:-2张
用户4正在进行抢票, 已抢到,还剩余:-1张!
用户1正在进行抢票, 已抢到,还剩余:-3张!
用户3正在进行抢票, 已抢到,还剩余:-4张!
用户2正在进行抢票, 已抢到,还剩余:-5张!

【为什么可能无法获得争取结果】

  • if 语句判断条件为真以后,代码可以并发的切换到其他线程。

  • usleep 这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段。

  • --ticket 操作本身就不是一个原子操作。

取出ticket--部分的汇编代码
objdump -d a.out > test.objdump
152 40064b: 8b 05 e3 04 20 00 mov 0x2004e3(%rip),%eax # 600b34 <ticket>
153 400651: 83 e8 01 sub $0x1,%eax
154 400654: 89 05 da 04 20 00 mov %eax,0x2004da(%rip) # 600b34 <ticket>

操作并不是原子操作,而是对应三条汇编指令:

  • load :将共享变量ticket从内存加载到寄存器中。

  • update : 更新寄存器里面的值,执行-1操作。

  • store :将新值,从寄存器写回共享变量ticket的内存地址。

要解决以上问题,需要做到三点:

  • 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。

  • 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。

  • 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。

【互斥量的接口】

初始化互斥量有两种方法:

静态分配:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

动态分配:

#include <pthread.h>int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrictattr);
// 参数:
//     mutex:要初始化的互斥量
//     attr:NULL

销毁互斥量

【注意】

  • 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁。

  • 不要销毁一个已经加锁的互斥量。

  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁。

int pthread_mutex_destroy(pthread_mutex_t *mutex);

互斥量加锁和解锁

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
// 返回值:成功返回0,失败返回错误号

调用 pthread_ lock 时,可能会遇到以下情况:

  • 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功。

  • 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。

【定义全局锁实例】不需要调用Init和Destroy函数对锁进行初始化和销毁!

// Makefile文件----------------------------------------------------------------------
# 定义替代关系
cc=g++
standard=-std=c++11# 定义myThread可执行依赖于Thread.cc文件
myThread:Thread.cc$(cc) -o $@ $^ $(standard) -l pthread# 定义删除可执行命令
.PHONY:clean
clean: rm -rf myThread// Thread.cc文件----------------------------------------------------------------------	
#include <memory>
#include <cstring>
#include <unistd.h>
#include "Thread.hpp" 
using namespace std;
// 以下演示结果的解释:
// 需要尽可能的让多个线程交叉执行。
// 多个线程交叉执行本质就是让调度器尽可能的频繁发生线程调度与切换。
// 线程一般在什么时候发生切换呢?时间片到了,来了更高优先级的线程,线程等待的时候。
// 线程是在什么时候检测上面的问题?在内核态返回用户态的时候,线程要对调度状态进行检测,如果可以,就直接发生线程切换。/* 概念:火车票(共享资源) */
int g_ticket = 10000;
// 解决问题的方式:
// 1、多个执行流进行安全访问的共享资源-->临界资源.
// 2、我们把多个执行流中,访问临界资源的代码,称为临界区,往往是线程代码的很小的那一部分。
// 3、想让多个线程访问共享资源,称为互斥。
// 4、对一个资源进行访问,要么不做,要么昨晚,称为原子性,不是原子性的情况,如果只用一条汇编就能完成
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;/* 线程函数 */
void* StartRoutine(void* args) {const char* userName = static_cast<const char*>(args);char buffer[64];while(true) {pthread_mutex_lock(&mutex);if(g_ticket > 0) {// 模拟真实抢票要花费的时间usleep(1);snprintf(buffer, sizeof(buffer), "[%s]正在进行抢票, 已抢到,还剩余:%d张!\n", userName, g_ticket--);cout << buffer << endl; pthread_mutex_unlock(&mutex);}else {snprintf(buffer, sizeof(buffer), "[%s]说没有没有票了,现在资源为:%d张!\n", userName, g_ticket);cout << buffer << endl;pthread_mutex_unlock(&mutex);break;}}return nullptr;
}/* 入口函数 */
int main() {unique_ptr<Thread> thread1(new Thread(StartRoutine, (void*)"用户1", 1));unique_ptr<Thread> thread2(new Thread(StartRoutine, (void*)"用户2", 2));unique_ptr<Thread> thread3(new Thread(StartRoutine, (void*)"用户3", 3));unique_ptr<Thread> thread4(new Thread(StartRoutine, (void*)"用户4", 4));unique_ptr<Thread> thread5(new Thread(StartRoutine, (void*)"用户5", 5));unique_ptr<Thread> thread6(new Thread(StartRoutine, (void*)"用户6", 6));unique_ptr<Thread> thread7(new Thread(StartRoutine, (void*)"用户7", 7));unique_ptr<Thread> thread8(new Thread(StartRoutine, (void*)"用户8", 8));thread1->Join();thread2->Join();thread3->Join();thread4->Join();thread5->Join();thread6->Join();thread7->Join();thread8->Join();return 0;
}// 打印结果:
[用户7]正在进行抢票, 已抢到,还剩余:5张!
[用户7]正在进行抢票, 已抢到,还剩余:4张!
[用户7]正在进行抢票, 已抢到,还剩余:3张!
[用户7]正在进行抢票, 已抢到,还剩余:2张!
[用户7]正在进行抢票, 已抢到,还剩余:1张!
[用户7]说没有没有票了,现在资源为:0张!
[用户1]说没有没有票了,现在资源为:0张!
[用户3]说没有没有票了,现在资源为:0张!
[用户5]说没有没有票了,现在资源为:0张!
[用户4]说没有没有票了,现在资源为:0张!
[用户8]说没有没有票了,现在资源为:0张!
[用户2]说没有没有票了,现在资源为:0张!
[用户6]说没有没有票了,现在资源为:0张!

【定义局部锁实例】

// Makefile文件----------------------------------------------------------------------
# 定义替代关系
cc=g++
standard=-std=c++11# 定义myThread可执行依赖于Thread.cc文件
myThread:Thread.cc$(cc) -o $@ $^ $(standard) -l pthread# 定义删除可执行命令
.PHONY:clean
clean: rm -rf myThread// Thread.cc文件----------------------------------------------------------------------	
#include <iostream>
#include <string>
#include <vector>
#include <memory>
#include <cstring>
#include <unistd.h>
#include <pthread.h> 
using namespace std;/* 概念:火车票(共享资源) */
int g_ticket = 10000;
// 解决问题的方式:
// 1、多个执行流进行安全访问的共享资源-->临界资源.
// 2、我们把多个执行流中,访问临界资源的代码,称为临界区,往往是线程代码的很小的那一部分。
// 3、想让多个线程访问共享资源,称为互斥。
// 4、对一个资源进行访问,要么不做,要么昨晚,称为原子性,不是原子性的情况,如果只用一条汇编就能完成class ThreadData {
public:/* 构造函数 */ThreadData(const string& name, pthread_mutex_t* mutex): _tdName(name), _tdLock(mutex){};/* 析构函数 */~ThreadData();public:string           _tdName;   // 线程名pthread_mutex_t* _tdLock;  // 线程锁
};/* 线程函数 */
void* StartRoutine(void* args) {// 获取参数化ThreadData* tid = static_cast<ThreadData*>(args);while(true) {// 【加锁】// 加锁和解锁的过程多个线程串行执行,程序变慢了!// 锁之规定互斥访问,没有规定必须让谁优先执行!// 锁就是让多个执行流进行进行竞争的结果!pthread_mutex_lock(tid->_tdLock);if(g_ticket > 0) {// 这个时间模拟线程需要抢票的时间usleep(1000);std::cout << tid->_tdName << "-正在进行抢票: " << g_ticket-- << std::endl;pthread_mutex_unlock(tid->_tdLock);}else {std::cout << tid->_tdName << "-说资源已用完: " << g_ticket << std::endl;pthread_mutex_unlock(tid->_tdLock);break;}// 这个时间模拟线程抢完票后,忙其他事情usleep(10);}return nullptr;
}/* 入口函数 */
int main() {
#define NUM 10// 创建锁pthread_mutex_t lock;// 初始化锁pthread_mutex_init(&lock, nullptr);// 创建线程vector<pthread_t> tds(NUM); // 存储线程id的容器.for(int i = 0; i < NUM; i++) {// 设置线程名称char buffer[64] = { 0 };snprintf(buffer, sizeof(buffer), "Thread%d", i + 1);// 创建线程数据ThreadData *td = new ThreadData(buffer, &lock);// 创建线程    pthread_create(&tds[i], nullptr, StartRoutine, (void*)td);}// 等待锁for(const auto& tid : tds) {pthread_join(tid, nullptr);}// 释放锁pthread_mutex_destroy(&lock);return 0;
}// 打印结果:
Thread2-正在进行抢票: 16
Thread9-正在进行抢票: 15
Thread5-正在进行抢票: 14
Thread7-正在进行抢票: 13
Thread6-正在进行抢票: 12
Thread8-正在进行抢票: 11
Thread1-正在进行抢票: 10
Thread4-正在进行抢票: 9
Thread3-正在进行抢票: 8
Thread10-正在进行抢票: 7
Thread2-正在进行抢票: 6
Thread9-正在进行抢票: 5
Thread5-正在进行抢票: 4
Thread7-正在进行抢票: 3
Thread6-正在进行抢票: 2
Thread8-正在进行抢票: 1
Thread1-说资源已用完: 0
Thread4-说资源已用完: 0
Thread3-说资源已用完: 0
Thread8-说资源已用完: 0
Thread10-说资源已用完: 0
Thread2-说资源已用完: 0
Thread9-说资源已用完: 0
Thread5-说资源已用完: 0
Thread7-说资源已用完: 0
Thread6-说资源已用完: 0

【如何看待锁】

  • 锁本身就是一个共享资源,全局的变量是要被保护的,锁是用来保护全局资源的,锁本身也是全局资源。

【锁的安全谁来保护呢】

  • pthread_mutex_lock、pthread_mutex_unlock加锁和解锁的过程必须是安全的!加锁的过程是原子的。

  • 如果申请成功,就继续向后执行,如果申请暂时没有成功,执行流会就会阻塞!

【如果线程一,申请锁成功,进入临界资源,正在访问临界资源期间,其他线程在做什么】

  • 阻塞等待!

【如果线程一,申请锁成功,进入临界资源,正在访问临界资源期间,我可不可以被切换呢】

  • 绝对可以!

  • 当持有锁的线程被切走的时候,是抱着锁被切走的,即便自己被切走了,其他线程依旧无法申请锁成功,也便无法向后执行!直到我最终释放这个锁!

所以,对于其他线程而言,有意义的锁的状态,无非两种:申请锁前、申请锁后!站在其他线程的角度,看待当前线程持有锁的过程!就是原子的,建议,未来我们在使用锁的时候,一定要尽量保证临界区的粒度要非常小!

【1.3】互斥量实现原理探究

  • 经过上面的例子,大家已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题。

  • 为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 现在我们把lock和unlock的伪代码改一下。

【1.4】RAII的加锁风格

【Makefile文件】

# 定义变量给变量复制对应的字符串标签
cc:= g++
standrad:= -std=c++11 # 定义编译链接关系
myThreadMutex: ThreadMutex.cc$(cc) -o $@ $^ $(standard) -lpthread# 定义命令
clean:rm -rf myThreadMutex.PHONY: clean

【ThreadMutex.hpp文件】

#pragma once 
#include <pthread.h>/* 原生线程锁类封装 */
class Mutex
{
public:/* - 构造函数*/Mutex(pthread_mutex_t* mutex = nullptr): _pMutex(mutex){}/* - 析构函数*/~Mutex(){}public:/* - 加锁*/void Lock() { if(_pMutex != nullptr) pthread_mutex_lock(_pMutex); }/* - 解锁*/void UnLock() { if(_pMutex != nullptr) pthread_mutex_unlock(_pMutex); }private:pthread_mutex_t*     _pMutex;
};/* 线程锁操作类封装 */
class LockGuardMutex
{
public:/* - 构造函数*/LockGuardMutex(pthread_mutex_t* mutex) : _mutex(mutex) { _mutex.Lock(); }/* - 析构函数*/~LockGuardMutex() { _mutex.UnLock(); }private:Mutex _mutex;
};

【Thread.cc文件】

#include <cstdio>
#include <iostream>
#include <vector>
#include <string>
#include <unistd.h>
#include <pthread.h>#include "ThreadMutex.hpp"
using namespace std;/* 概念:火车票(共享资源) */
int g_ticket = 10000;class ThreadData
{
public:ThreadData(const string& name, pthread_mutex_t* lock): _threadName(name), _threadLock(lock){}~ThreadData() {}public:string           _threadName;pthread_mutex_t* _threadLock;
};static void* StartRoutine(void* args)
{ThreadData* tData = static_cast<ThreadData*>(args);while(true) {{// pthread_mutex_lock(tData->_threadLock);LockGuardMutex lockGuard(tData->_threadLock);if(g_ticket > 0) {std::cout << tData->_threadName << "-正在进行抢票: " << g_ticket-- << std::endl;// pthread_mutex_unlock(tData->_threadLock);   }else {std::cout << tData->_threadName << "-说资源已用完: " << g_ticket << std::endl;// pthread_mutex_unlock(tData->_threadLock);break;}}// 这个时间模拟线程抢完票后,忙其他事情usleep(1000);}return nullptr;
}#define NUM 10
int main()
{// 创建线程Idvector<pthread_t> tIds(NUM);// 创建线程锁pthread_mutex_t mutex;pthread_mutex_init(&mutex, nullptr);// 创建线程for(int i = 0; i < NUM; i++){// 构建名称char buffer[64];snprintf(buffer, sizeof(buffer), "Thread-%d", i + 1);ThreadData* tData = new ThreadData(buffer, &mutex);pthread_create(&tIds[i], nullptr, StartRoutine, (void*)tData);usleep(10);}// 等待线程for(const auto& id : tIds){pthread_join(id, nullptr);}// 释放锁pthread_mutex_destroy(&mutex);return 0;
}

【打印结果】

Thread-4-正在进行抢票: 16
Thread-8-正在进行抢票: 15
Thread-9-正在进行抢票: 14
Thread-10-正在进行抢票: 13
Thread-3-正在进行抢票: 12
Thread-7-正在进行抢票: 11
Thread-6-正在进行抢票: 10
Thread-1-正在进行抢票: 9
Thread-5-正在进行抢票: 8
Thread-8-正在进行抢票: 7
Thread-2-正在进行抢票: 6
Thread-4-正在进行抢票: 5
Thread-9-正在进行抢票: 4
Thread-10-正在进行抢票: 3
Thread-7-正在进行抢票: 2
Thread-3-正在进行抢票: 1
Thread-6-说资源已用完: 0
Thread-1-说资源已用完: 0
Thread-4-说资源已用完: 0
Thread-8-说资源已用完: 0
Thread-2-说资源已用完: 0
Thread-10-说资源已用完: 0
Thread-7-说资源已用完: 0
Thread-5-说资源已用完: 0
Thread-9-说资源已用完: 0
Thread-3-说资源已用完: 0

【2】可重入VS线程安全

【2.1】概念

【线程安全】多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。

【重入】同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

【2.2】常见的线程不安全的情况

  • 不保护共享变量的函数。

  • 函数状态随着被调用,状态发生变化的函数。

  • 返回指向静态变量指针的函数。

  • 调用线程不安全函数的函数。

【2.3】常见的线程安全的情况

  • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的。

  • 类或者接口对于线程来说都是原子操作。

  • 多个线程之间的切换不会导致该接口的执行结果存在二义性。

【2.4】常见不可重入的情况

  • 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的。

  • 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

  • 可重入函数体内使用了静态的数据结构。

【2.5】常见可重入的情况

  • 不使用全局变量或静态变量。

  • 不使用用malloc或者new开辟出的空间。

  • 不调用不可重入函数。

  • 不返回静态或全局数据,所有数据都有函数的调用者提供。

  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。

【2.6】可重入与线程安全联系

  • 函数是可重入的,那就是线程安全的。

  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题。

  • 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

【2.7】可重入与线程安全区别

  • 可重入函数是线程安全函数的一种。

  • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。

  • 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

【3】死锁

【3.1】死锁的概念

        死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。

【3.2】死锁四个必要条件

  • 【互斥条件】一个资源每次只能被一个执行流使用。

  • 【请求与保持条件】一个执行流因请求资源而阻塞时,对已获得的资源保持不放。

  • 【不剥夺条件】一个执行流已获得的资源,在末使用完之前,不能强行剥夺。

  • 【循环等待条件】若干执行流之间形成一种头尾相接的循环等待资源的关系。

【3.3】避免死锁

  • 破坏死锁的四个必要条件。

  • 加锁顺序一致。

  • 避免锁未释放的场景。

  • 资源一次性分配。

【3.4】避免死锁算法

  • 死锁检测算法(了解)

  • 银行家算法(了解)

【4】线程同步

【4.1】条件变量

  • 当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。

  • 例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。

【4.2】同步概念与竞态条件

【同步】在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。

【竞态条件】因为时序问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解。

【4.3】条件变量函数

【初始化】

  • 静态初始化

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
  • 动态初始化

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrictattr);
// 参数:
//     cond:要初始化的条件变量
//     attr:NULL

【销毁】

int pthread_cond_destroy(pthread_cond_t *cond)

【等待条件满足】

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
// 参数:
//     cond:要在这个条件变量上等待
//     mutex:互斥量,后面详细解释

【唤醒满足】

// 一次性唤醒线程
int pthread_cond_broadcast(pthread_cond_t *cond);
// 唤醒单个线程
int pthread_cond_signal(pthread_cond_t *cond);

【代码实例】

#include <iostream>
#include <cstdio>
#include <cassert>
#include <unistd.h>
#include <pthread.h>
using namespace std;/* 共享资源 */
int g_tickets = 1000;/* 定义互斥锁 */
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;/* 定义信号量 */
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;/* 线程函数 */
void* StartRoutine(void* args) {const char* threadName = static_cast<const char*>(args);// 线程执行while(true) {// 加锁pthread_mutex_lock(&mutex);// 条件等待pthread_cond_wait(&cond, &mutex);cout << threadName << " - " << --g_tickets << endl;// 解锁pthread_mutex_unlock(&mutex);}// 释放内存delete[] threadName;return nullptr;
}/* 程序入口函数 */
int main() {
#define NUM 3pthread_t tds[NUM];for(int i = 0; i < NUM; i++) {char* threadName = new char[64];snprintf(threadName, sizeof(char) * 64, "Thread-%d", i + 1);int n = pthread_create(tds + i, nullptr, StartRoutine, (void*)threadName);assert(n == 0); (void)n;}// 唤醒线程执行while(true) {sleep(1);pthread_cond_signal(&cond);cout << "main thread wekeup one thread..." << endl;}for(int i = 0; i < NUM; i++) {int n = pthread_join(*(tds + 1), nullptr);assert(n == 0); (void)n;}return 0;
}// 打印结果:
main thread wekeup one thread...
Thread-2 - 992
main thread wekeup one thread...
Thread-3 - 991
main thread wekeup one thread...
Thread-1 - 990
main thread wekeup one thread...
Thread-2 - 989
main thread wekeup one thread...
Thread-3 - 988
main thread wekeup one thread...
Thread-1 - 987

【为什么 pthread_cond_wait 需要互斥量】

        条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。

        条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据。

【按照上面的说法,我们设计出如下的代码】

        先上锁,发现条件不满足,解锁,然后等待在条件变量上不就行了。

【代码实例】

#include <iostream>
#include <cstdio>
#include <cassert>
#include <unistd.h>
#include <pthread.h>
using namespace std;/* 共享资源 */
int g_tickets = 1000;/* 定义互斥锁 */
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;/* 定义信号量 */
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;/* 线程函数 */
void* StartRoutine(void* args) {const char* threadName = static_cast<const char*>(args);// 线程执行while(true) {// 加锁pthread_mutex_lock(&mutex);// 条件等待pthread_cond_wait(&cond, &mutex);cout << threadName << " - " << --g_tickets << endl;// 解锁pthread_mutex_unlock(&mutex);}// 释放内存delete[] threadName;return nullptr;
}/* 程序入口函数 */
int main() {
#define NUM 3pthread_t tds[NUM];for(int i = 0; i < NUM; i++) {char* threadName = new char[64];snprintf(threadName, sizeof(char) * 64, "Thread-%d", i + 1);int n = pthread_create(tds + i, nullptr, StartRoutine, (void*)threadName);assert(n == 0); (void)n;}// 唤醒线程执行while(true) {sleep(1);// 唤醒单个进程// pthread_cond_signal(&cond);// 唤醒多个进行pthread_cond_broadcast(&cond);cout << "main thread wekeup one thread..." << endl;}for(int i = 0; i < NUM; i++) {int n = pthread_join(*(tds + 1), nullptr);assert(n == 0); (void)n;}return 0;
}// 打印结果:
main thread wekeup one thread...
Thread-1 - 990
Thread-2 - 989
Thread-3 - 988
main thread wekeup one thread...
Thread-1 - 987
Thread-2 - 986
Thread-3 - 985
main thread wekeup one thread...
Thread-1 - 984
Thread-2 - 983
Thread-3 - 982
main thread wekeup one thread...
Thread-1 - 981
Thread-2 - 980
Thread-3 - 979

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/138327.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

无线定位中TDOA时延估计算法matlab仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 1.算法运行效果图预览 2.算法运行软件版本 matlab2022a 3.部分核心程序 ...................................................................figure; plot(P1x,P1y…

【Graph Net学习】LINE实现Graph Embedding

一、简介 LINE (Large-scale Information Network Embedding,2015) 是一种设计用于处理大规模信息网络的算法。它主要的目标是在给定的大规模信息网络中学习高质量的节点嵌入&#xff0c;并尽量保留网络中信息的丰富性。其具体的表现为在一个低 维空间里以向量形式表示网络中的…

华为云云耀云服务器L实例评测|华为云上安装监控服务Prometheus三件套安装

文章目录 华为云云耀云服务器L实例评测&#xff5c;华为云上试用监控服务Prometheus一、监控服务Prometheus三件套介绍二、华为云主机准备三、Prometheus安装四、Grafana安装五、alertmanager安装六、三个服务的启停管理1. Prometheus、Alertmanager 和 Grafana 启动顺序2. 使用…

mysql workbench常用操作

1、No database selected Select the default DB to be used by double-clicking its name in the SCHEMAS list in the sidebar 方法一&#xff1a;双击你要使用的库 方法二&#xff1a;USE 数据库名 2、复制表名&#xff0c;字段名 3、保存链接

折线图geom_line()参数选项

往期折线图教程 图形复现| 使用R语言绘制折线图折线图指定位置标记折线图形状更改 | 绘制动态折线图跟着NC学作图 | 使用python绘制折线图 前言 我们折线的专栏推出一段时间&#xff0c;但是由于个人的原因&#xff0c;一直未进行更新。那么今天&#xff0c;我们也参考《R语…

Kafka-UI

有多款kafka管理应用&#xff0c;目前选择的是github上star最多的UI for Apache Kafka。 关于 To run UI for Apache Kafka, you can use either a pre-built Docker image or build it (or a jar file) yourself. UI for Apache Kafka is a versatile, fast, and lightweight…

关于ABB机器人的IO创建和设置

首先要链接网线&#xff0c;请求写权限 关于ABB机器人的默认地址位10有的是63.31看你硬接线 ABB机器人分配好信号机器人控制柜要重启 可以把机器人分配的信号导出为EIO 类似与发那科机器人IO

Spring底层原理之 BeanFactory 与 ApplicationContext

&#x1f40c;个人主页&#xff1a; &#x1f40c; 叶落闲庭 &#x1f4a8;我的专栏&#xff1a;&#x1f4a8; c语言 数据结构 javaEE 操作系统 Redis 石可破也&#xff0c;而不可夺坚&#xff1b;丹可磨也&#xff0c;而不可夺赤。 Spring底层原理 一、 BeanFactory 与 Appli…

ChatGPT追祖寻宗:GPT-3技术报告要点解读

论文地址&#xff1a;https://arxiv.org/abs/2005.14165 往期相关文章&#xff1a; ChatGPT追祖寻宗&#xff1a;GPT-1论文要点解读_五点钟科技的博客-CSDN博客ChatGPT追祖寻宗&#xff1a;GPT-2论文要点解读_五点钟科技的博客-CSDN博客 本文的标题之所以取名技术报告而不是论文…

无法删除目录(设备或资源忙)

文章目录 无法删除目录&#xff08;设备或资源忙&#xff09;问题原因解决方案步骤一&#xff1a;首先找到挂载的位置步骤二&#xff1a;取消挂载步骤三&#xff1a;查看挂在情况 无法删除目录&#xff08;设备或资源忙&#xff09; 问题 原因 网络共享挂载导致无法删除 解决…

C++实现观察者模式(包含源码)

文章目录 观察者模式一、基本概念二、实现方式三、角色四、过程五、结构图六、构建思路七、完整代码 观察者模式 一、基本概念 观察者模式&#xff08;又被称为模型&#xff08;Model&#xff09;-视图&#xff08;View&#xff09;模式&#xff09;是软件设计模式的一种。在…

Twitter图片数据优化的细节

Twitter个人数据优化&#xff1a;吸引更多关注和互动 头像照片在Twitter上&#xff0c;头像照片是最快识别一个账号的方法之一。因此&#xff0c;请务必使用公司的标志或与品牌相关的图片。建议尺寸为400x400像素。 为了建立强大的品牌形象和一致性&#xff0c;强烈建议在所有…

Spring MVC常见面试题

Spring MVC简介 Spring MVC框架是以请求为驱动&#xff0c;围绕Servlet设计&#xff0c;将请求发给控制器&#xff0c;然后通过模型对象&#xff0c;分派器来展示请求结果视图。简单来说&#xff0c;Spring MVC整合了前端请求的处理及响应。 Servlet 是运行在 Web 服务器或应用…

Xcode 15 运行<iOS 14, 启动崩溃问题

如题. Xcode 15 启动 < iOS 14(没具体验证过, 我的问题设备是iOS 13.7)真机设备 出现启动崩溃 解决方案: Build Settings -> Other Linker Flags -> Add -> -ld64

第八天:gec6818arm开发板和Ubuntu中安装并且编译移植mysql驱动连接QT执行程序

一、Ubuntu18.04中安装并且编译移植mysql驱动程序连接qt执行程序 1 、安装Mysql sudo apt-get install mysql-serverapt-get isntall mysql-clientsudo apt-get install libmysqlclient-d2、查看是否安装成功&#xff0c;即查看MySQL版本 mysql --version 3、MySQL启动…

STP生成树协议基本配置示例---STP逻辑树产生和修改

STP是用来避免数据链路层出现逻辑环路的协议&#xff0c;运行STP协议的设备通过交互信息发现环路&#xff0c;并通过阻塞特定端口&#xff0c;最终将网络结构修剪成无环路的树形结构。在网络出现故障的时候&#xff0c;STP能快速发现链路故障&#xff0c;并尽快找出另外一条路径…

Selenium-介绍下其他骚操作

Chrome DevTools 简介 Chrome DevTools 是一组直接内置在基于 Chromium 的浏览器&#xff08;如 Chrome、Opera 和 Microsoft Edge&#xff09;中的工具&#xff0c;用于帮助开发人员调试和研究网站。 借助 Chrome DevTools&#xff0c;开发人员可以更深入地访问网站&#xf…

SpringMVC初级

文章目录 一、SpringMVC 概述二、springMVC步骤1、新建maven的web项目2、导入maven依赖3、创建controller4、创建spring-mvc.xml配置文件&#xff08;本质就是spring的配置件&#xff09;5、web.xml中配置前端控制器6、新建a.jsp文件7、配置tomcat8、启动测试 三、工作流程分析…

【Vue】如何搭建SPA项目--详细教程

&#x1f3ac; 艳艳耶✌️&#xff1a;个人主页 &#x1f525; 个人专栏 &#xff1a;《Spring与Mybatis集成整合》《springMvc使用》 ⛺️ 生活的理想&#xff0c;为了不断更新自己 ! 目录 1.什么是vue-cli 2.安装 2.1.创建SPA项目 2.3.一问一答模式答案 3.运行SPA项目 3…

UML活动图

在UML中&#xff0c;活动图本质上就是流程图&#xff0c;它描述系统的活动、判定点和分支等&#xff0c;因此它对开发人员来说是一种重要工具。 活动图 活动是某件事情正在进行的状态&#xff0c;既可以是现实生活中正在进行的某一项工作&#xff0c;也可以是软件系统中某个类…