【Linux】线程安全——补充|互斥、锁|同步、条件变量

news2024/11/16 11:41:42

文章目录

    • 一、知识补充
      • 线程的ID
      • 局部存储验证
      • Thread.hpp——线程的封装
    • 二、线程安全问题
    • 三、Linux线程互斥
      • 互斥相关概念
      • 互斥量mutex
      • mutex的使用
        • 全局锁的使用
        • 局部锁的使用
      • mutex的理解
      • Mutex.hpp——mutex的封装
      • 可重入VS线程安全
      • 死锁
    • 四、Linux线程同步
      • 条件变量
      • 条件变量接口
      • 理解条件变量
      • 条件变量的使用

一、知识补充

线程的ID

pthread_create创建一个线程,产生一个线程ID存放在第一个参数之中,该线程ID与内核中的LWP并不是一回事。pthread_create函数第一个参数指向一块虚拟内存单元,该内存单元的地址就是新创建线程ID,这个ID是线程库的范畴,而内核中LWP是进程调度的范畴,轻量级进程是OS调度的最小单位,需要一个数值来表示该唯一线程。

Linux并不提供真正的线程,只提供了LWP,但是程序员用户不管LWP,只要线程。所以OS在OS与应用程序之间设计了一个原生线程库,pthread库,系统保存LWP,原生线程库可能存在多个线程,别人可以同时在用。OS只需要对内核执行流LWP进行管理,而提供用户使用的线程接口等其他数据则需要线程库自己来管理。所以线程库需要对线程管理“先描述,在组织”。

线程库实际上就是一个动态库:

image-20230331120556954

进程运行时动态库加载到内存,然后通过页表映射到进程地址空间的共享区,这时候进程的所有线程都是能看到这个动态库的:

image-20230331121415523

每个线程都有自己独立的栈:主线程采用的栈是进程地址空间中原生的栈,其他线程采用的是共享区中的栈,每个线程都有自己的struct pthread,包含了对应线程的属性,每个线程也有自己的线程局部存储(添加__thread可以将一个内置类型设置为线程局部存储),包含对应的线程被切换时的上下文。每一个新的线程在共享区都有一块区域对其描述,所以我们要找到一个用户级线程只需要找到该线程内存块的起始地址就可以获取到该线程的信息了:

image-20230331122833069

线程函数起始是在库内部对线程属性进行操作,最后将要执行的代码交给对应的内核级LWP去执行。所以线程数据的管理本质在共享区。

线程ID本质就是进程地址空间共享区上的一个虚拟地址:

void* start_routine(void*args)
{
    while(true)
    {
        printf("new thread tid:%p\n",pthread_self());
        sleep(2);
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,start_routine,nullptr);
    while(true)
    {
        printf("main thread tid:%p\n",pthread_self());
    }
    return 0;
}

image-20230331134341468

局部存储验证

给一个全局变量g_val,让一个线程进行++,其他线程会受到影响:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
int g_val = 100;
void* start_routine(void*args)
{
    string name = static_cast<const char*>(args);
    while(true)
    {
        cout<<name<<" running ... "<<"g_val: "<<g_val<<"&g_val: "<<&g_val<<endl;
        sleep(1);
        ++g_val;
    }
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,start_routine,(void*)"thread 1");
    while(true)
    {
        printf("main thread g_val: %d &g_val: 0x%x\n",g_val,&g_val);
        sleep(1);
    }
    pthread_join(tid,nullptr);
    return 0;
}

image-20230404232354409

此时在给全局变量g_val加上__thread:

image-20230404232623125

从输出结果上看,g_val此时就不再是共享了,每个线程独有。添加__thread可以将一个内置类型设置为线程局部存储。每个线程都有一份,介于全局变量和局部变量之间线程特有属性。

Thread.hpp——线程的封装

我们如果想要跟C++一样使用,创建使用线程时,直接构造对象设置回调函数,对线程原生接口可以进行简单的封装:

