【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 
#include 
#include 
#include "Thread.hpp" 
using namespace std;
// 以下演示结果的解释:
// 需要尽可能的让多个线程交叉执行。
// 多个线程交叉执行本质就是让调度器尽可能的频繁发生线程调度与切换。
// 线程一般在什么时候发生切换呢?时间片到了,来了更高优先级的线程,线程等待的时候。
// 线程是在什么时候检测上面的问题?在内核态返回用户态的时候,线程要对调度状态进行检测,如果可以,就直接发生线程切换。

/* 概念:火车票(共享资源) */
int g_ticket = 10000;

/* 线程函数 */
void* StartRoutine(void* args) {
    const char* userName = static_cast(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 thread1(new Thread(StartRoutine, (void*)"用户1", 1));
    unique_ptr thread2(new Thread(StartRoutine, (void*)"用户2", 2));
    unique_ptr thread3(new Thread(StartRoutine, (void*)"用户3", 3));
    unique_ptr thread4(new Thread(StartRoutine, (void*)"用户4", 4));
    unique_ptr thread5(new Thread(StartRoutine, (void*)"用户5", 5));
    unique_ptr thread6(new Thread(StartRoutine, (void*)"用户6", 6));
    unique_ptr thread7(new Thread(StartRoutine, (void*)"用户7", 7));
    unique_ptr 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张!

【Linux】系统编程线程互斥与同步(C++)_第1张图片

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

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

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

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

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

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

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

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

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

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

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

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

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

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

【Linux】系统编程线程互斥与同步(C++)_第2张图片

【互斥量的接口】

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

静态分配:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

动态分配:

#include 

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 
#include 
#include 
#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(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 thread1(new Thread(StartRoutine, (void*)"用户1", 1));
    unique_ptr thread2(new Thread(StartRoutine, (void*)"用户2", 2));
    unique_ptr thread3(new Thread(StartRoutine, (void*)"用户3", 3));
    unique_ptr thread4(new Thread(StartRoutine, (void*)"用户4", 4));
    unique_ptr thread5(new Thread(StartRoutine, (void*)"用户5", 5));
    unique_ptr thread6(new Thread(StartRoutine, (void*)"用户6", 6));
    unique_ptr thread7(new Thread(StartRoutine, (void*)"用户7", 7));
    unique_ptr 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 
#include 
#include 
#include 
#include 
#include 
#include  
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(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 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的伪代码改一下。

【Linux】系统编程线程互斥与同步(C++)_第3张图片

【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 

/* 原生线程锁类封装 */
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 
#include 
#include 
#include 
#include 
#include 

#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(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()
{
    // 创建线程Id
    vector 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 
#include 
#include 
#include 
#include 
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(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 3
    pthread_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 
#include 
#include 
#include 
#include 
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(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 3
    pthread_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

你可能感兴趣的:(Linux,java)