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

news2024/10/6 8:36:43

目录

【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()
{
    // 创建线程Id
    vector<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 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 <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 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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【golang】深入理解GMP调度模型

Goroutine Go中&#xff0c;协程被称为goroutine&#xff0c;它非常轻量&#xff0c;一个goroutine只占几KB&#xff0c;并且这几KB就足够goroutine运行完&#xff0c;这就能在有限的内存空间内支持大量goroutine&#xff0c;支持了更多的并发&#xff0c;虽然一个goroutine的…

基于YOLOv8模型的条形码二维码检测系统(PyTorch+Pyside6+YOLOv8模型)

摘要&#xff1a;基于YOLOv8模型的条形码二维码检测系统可用于日常生活中检测与定位条形码与二维码目标&#xff0c;利用深度学习算法可实现图片、视频、摄像头等方式的目标检测&#xff0c;另外本系统还支持图片、视频等格式的结果可视化与结果导出。本系统采用YOLOv8目标检测…

Ubuntu 12.04增加右键命令:在终端中打开增加打开文件

Ubuntu 12.04增加右键命令&#xff1a;在终端中打开 软件中心&#xff1a;搜索nautilus-open-terminal安装 用快捷键CtrlT打开命令行输入&#xff1a; sudo apt-get install nautilus-open-terminal 重新加载文件管理器 nautilus -q 或注销再登录即要使用

一文弄懂基于采样的路径规划-RRT系列(python代码)

基于采样的路径规划算法-RRT系列 VX关注晓理紫并回复rrt获取代码 [晓理紫] 1、基于采样的路径规划算法 基于抽样的规划方法&#xff08;或称概率方法&#xff09;通过在连续 C 空间中逐步或批量抽样&#xff0c;构建由离散 C 空间样本连接的树或图&#xff0c;从而捕捉解空间的…

飞书应用配置+蓝鲸流水线+jump server

开发者后台创建应用 配置应用基础信息&#xff0c;权限&#xff0c;安全等 管理后台 设置应用在工作台的可见范围和其他设置 Linux 常用命令&#xff1a;Linux 常用命令, ll 文件夹下文件&#xff0c;ls 文件&#xff0c;cd进入目录&#xff0c; cat 查看文件&#xff0c; v…

php反序列化+题

含义&#xff1a; php序列化&#xff08;serialize&#xff09;&#xff1a;是将变量转换为可保存或传输的字符串的过程php反序列化&#xff08;unserialize&#xff09;&#xff1a;就是在适当的时候把这个字符串再转化成原来的变量使用这两个过程结合起来&#xff0c;可以轻…

微信小程序底部安全区域高度获取

CSS 属性 safe-area-inset-bottom safe-area-inset-bottom 就是安全区的高度 padding-bottom:env(safe-area-inset-bottom); wx.getSystemInfoSync() wx.getSystemInfoSync()可以获取系统信息 let system wx.getSystemInfoSync() let bottomSafe system.screenHeight -…

React 全栈体系(十三)

第七章 redux 五、redux 异步编程 1. 理解 redux 默认是不能进行异步处理的,某些时候应用中需要在 redux 中执行异步任务(ajax, 定时器) 2. 使用异步中间件 npm install --save redux-thunk 3. 代码 - 异步 action 版 3.1 store /* src/redux/store.js */ /*** 该文件专…

全景剖析|国产芯(CPU、GPU、SSD、NAND、DRAM)虽有突破,但路还很长

这两天小编看到两个有关国产芯的消息,很有感触,本文小编分享下所思所想,如有不当之处,还望包涵,可以留言指正! 第一个消息是:近日,华为轮值主席 徐直军在2023世界计算大会上,有个国产芯的呼吁,“我们不要抱有幻想,应该坚定不移的打造可持续发展的计算产业生态。从计…

Webpack打包时Bable解决浏览器兼容问题

当我们使用js新特性语法编写代码时&#xff0c;在旧的浏览器中兼容性并不好。但是我们希望能够在旧浏览器中使用这些新特性。 使用babel可以使js新代码转换为js旧代码&#xff0c;增加浏览器的兼容性。 如果我们希望在Webpack中支持babel&#xff0c;则需要在Webpack中引入bab…

【C++】静态成员函数 ( 静态成员函数概念 | 静态成员函数声明 | 静态成员函数访问 | 静态成员函数只能访问静态成员 )

文章目录 一、静态成员函数简介1、静态成员函数概念2、静态成员函数声明3、静态成员函数访问4、静态成员函数只能访问静态成员 二、代码示例 - 静态成员函数 一、静态成员函数简介 1、静态成员函数概念 静态成员函数归属 : 在 C 类中 , 静态成员函数 是一种 特殊的函数 , 该函数…

记一次manjaro-i3系统sogoupinying候选词无法正常显示中文(变方框了)问题解决方案

记一次manjaro-i3系统sogoupinying候选词无法正常显示中文&#xff08;变方框了&#xff09;问题解决方案 前言解决方案 前言 今天早上发现公司电脑显卡驱动好像坏了&#xff0c;各种折腾完了干脆把系统搞黑屏无法开机了&#xff0c;时间有限懒再修了&#xff0c;于是重装了系…

【C++面向对象侯捷下】1.导读

文章目录 来源&#xff1a;我的百度网盘 百科全书 专家书籍 C标准库 C编译器

解锁学习新方式——助您迈向成功之路

近年来&#xff0c;随着吉林开放大学广播电视大学的崛起&#xff0c;越来越多的学子选择这所优秀的学府来实现自己的梦想。而作为一名学者&#xff0c;我有幸见证了电大搜题微信公众号的诞生&#xff0c;为广大学子提供了一个全新的学习支持平台。 电大搜题微信公众号&#xff…

解决老版本Oracle VirtualBox 此应用无法在此设备上运行问题

问题现象 安装华为eNSP模拟器的时候&#xff0c;对应的Oracle VirtualBox-5.2.26安装的时候提示兼容性问题&#xff0c;无法进行安装&#xff0c;具体版本信息如下&#xff1a; 软件对应版本备注Windows 11专业工作站版22H222621eNSP1.3.00.100 V100R003C00 SPC100终结正式版…

Android studio安卓生成APK文件安装包方法

1.点击Build->Generate Signed Bundle/APK 2.选择APK 3.首次生成&#xff0c;没有jks文件&#xff0c;就点击Create new。再次生成&#xff0c;直接点Next 4.选择创建jks文件路径 5.点击Next 6.选择release 7.生成完成的apk安装包路径

PostgreSql 统一修改date字段为timestamp

在《Powdersigner PostgreSql 同步表结构到pg数据库》中&#xff0c;导入表结构到pg数据后&#xff0c;发下时间对不上了。mysql的datetime转换后pg的变成了date了。 再同步到数据后&#xff0c;就变成日期类型了。 因为表中基本都有创建时间和修改时间&#xff0c;两个相对固…

电脑桌面透明便签软件是哪个?

在现代快节奏的工作环境中&#xff0c;许多上班族都希望能够在电脑桌面上方便地记录工作资料、重要事项、工作流程等内容。为了解决这个问题&#xff0c;一款优秀的电脑桌面便签软件是必不可少的。在选择桌面便签软件时&#xff0c;许多用户也希望便签软件能够与电脑桌面壁纸相…

文件操作(1)

1. 为什么使⽤⽂件&#xff1f; 如果没有⽂件&#xff0c;我们写的程序的数据是存储在电脑的内存中&#xff0c;如果程序退出&#xff0c;内存回收&#xff0c;数据就丢失 了&#xff0c;等再次运⾏程序&#xff0c;是看不到上次程序的数据的&#xff0c;如果要将数据进⾏持久…

CBOW (以txt文本小说为例) pytorch实战

CBOW &#xff08;以txt文本小说为例 pytorch实战 今天博主做了一个不错的实验&#xff0c;我认为&#xff0c;很多小伙伴可能都可以从中学到东西。 我先说一下这个实验&#xff0c;我做了什么&#xff0c;在这个实验中&#xff0c;博主会从零&#xff0c;开始从一个txt文件开…