#include <iostream>
#include <pthread.h>
#include <cassert>
#include <cstring>
#include <functional>
class Thread;
//上下文
class Context
{
public:
    Thread *this_;
    void *args_;
public:
    Context():this_(nullptr),args_(nullptr)
    {}
    ~Context()
    {}
};
class Thread
{
public:
    typedef std::function<void*(void*)> func_t;
    const int num = 1024;
public:
    Thread(func_t func,void*args,int number):func_(func),args_(args)
    {
        char buffer[num];
        snprintf(buffer,sizeof(buffer),"thread-%d",number);
        name_=buffer;
        Context*ctx = new Context();
        ctx->this_ = this;
        ctx->args_=args_;
        int n = pthread_create(&tid_,nullptr,start_routine,ctx);
        assert(n==0);
        (void)n;
    }
    static void*start_routine(void*args)
    {
        Context*ctx = static_cast<Context*>(args);
        void*ret = ctx->this_->run(ctx->args_);
        delete ctx;
        return ret;
    }
    // void start()
    // {
    //     Context*ctx = new Context();
    //     ctx->this_ = this;
    //     ctx->args_=args_;
    //     int n = pthread_create(&tid_,nullptr,start_routine,ctx);
    //     assert(n==0);
    //     (void)n;
    // }
    void join()
    {
        int n = pthread_join(tid_,nullptr);
        assert(n==0);
        (void)n;
    }
    void*run(void*args)
    {
        return func_(args);
    }

    ~Thread()
    {}

private:
    std::string name_;
    pthread_t tid_;
    func_t func_;
    void* args_;
};

main.cc

void* thread_run(void* args)
{
    std::string name = static_cast<const char *>(args);
    while(true)
    {
        cout << name << endl;
        sleep(1);
    }
}
int main()
{
    std::unique_ptr<Thread> thread1(new Thread(thread_run,(void*)"hellothread",1));
    std::unique_ptr<Thread> thread2(new Thread(thread_run,(void*)"COUTthread",2));
    std::unique_ptr<Thread> thread3(new Thread(thread_run,(void*)"PRINTthread",3));

    //thread1->start();
    //thread2->start();
    //thread3->start();
    thread1->join();
    thread2->join();
    thread3->join();
    return 0;
}

二、线程安全问题

全局变量g_val可以被多个线程同时访问,可以被多个线程访问是共享资源,多个线程对其进行操作,可能会出现问题:

下面模拟抢票的过程,多个线程对共享资源tickets做–的过程:

#include "Thread.hpp"
using std::cout;
using std::endl;
//共享资源
int tickets = 1000;
void* get_ticket(void* args)
{
    std::string name = static_cast<const char *>(args);
    while(true)
    {
        if(tickets>0)
        {
            usleep(1234);
            cout<<name<<"正在抢票 : "<<tickets<<endl;
            tickets--;
        }
        else
        {
            break;
        }
    }
    return nullptr;
}
int main()
{
    std::unique_ptr<Thread> thread1(new Thread(get_ticket,(void*)"hellothread",1));
    std::unique_ptr<Thread> thread2(new Thread(get_ticket,(void*)"COUTthread",2));
    std::unique_ptr<Thread> thread3(new Thread(get_ticket,(void*)"PRINTthread",3));
    std::unique_ptr<Thread> thread4(new Thread(get_ticket,(void*)"TESTthread",4));

    thread1->join();
    thread2->join();
    thread3->join();
    thread4->join();
    return 0;
}

image-20230405001609431

此时结果出现了负数,在现实生活中,抢票怎么可能出现负数

结果出现负数:

如果需要出现负数的现象:尽可能让多个线程交叉执行,多个线程交叉执行的本质:让调度器尽可能的频繁发生线程调度与切换

线程一般什么时候发生线程切换时间片到了或者来了更高优先级的线程或者线程等待的时候

线程什么时候检测上面的问题:从内核态返回用户态的时候,线程要对调度状态进行检测,如果可以就直接发生线程切换

出现负数的票就是因为多个线程交叉执行,多个线程交叉执行的本质:让调度器尽可能频繁的发生线程的调度与切换

tickets==1时,所有进程都可以进去,然后在判断:1.读取内存数据cpu内的寄存器中2.进行判断;第一个线程判断是大于0的,此时线程会被切走,寄存器只有一个,寄存器的内容是当前执行流的上下文,会把上下文带走,还没来得及tickets–,其他线程看到的tickets也是1,也要保存自己的上下文…ticket减减前线程都会休眠一会,当一个线程唤醒tickets–

--的本质就是1.读取数据2.更改数据3.写回数据

对一个全局变量进行多线程更改是不安全的:

对变量进行++或者–,在C、C++上看起来只有一条语句,但是汇编之后至少是三条语句:

1.从内存读取数据到CPU寄存器中2.在寄存器中让CPU进行对应的算逻运算3.写回新的结果到内存中变量的位置

现在线程1把数据加载到寄存器中,做–,成为999,到第三步的时候写回到内存的时候很不幸被切走了,把上下文顺便也卷走了:

image-20230419193926618

此时调度线程2,线程2很开心,一直在–,到1tickets变为100的时候,内存中变量的也变为了100,但是当它想继续–的时候,线程2倍切走了,带着自己的上下文走了,现在线程1回来了:恢复上下文,继续之前的第三步,此时线程2好不容易把tickets变为100,但是被线程1改为了999

image-20230419194254020

又变成了999,造成了干扰

image-20230419194457726

由此可知我们定义的全局变量在没有保护的时候,往往是不安全的,像上面的例子,多个线程交替执行时造成数据安全问题,发生了数据不一致问题

而解决这种问题的办法就是加锁!


三、Linux线程互斥

互斥相关概念

临界资源:多个执行流进行安全访问的共享资源就叫临界资源

临界区:多个执行流进行访问临界资源的代码就是临界区

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

原子性: 不会被任何调度机制打断的操作,该操作只有两态,要么不做,要么做完,这就是原子性。

现在先简单理解原子性:一个资源进行的操作如果只用一条汇编语句就能完成,就是原子性的,反之不是原子的。

对变量++或者–。在C、C++上,看起来只有一条语句,但是汇编之后至少是三条语句:

1.从内存读取数据到CPU寄存器中

2.在寄存器中让CPU进行对应的算逻运算

3.写回新的结果到内存中变量的位置

对一个资源访问的时候,要么不做,要么做完,不是原子性的情况:线程A被切换,没做完,有中间状态,不是原子性。

实际上对变量做–的时候,对应三条汇编语句,未来会对应三条汇编语句!所以很明显,++、–不是原子性的,不是一条语句。

单纯的++或者++都不是原子的,有可能会有数据一致性的问题。

互斥量mutex

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

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

多个线程并发的操作共享变量,会带来问题:数据不一致问题

要解决线程不安全的情况,保护共享资源:

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

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

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

实际上就是需要一把锁,Linux提供的这把锁就叫互斥量,如果一个线程持有锁,那么其他的线程就无法进来访问了。

常见的相关接口:

#include <pthread.h>
// 初始化
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
// 销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
// 全局
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//成功返回0,失败返回错误码

即可以定义成局部的,也可以定义成全局的。

pthread_mutex_t是锁的类型,如果我们定义的锁是全局的,就不要用pthread_mutex_int和pthread_mutex_destroy初始化和销毁了。

#include <pthread.h>
//加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);

//如果加锁成功,直接持有锁,加锁不成功,此时立马出错返回(试着加锁,非阻塞获取方式)
int pthread_mutex_trylock(pthread_mutex_t *mutex);

//解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
// 成功返回0,失败返回错误码

mutex的使用

全局锁的使用

使用全局锁+4个线程的代码:

定义全局锁并初始化PTHREAD_MUTEX_INITIALIZER,同时用pthread_create创建4个线程进行测试,由于此时锁是全局的,我们不需要把锁传给每个线程:

#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <pthread.h>
using std::cout;
using std::endl;
#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <pthread.h>
using std::cout;
using std::endl;
int tickets = 1000;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void* get_ticket(void* args)
{
    std::string username = static_cast<const char *>(args);
    while(true)
    {
        pthread_mutex_lock(&lock);
        if(tickets>0)
        {
            usleep(11111);
            cout<<username<<"正在抢票 : "<<tickets<<endl;
            tickets--;
            pthread_mutex_unlock(&lock);
        }
        else
        {
            pthread_mutex_unlock(&lock);
            break;
        }
    }
    return nullptr;
}
int main()
{
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, nullptr,  get_ticket, (void *)"thread 1");
    pthread_create(&t2, nullptr,  get_ticket, (void *)"thread 2");
    pthread_create(&t3, nullptr,  get_ticket, (void *)"thread 3");
    pthread_create(&t4, nullptr, get_ticket, (void *)"thread 4");

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
    pthread_join(t4, nullptr);
    return 0;
}

image-20230419204523501

局部锁的使用

局部锁+for循环创建线程的代码:

此时的锁是局部的,为了把锁传递给每个线程,我们可以定义一个结构体ThreadData,存放着线程名与锁:

#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <pthread.h>
using std::cout;
using std::endl;
int tickets = 1000;
class ThreadData
{
public:
    ThreadData(const std::string&threadname,pthread_mutex_t *mutex_p)
        :threadname_(threadname),mutex_p_(mutex_p)
    {}
    ~ThreadData(){}
public:
    std::string threadname_;
    pthread_mutex_t *mutex_p_;
};
void* get_ticket(void* args)
{
    ThreadData*td = static_cast<ThreadData *>(args);
    while(true)
    {
        pthread_mutex_lock(td->mutex_p_);
        if(tickets>0)
        {
            usleep(11111);
            cout<<td->threadname_<<"正在抢票 : "<<tickets<<endl;
            tickets--;
            pthread_mutex_unlock(td->mutex_p_);
        }
        else
        {
            pthread_mutex_unlock(td->mutex_p_);
            break;//注意这里有break
        }
    }
    return nullptr;
}
int main()
{
 #define NUM 4
     pthread_mutex_t lock;
     pthread_mutex_init(&lock,nullptr);
     std::vector<pthread_t> tids(NUM);
     for(int i =0;i<NUM;i++)
     {
         char buffer[64];
         snprintf(buffer,sizeof(buffer),"thread %d",i+1);
         ThreadData *td = new ThreadData(buffer,&lock);
         pthread_create(&tids[i],nullptr,get_ticket,td);
     }
     for(const auto&tid:tids)
     {
        pthread_join(tid,nullptr);
     }
    pthread_mutex_destroy(&lock);
    return 0;
}

image-20230419204133974

此时的运行结果每次都是能够减到1,但是运行的速度也变慢了。这是因为加锁和加锁的过程是多个线程串行执行的,程序变慢了

同时这里看到每次都是只有一个线程在抢票,这是因为锁只规定互斥访问,并没有规定谁来优先执行所以谁的竞争力强就谁来持有锁。

要想解决这个问题:想想抢完票就结束了吗?实际的生活当中,抢完票后还有一些工作需要完成:比如发送订单

image-20230419204953465

image-20230419205004976

至此解决抢票的问题。

mutex的理解

  • 看待锁

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

pthread_mutex_lock、pthread_mutex_unlock:加锁和解锁的过程必须是安全的!加锁的过程其实是原子的

谁持有锁,谁就进入临界区!

如果申请成功,就继续向后执行,如果申请暂时没有成功,执行流会如何:第一次加锁,然后在加一次锁:结果会怎么样:

image-20230419205806708

此时运行,程序不在执行,执行流会阻塞!

image-20230419210512025

pthread_mutex_trylock:尝试去加锁,如果加锁成功就持有锁,加锁不成功立马出错返回

一般把这种锁称为挂起等待锁:如果申请暂时没有成功,执行流会阻塞,等待锁成功释放!

  • 原子性概念理解

image-20230419210932013

如果线程1,申请锁成功,进入临界资源,正在访问临界资源期间,其他线程在做:阻塞等待

如果线程1,申请锁成功,进入临界资源,正在访问临界资源期间,我可以被切换!!绝对可以

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

所以,对于其他线程而言,有意义的锁的状态,无非两种:1.申请锁前2.释放锁后

站在其他线程的角度,看待当前线程持有锁的过程就是原子的

结论

**未来我们使用锁的时候:一定要尽量保证临界区的粒度要非常小(粒度:锁中间保护代码的多少)**注意:加锁是程序员行为,必须要做到要加就都要加!(公共资源,要么加锁,要么不加锁,这是程序员行为,不要写BUG)!

理加锁和解锁的本质:加锁的过程是原子的!加锁了,未来解锁的一定是一个执行流。

  • 互斥锁实现原子性原理

单纯的i++,++i都不是原子的,会导致数据不一致问题

从汇编谈加锁:为了实现互斥锁操作,大多数体系结构提供了swap和exchange指令,作用是把寄存器和内存单元的数据直接做交换,由于只用一条指令,就可以保证原子性

image-20230419211948096

加锁

image-20230419212503660

这里的xchgb指令可以把cpu中的数据和内存中的数据直接交换

image-20230419213809180

image-20230419214158235

解锁:过程很简单,把寄存器的内容1移动到内存中,直接return,解锁完成

Mutex.hpp——mutex的封装

如果我们想简单的使用,该如何进行封装设计 ——做一个简单设计,传入一个锁自动帮我们加锁和解锁,RAII风格加锁

Mutex.hpp

//Mutex.hpp
#pragma once
#include <iostream>
#include <pthread.h>
class Mutex
{
public:
    Mutex(pthread_mutex_t *lock_p=nullptr):lock_p_(lock_p)
    {}
    
    void lock()
    {
        if(lock_p_) pthread_mutex_lock(lock_p_);
    }

    void unlock()
    {
        if(lock_p_) pthread_mutex_unlock(lock_p_);
    }
    ~Mutex()
    {

    }
private:
    pthread_mutex_t *lock_p_;
};
class LockGuard
{
public:
    LockGuard(pthread_mutex_t *mutex):mutex_(mutex)
    {
        mutex_.lock();//在构造函数中加锁
    }
    ~LockGuard()
    {
        mutex_.unlock();//在析构函数中解锁
    }

private:
    Mutex mutex_;
};

main.cc

using std::cout;
using std::endl;
int tickets = 1000;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void *get_ticket(void *args)
{
    std::string username = static_cast<const char *>(args);
    while (true)
    {
        {//代码块,不给usleep加锁
            LockGuard lockguard(&lock);
            if (tickets > 0)
            {
                usleep(1111);
                cout << username << "正在抢票 : " << tickets << endl;
                tickets--;
            }
            else
            {
                break;
            }
        }
        usleep(1000);
    }
    return nullptr;
}
int main()
{
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, nullptr, get_ticket, (void *)"thread 1");
    pthread_create(&t2, nullptr, get_ticket, (void *)"thread 2");
    pthread_create(&t3, nullptr, get_ticket, (void *)"thread 3");
    pthread_create(&t4, nullptr, get_ticket, (void *)"thread 4");

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
    pthread_join(t4, nullptr);
    pthread_join(t4, nullptr);
    return 0;
}

可重入VS线程安全

可重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流在次进入,我们称为重入。

一个函数在重入的情况的下,运行结果不会出现任何不同回或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数

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

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

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

常见的线程安全的情况:

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

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

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

常见不可重入的情况

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

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

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

死锁

一组执行流(不管进程还是线程)持有自己锁资源的同时,还想要申请对方的锁,锁是不可抢占的(除非自己主动归还),会导致多个执行流互相等待对方的资源,而导致代码无法推进。这就是死锁

ps:一把锁可以造成死锁,在抢票的时候我们就写过,在加一把锁导致死锁。

推导链:为什么会有死锁:一定是你用了锁——锁保证临界资源的安全,多线程访问我们可能出现数据不一致的问题——多线程、全局资源——多线程大部分资源(全局的)是共享的——多线程的特性,解决问题的同时带来了新的问题:死锁,任何技术都有自己的边界,在解决问题的同时一定可能会引入新的问题

死锁四个必要条件:

1.互斥:一个共享资源每次被一个执行流使用

2.请求与保持:一个执行流因请求资源而阻塞,对已有资源保持不放

3.不剥夺:一个执行流获得的资源在未使用完之前,不能强行剥夺

4.环路等待条件:执行流间形成环路问题,循环等待资源

避免死锁,1.破坏死锁的四个必要条件2.加锁顺序一致3.避免锁未释放的场景4.资源一次性分配

避免死锁算法(了解):死锁检测算法、银行家算法


四、Linux线程同步

引入一些情景:自习室VIP,先到先得,上厕所时反锁,别人进不去,离资源近竞争力强,一直是你自己,重复放钥匙拿钥匙,造成其他人饥饿状态;再比如抢票系统我们看到一个线程一直连续抢票,造成了其他线程的饥饿,为了解决这个问题:我们在数据安全的情况下让这些线程按照一定的顺序进行访问,这就是线程同步

饥饿状态:得不到锁资源而无法访问公共资源的线程处于饥饿状态。但是并没有错,但是不合理

竞态条件:因为时序问题,而导致程序异常,我们称为竞态条件。

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

条件变量

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

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

条件变量通常需要配合互斥锁一起使用。

条件变量的使用:一个线程等待条件变量的条件成立而被挂起;另一个线程使条件成立后唤醒等待的线程。

条件变量接口

#include <pthread.h>
//初始化
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
//销毁
int pthread_cond_destroy(pthread_cond_t *cond);
#include <pthread.h>
//特定时间阻塞等待
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
       pthread_mutex_t *restrict mutex,
       const struct timespec *restrict abstime);
//等待
int pthread_cond_wait(pthread_cond_t *restrict cond,
       pthread_mutex_t *restrict mutex);
#include <pthread.h>
// 唤醒一批线程
int pthread_cond_broadcast(pthread_cond_t *cond);
// 唤醒一个线程
int pthread_cond_signal(pthread_cond_t *cond);

理解条件变量

举个例子:公司进行招聘:应聘者要面试,大家不能同时进入房间进行面试,但是没有由于没有组织,上一个人面试完之后,面试官打开门准备面试下一个,一群人在外面等待面试,但是有人抢不过别人,人太多了,面试官记不住谁面试过了,所以有可能一个人面试完之后又去面试了,造成其他人饥饿问题,这时候效率很低

后来hr重新进行管理:设立一个等待区,先排队去等待区进行等待面试,现在每个人都进行排队,都有机会面试了,而这个等待区就是条件变量,如果一个人想面试,先得去排队等待区等待,未来所有应聘者都要去条件变量等

image-20230409143410229

条件不满足的时候,线程必须去某些定义好的条件变量上进行等待

条件变量(struct cond,结构体)里面包含状态,队列,而我们定义好的条件变量包含一个队列,不满足条件的线程就链接在这个队列上进行等待。

image-20230419235917050

条件变量的使用

通过条件变量来控制线程的执行

条件变量本身不具备互斥的功能,所以条件变量必须配合互斥锁使用:

  • 一次唤醒一个线程

创建2个线程,通过条件变量一秒唤醒一个线程(或者全部唤醒):

int tickets = 1000;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
void* start_routine(void* args)
{
    string name = static_cast<const char*>(args);
    while(true)
    {
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond,&mutex);
        //判断省略
        cout<<name<<" -> "<<tickets<<endl;
        tickets--;
        pthread_mutex_unlock(&mutex);
    }
}
int main()
{
    pthread_t t1,t2;
    pthread_create(&t1,nullptr,start_routine,(void*)"thread 1");
    pthread_create(&t1,nullptr,start_routine,(void*)"thread 2");

    while(true)
    {
        sleep(1);
        pthread_cond_signal(&cond);
        cout<<"main thread wakeup one thread..."<<endl;
    }
    pthread_join(t1,nullptr);
    pthread_join(t2,nullptr);

    return 0;
}

image-20230420000722967

主线程一个一个去叫,按照一定的顺序输出打印。

  • 一次唤醒一大批线程
int tickets = 1000;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
void* start_routine(void* args)
{
    string name = static_cast<const char*>(args);
    while(true)
    {
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond,&mutex);
        //判断省略
        cout<<name<<" -> "<<tickets<<endl;
        tickets--;
        pthread_mutex_unlock(&mutex);
    }
}
int main()
{
    pthread_t t1,t2;
    pthread_t t[5];
    for(int i = 0;i<5;i++)
    {
        char*name = new char[64];
        snprintf(name,64,"thread %d",i+1);
        pthread_create(t+i,nullptr,start_routine,name);
    }
    while(true)
    {
        sleep(1);
        pthread_cond_broadcast(&cond);
        cout<<"main thread wakeup one thread..."<<endl;
    }
    for(int i = 0;i<5;i++)
    {
        pthread_join(t[i],nullptr);
    }
    return 0;
}

image-20230420001539170

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

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

相关文章

工序流转二维码的应用和制作方法

很多中小型生产企业还在使用纸制的“工序流转卡”&#xff0c;每天交给专员人工录入到电脑上&#xff0c;不仅费时费力&#xff0c;还容易出错&#xff0c;更重要的是管理员不能实时掌握各个订单的进展情况&#xff0c;因此经常会发生订单延期交付的情况&#xff0c;给企业带来…

centos7.9系统部署NFS详细流程—2023.04

文章目录 NFS与RPC关系前提关闭防火墙和selinux安装 NFS 和 RPC测试取消挂载 NFS与RPC关系 简单点可以这么理解&#xff0c;RPC和NFS的关系&#xff1a;NFS是一个文件系统&#xff0c;而RPC是负责负责信息的传输。 NFS&#xff08;Network File System&#xff09;即网络文件…

Python数据分析项目实战

现成案例分享 Python数据分析&#xff1a;股票数据分析案例 步骤&#xff1a; 准备数据可视化数据、审查数据处理数据根据ACF、PACF定阶拟合ARIMA模型预测 作者&#xff1a;python分享站 链接&#xff1a;https://www.zhihu.com/question/280744341/answer/1651341817 来源&…

Windows下载redis

下载微软的 Redis和配置 Windows下载redis1&#xff0c;下载redis2&#xff0c;解压压缩包3&#xff0c;启动Redis临时服务4&#xff0c;启动Redis客户端测试连接5, Redis配置 &#xff08;可选&#xff09;1&#xff0c; 配置系统环境变量2&#xff0c;添加Redis服务3&#xf…

数据结构——排序(5)

作者&#xff1a;几冬雪来 时间&#xff1a;2023年4月14日 内容&#xff1a;数据结构排序内容讲解 目录 前言&#xff1a; 1.非递归归并排序&#xff1a; 2.越界情况&#xff1a; 3.计数排序&#xff1a; 结尾&#xff1a; 前言&#xff1a; 在上一篇博客中我们对归…

硬件语言Verilog HDL牛客刷题day10 华W部分 和 DJ部分

1. VL63 并串转换 1.题目&#xff1a; 设计一个模块进行并串转换&#xff0c;要求每四位d输为转到一位dout输出&#xff0c;输出valid_in表示此时的输入有效。 2.解题思路 2.1 计数记录输出的位数。 2.2 数据 有一个延时。 2.3 思路就是 搞一个寄存器存储数据&#xff0c;然后…

【蓝桥系列】为什么不会「输入输出」,你一道题都做不出来?(输入输出模板)

欢迎各位旅行者&#xff0c;来到小蓝の蓝桥城&#xff01; 全文目录 &#x1f4d6;第一幕 「藏宝地图」 &#x1f33c; 输入 &#x1f331;输入一行 &#x1f331;输入多行 &#x1f33c; 输出 &#x1f331;输出 一个数 &#x1f331;输出 一维列表 &#x1f331;输出…

TCP知识

计算机网络模型 OSC(Open System Interconnect)采用分层的结构化技术&#xff0c;共分七层&#xff1a;物理层&#xff0c;数据链路层&#xff0c;网络层&#xff0c;传输层&#xff0c;会话层&#xff0c;表示层&#xff0c;应用层。 TCP/IP模型共分四层&#xff1a;链路层&…

干货分享 | 图解如何写出优秀的项目总结?

不论是公司&#xff0c;还是个人&#xff0c;要不断地提升&#xff0c;就要不断地进行总结与改进&#xff0c;再总结再改进&#xff0c;如此循环&#xff0c;通过不断地总结与改进&#xff0c;改善我们的工作方法、优化工作流程、提升工作效率。 ​这就是PDCA基本思路&#xf…

ROS 教程之 vision : 用笔记本摄像头获取图像

如何用笔记本自带的摄像头采集图像 尝试一&#xff1a;安装Webcam 驱动 参考 大神白巧克力亦唯心博客&#xff1a; 链接: ROS 教程之 vision : 用各种摄像头获取图像. sudo apt-get install git-core cd ~/catkin_ws/src #catkin_ws/src对应你自己建立的catkin工作空间…

区间动态规划

区间DP 石子合并&#xff1a;前缀和动态规划最长合法子序列环形石子合并石子合并 II城镇国王超级括号序列炸弹人 区间DP&#xff1a; 状态&#xff1a;区间左右端点 dp[i][j]阶段&#xff1a;区间长度转移&#xff1a;由外到内 石子合并&#xff1a;前缀和动态规划 问题特征&…

第3章 数据科学的5个步骤

第3章 数据科学的5个步骤 文章目录 第3章 数据科学的5个步骤3.1 数据科学简介3.2 5个步骤概览3.2.1 提出有意思的问题3.2.2 获取数据3.2.3 探索数据3.2.4 数据建模3.2.5 可视化和分享结果3.3.1 数据探索的基本问题3.3.2 数据集1&#xff1a;Yelp点评数据DataFrameSeries定性数据…

Steam无法载入网页 - 解决方案

前言 用户在使用Steam客户端时经常会遇到无法载入网页的情况&#xff0c;如下图。下文介绍解决方案。 解决方案 检查防火墙 打开Windows设置&#xff0c;选择更新和安全&#xff0c;选择Windows安全中心 - 防火墙和网络保护&#xff0c;如下图&#xff1a; 点击允许应用通…

提升业务韧性的最佳路径,亚马逊云科技帮您打开成本优化的路径

众所周知&#xff0c;当你想要从头开始建立一个云财务管理计划似乎是稍有难度的。因此&#xff0c;亚马逊云科技解构了4个云财务管理CFM原则——查看、节省、计划和运营——并分享可以实施的操作指南&#xff0c;帮助您在云上取得成功。 云成本管理工具 亚马逊云科技提供一系…

学生信息管理系统(student information manage system, SIMS)

一、前言 本项目为学生信息管理系统&#xff0c;使用C语言编写。 ★★★项目详见本人gitee仓库&#xff0c;地址 https://gitee.com/omnipotent-brother/student-information-manage-system.git ★★★ 二、项目介绍 开发环境&#xff1a; 基于windows 11系统下的Visual Studio…

SQL Server 服务器安装配置和使用

目录 一、SQL Server概述 1、SQL Server 环境需求 2、SQL Server的特点和组成 &#xff08;1&#xff09;SQL Server 特点 &#xff08;2&#xff09;SQL Server 基本组成 3、SQL Server 安装需注意 &#xff08;1&#xff09;数据文件的存储位置 &#xff08;2&#xf…

【Visual Studio Code】编码速度提升小技巧

简言 用了这么久的vscode。在此记录下我常用的快捷键和小技巧。 小技巧 这个是vscode的工作界面。 值得一提的是&#xff0c;界面下边一行是快捷显示信息和快捷操作。可以快速了解打开文件的描述信息。 使用扩展 在使用vscode的时候&#xff0c;一定要安装相应的扩展包。 扩…

08 - 智能编程语言

一、为什么需要智能编程语言 1. 语义鸿沟 传统C/C++等是以面向通用计算的加、减、乘、除等基本标量操作为基础的,通常不具有和具体任务及应用场景相关的高层语义。例如:使用纯标量计算的C++语言编写的卷积运算包含7重循环,而采用向量语义的Python语言编写的卷积运算只需要…

PowerToys——免费、强大、高效的微软官方效率提升工具集,办公学习宝藏软件

名人说&#xff1a;博观而约取&#xff0c;厚积而薄发。——宋苏轼 Code_流苏(CSDN)&#xff08;一个喜欢古诗词和编程的Coder&#x1f60a;&#xff09; 目录 一、简单介绍1、PowToys是什么&#xff1f;2、它的功能有哪些&#xff1f; 二、下载安装三、功能示例1、始终置顶2、…

弱算法MD5、SHA1、DES、AES CBC-修复建议

一、修复原则 修复原则上2条走&#xff1a; 1、新产品、新业务 不应使用弱算法&#xff0c;如果使用应及时修复 2、已经产品 【金融类】业务应自行根据涉及广度 排期修复 【非金融】 类&#xff0c;在修复难度大、涉及面广的情况下 可以暂时不修复&#xff0c;择机修复。 …