线程互斥与同步

线程的互斥与同步 什么是互斥?什么是同步? 互斥:一个资源一次只能被一个访问者使用(保证访问数据,唯一访问性) 举个例子:你去上厕所,门一开,人进去,门锁上,在你上厕所期间别人不能打扰 同步:当多个访问者一起工作时并对每个访问者访问的时序有一定限制(保证访问数据,时序访问性) 举个例子:你上完厕所,刚出来,本来应该由别人了,但是你此时又进去,你反复的进去出来,导致别人都上不了厕所。因此同步就是保证时序性的,... 阅读详情

线程互斥

进程之间如果要进行通信我们需要先创建第三方资源(管道,共享内存等),让不同的进程看到同一份资源从而做到通信的目的。进程间通信中的第三方资源就叫做临界资源,访问第三方资源的代码就叫做临界区

在多线程中,大部分数据都是共享的(比如全局变量,文件描述符等)。多线程下,为了更好的临界区的保护,让多执行流在任何时刻,都只能有一个线程进入临界区访问临界资源。这种情况就称之为互斥。

在多线程下,访问临界资源时如果不加互斥行为(就是多线并发操作共享数据),会带来一些问题。
比如下面这个例子:
主线程一次性创建出了五个线程,这五个线程并发的对全局变量count做–。

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;

int count = 10000;

void* TouchThread(void* arg)
{
    while(1)
    {
        if(count > 0)
        {
            usleep(1000);
            cout << "thread " <<(long long)arg << ":" << count << endl;
            count--;
        }
        else
        {
            break;
        }
    }
    return nullptr;
}
int main()
{
    const int NUM = 5;
    pthread_t tid[NUM];
    //create thread
    for(size_t  i = 0 ; i < NUM; ++i)
    {
        pthread_create(tid+i,nullptr,TouchThread,(void*)(i+1));
    }

    //join thread
    for(size_t i = 0; i < NUM; ++i)
    {
        pthread_join(tid[i],nullptr);
    }

    return 0;
}

运行结果:

recording.gif

可以看出,多线程确实快。使用timetime 可执行程序可以统计程序运行的时间。我这里分别创建了2个,5个,10个线程分别测试了。结果如下:

image.png

image.png

image.png
但是,程序最终的结果是错误的。全局变量count的值都变为负数了。

image.png
为什么会出现负数:

  • if 语句判断条件为真以后,代码可以并发的切换到其他线程
  • usleep 这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段。
  • count-- 操作本身就不是一个原子操作

原子性:不会被调度机制打断,该操作的状态只有两种,要么成功,要么不成功。

--count这个操作,对应的汇编语言实际上一共要执行三步。(原子性也可以用汇编指令来衡量,只用一条汇编指令就可以完成的操作,则认为他是原子的)
count–的汇编指令如下:

image.png
count–的操作并不是原子的,对应的三条汇编指令。

  • load:将共享变量count从内存加载到寄存器中。
  • update:更新寄存器里面的值,执行-1操作。
  • store :将新值,从寄存器写回共享变量count的内存地址

image.png
count–需要三步才能完成。当线程1将内存中的数据load到寄存器的时候,调度器可能就会线程1切走了,假设此时线程1读到的数据是10000,线程1被切走时,10000属于线程1的上下文信息,CPU会保存线程的上下文信息,然后这个线程就会被挂起。

假设此时线程2被调度器调度,CPU开始执行线程2了。而此时,线程2拿到内存中的count属于全局变量,还是10000。而系统给线程2的时间片可能较多,导致线程2一次性执行了很多次--才被切走,最终将count由1000减到了9000。

此时调度器再把线程1恢复上来,寄存器中保存着线程1的上下文信息,并且要将线程1曾经的硬件上下文信息恢复出来,此时寄存器当中的值是恢复出来的10000,然后线程1继续执行--操作的第二步和第三步,最终将9999写回内存。

上述过程中,同样的数据,对于两个线程拿到的结果却是不一样的。如果这个数据涉及金钱,那么后果不堪设想。

因此对一个变量进行–操作并不是原子的,虽然count–看起来就是一行代码,但这行代码被编译器编译后本质上是三行汇编。
要解决这一个问题,需要做到三点:

  1. 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
  2. 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
  3. 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

要做到这三点,需要给临界区上一把锁。Linux上提供的这把锁叫互斥量。

互斥量 mutex

在临界区访问临界资源的时候加一把锁。做到让临界区的代码只允许一个线程执行,不允许多个线程同时执行。
image.png

互斥量接口

互斥量就是phtread库中提供的一个数据类型,叫做pthread_mutex_t。
使用互斥量之前要初始化,使用完之后要销毁互斥量。

  • 初始化

    • 全局或者静态互斥量初始化(使用宏初始化)
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
    
    • 局部互斥量初始化(使用init函数初始化)
    int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
    
    • 参数一:mutex要初始化的互斥量。
    • 参数二:attr设置互斥量的属性,一般设置为NULL即可。
    • 返回值:成功返回0,失败返回错误码
  • 销毁

    int pthread_mutex_destroy(pthread_mutex_t *mutex);
    
    • 参数一:mutex要销毁的互斥量
    • 返回值:成功返回0,失败返回错误码

    销毁互斥量时要注意:

    1. 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
    2. 不能销毁一个已经加锁的互斥量
    3. 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
  • 互斥量的加锁和解锁

    • 加锁
    int pthread_mutex_lock(pthread_mutex_t *mutex);
    
    • 解锁
    int pthread_mutex_unlock(pthread_mutex_t *mutex);
    

    参数和返回值与上面的接口都一样。

调用pthread_mutex_lock给互斥量加锁时,可能会遇到下面的情况:

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

对上面的例子进行互斥加锁行为如下:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;

int count = 10000;
//线程数据
class ThreadData
{
public:
    ThreadData(string &name, pthread_mutex_t *mutex)
        : _name(name), _mutex(mutex)
    {}

public:
    string _name;
    pthread_mutex_t *_mutex;
};

void *TouchThread(void *arg)
{
    ThreadData *td = (ThreadData *)arg;
    while (1)
    {
        pthread_mutex_lock(td->_mutex); // 加锁
        if (count > 0)
        {
            usleep(1000);
            cout << td->_name << ":" << count << endl;
            --count;
            pthread_mutex_unlock(td->_mutex); // 解锁
        }
        else
        {
            pthread_mutex_unlock(td->_mutex); // 解锁
            break;
        }
    }

    delete td;
    return nullptr;
}

int main()
{
    const int NUM = 5;
    pthread_t tid[NUM];
    pthread_mutex_t mutex;            // 互斥量
    pthread_mutex_init(&mutex, NULL); // 初始化互斥量

    // create thread
    for (size_t i = 0; i < NUM; ++i)
    {
        string name = "thread";
        name += to_string(i + 1);
        ThreadData *td = new ThreadData(name, &mutex);//线程的信息,需要名字和互斥量
        pthread_create(tid + i, nullptr, TouchThread, (void *)td);
    }

    // join thread
    for (size_t i = 0; i < NUM; ++i)
    {
        pthread_join(tid[i], nullptr);
    }

    pthread_mutex_destroy(&mutex); // 销毁互斥量
    return 0;
}

这里创建了一个局部互斥量,给每一个线程传参数时不仅要传互斥量还要传线程名。这里使用一个类ThreadData封装了这两个信息传给回调函数。

运行结果:

recording.gif
可以明显看出,加锁之后,速度大不如前。但是最终的结果是正确的,也可以看出,每个线程都是运行一段时间后再让其他线程运行。而不像最之前,五个线程是一起并行执行。

上面的代码在临界区之前进行了对互斥量加锁的行为。加锁之后,只有拿到互斥量的线程才能访问临界资源,没有拿到互斥量的只能阻塞等待。(因为给互斥量加锁时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量, 那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁)这样就能保证线程互斥,临界资源只允许一个线程访问,不允许多线程并行访问。

一旦访问完临界资源,就应该对互斥量进行解锁,让其他线程竞争到这个互斥量。所以,在对互斥量加锁的时候要保证力度越小越好。也就是临界区的代码越少越好。(上面的代码中,第26行if语句,对临界资源进行判断,这个操作也是要访问临界资源的,所以加锁在if判断之前,当访问完临界资源的时候,就要立即解锁)

通过上面的分析,可以看出,给互斥量加锁之后,执行临界区的代码从并行变为了串行。性能也会损耗不少,只是无法避免的。所以在加锁的时候要在合适的位置进行加锁和解锁,这样尽可能减少性能的损耗。

总结:
对临界资源的保护,是每一执行流都要遵守的,也就是说多线程要访问临界,每一个线程都必须要先申请锁,并且这些线程看到的是同一个互斥量。如果一个线程不申请锁就访问临界资源,这是错误的编码方式。

互斥量加锁的实现原理

在汇编的角度来看,只有一条汇编语句的话,认为该汇编语句是原子的。汇编指令swap或者exchang指令,就可以做到以一条指令的方式,将CPU寄存器和内存的数据交换。

当调用pthread_mutex_init函数时,他的汇编伪码是:
image.png
下面以两个线程A和B的场景为例,介绍一下加锁的时候,操作系统做了什么。

  1. 当线程A执行movb $0, %al时,将寄存器%al的值设置为0。

image.png

  1. 当线程A执行第二天汇编xchgb时,可能会被调度器调走,而寄存器中的内容是线程A私有的数据,属于线程的上下文信息。切走的时候,将0记录下来。
  2. 线程B被调度器调度,也要加锁,执行movb指令,将寄存器的值设置为0。假设此时没有被调度器切走,然后继续执行xchgb指令,将寄存器的内容和内存中mtuex的值进行交换。

image.png

  1. 此时al寄存器的内容为1。大于0,加锁成功。然后执行临界区的代码。(没执行到解锁也被切走了)

  2. 线程B被切走,al寄存器的内容属于线程B的上下文信息,被保存起来。然后执行线程A。

  3. 线程A别执行,恢复线程上下文信息,将寄存器al的值0恢复。

image.png

  1. 线程A执行xchgb指令,交换内存和寄存器的值。此时内存中mutex值还是0。换完后,进行判断。不大于0,继续挂起的等待。然后操作系统再让线程B来执行。
  2. 线程B临界区代码执行结束,进行解锁操作。解锁汇编伪码如下:

image.png
将寄存器al的值1,和内存中mutex做交换,这样下一个线程进程加锁的时候就可以成功加锁了。

以上就是互斥量加锁的原理。从汇编的角度看。用一条汇编指令,将内存和CPU内寄存器的值做交换,做到了原子性,保证了数据交换的时候线程不会被切走。以加锁的方式保证了临界资源被线程串行访问,保证了临界资源中数据一致性。

线程同步

同步更强调解决访问临界资源合理性的问题,加锁可以解决临界资源数据一致性的问题,但是同时也带来了新的问题,当一个线程带着锁访问临界资源的时候,造成了其他线程阻塞无法访问的情况,带来了线程饥饿问题。解决这一问题,引入了同步。

同步就是在保证数据安全的前提下,按照一定的规则,顺序的去访问临界资源。从而解决饥饿问题。

单纯的加锁,它能够保证在同一时间只有一个线程进入临界区,但它没有高效的让每一个线程使用这份临界资源。当添加一个规则:

  • 当一个线程释放锁后,这个线程不能立马再次申请锁,该线程必须排到这个锁的资源等待队列的最后。
  • 增加这个规则之后,下一个获取到锁的资源的线程就一定是在资源等待队列首部的线程,如果有十个线程,此时我们就能够让这十个线程按照某种次序进行临界资源的访问。

例如,现在有两个线程访问一临界资源,一个线程写入数据,另一个线程读取数据,但负责数据读取的线程的竞争力特别强,该线程每次都能竞争到锁,那么此时该线程就一直在执行读操作,而写线程还没有向临界资源写过任何数据。这就再做无用功。引入同步后该问题就能很好的解决。哪怕是读线程先读,没有读到,下次也轮到写线程写了。这样就不会造成一直读不到数据的问题。

条件变量

条件变量是解决线程同步的一种方案,需要配合互斥锁使用。
条件变量的使用和互斥锁差不多,也需要初始化和销毁。
pthread_cond_t是库中提供的条件变量类型。

  • 初始化
    • 全局或静态使用宏PTHREAD_COND_INITIALIZER初始化
    • 局部使用函数初始化pthread_cond_init(使用方法和互斥量一样)
  • 销毁
    • 使用宏初始化的不用销毁
    • 使用函数初始化用pthread_cond_destroy销毁

互斥量的重点在加锁和解锁,而条件变量的重点在等待和唤醒。

  • 一个线程等待条件变量的条件成立而被挂起。

  • 另一个线程使条件成立后唤醒等待的线程。

  • 等待

    • 函数原型
    int pthread_cond_wait(pthread_cond_t *restrict cond,
              pthread_mutex_t *restrict mutex);
    
    • 参数一:需要等待的条件变量。
    • 参数二:当前线程所处临界区对应的互斥锁
    • 返回值:函数调用成功返回0,失败返回错误码
  • 唤醒

    • 函数原型:
    int pthread_cond_broadcast(pthread_cond_t *cond);
    int pthread_cond_signal(pthread_cond_t *cond);
    
    • 唤醒有两个函数,第一个一次唤醒同一条件变量下的所有线程。
    • 第二个唤醒唤醒等待队列中首个线程。
    • 返回值:成功返回0,失败返回错误码。

使用示例:
创建四个线程,对count进行–,申请成功之后等待,主线程进行唤醒操作

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;

int count = 10000;

class ThreadData
{
public:
    ThreadData(string &name, pthread_mutex_t *mutex,pthread_cond_t *cond)
        : _name(name), _mutex(mutex),_cond(cond)
    {}

public:
    string _name;
    pthread_mutex_t *_mutex;
    pthread_cond_t *_cond;
};

void *TouchThread(void *arg)
{
    ThreadData *td = (ThreadData *)arg;
    while (1)
    {
        pthread_mutex_lock(td->_mutex); // 加锁
        pthread_cond_wait(td->_cond,td->_mutex);//等待唤醒
        if (count > 0)
        {
            usleep(1000);
            cout << td->_name << ":" << count << endl;
            --count;
            pthread_mutex_unlock(td->_mutex); // 解锁
            
        }
        else
        {
            pthread_mutex_unlock(td->_mutex); // 解锁
            break;
        }
    }

    delete td;
    return nullptr;
}

int main()
{
    const int NUM = 5;
    pthread_t tid[NUM];
    pthread_mutex_t mutex;            // 互斥量
    pthread_cond_t cond;//条件变量

    pthread_mutex_init(&mutex, NULL); // 初始化互斥量
    pthread_cond_init(&cond,NULL);//初始化条件变量

    // create thread
    for (size_t i = 0; i < NUM; ++i)
    {
        string name = "thread";
        name += to_string(i + 1);
        ThreadData *td = new ThreadData(name, &mutex,&cond);//线程的信息,需要名字和互斥量
        pthread_create(tid + i, nullptr, TouchThread, (void *)td);
    }
    sleep(2);

    //唤醒线程
    while(count > 0 )
    {
        cout << "唤醒一批线程ing:" << endl;
        pthread_cond_broadcast(&cond);//每次唤醒一批线程
        sleep(1);
    }
    // join thread
    for (size_t i = 0; i < NUM; ++i)
    {
        pthread_join(tid[i], nullptr);
    }

    pthread_mutex_destroy(&mutex); // 销毁互斥量
    pthread_cond_destroy(&cond); // 销毁条件变量
    return 0;
}

运行结果:
主线程休眠两秒后,开始唤醒线程一次唤醒5个线程。每隔一秒唤醒一次
recording.gif

条件变量为什么需要互斥量
  • 当一个线程访问临界资源的时候,此时临界资源如果还没有准备好,就会让线程去等待,等待的时候,如果不解锁,其他线程来了以后,无法申请到锁,就会被挂起,只有等到带锁的线程解锁之后才可以去竞争锁。所以等待的时候要解锁,让其他线程来了之后也可以申请锁,当线程被唤醒的时候,在哪里等待挂起继续在哪里执行,唤醒的时候会自动加锁。然后再去访问临界资源。
  • 所以在调用pthread_cond_wait函数时,还需要将对应的互斥锁传入,此时当线程因为某些条件不满足需要在该条件变量下进行等待时,就会自动释放该互斥锁。
  • 当该线程被唤醒时,该线程会接着执行临界区内的代码,此时便要求该线程必须立马获得对应的互斥锁,因此当某一个线程被唤醒时,实际会自动获得对应的互斥锁。
  • 条件变量等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有另一个线程通过某些操作,改变临界资源,使原先不满足的条件变得满足,并且通知等待在条件变量上的线程。
  • 条件不会无缘无故的突然变得满足了,必然会牵扯到临界资源的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改临界资源。

上面的例子不能很好的说明这个问题,多线程的应用场景有一个非常经典的生产者消费者模型,通过生产者消费者模型,能更好的说明上面的问题

生产者消费者模型

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过特定容器(阻塞队列)来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
image.png

生产者消费者模型的特点

生产者消费者模型是多线程同步与互斥的一个经典场景,其特点如下:

  • 三种关系
    • 生产者和生产者之间的关系:互斥关系。(这里的阻塞队列就是临界资源,生产者向队列生产数据时,必须是互斥关系,保证队列中数据一致性。)
    • 消费者和消费者之间的关系:互斥关系。(同理,消费者线程访问临界资源也必须是互斥关系)
    • 生产者和消费者之间的关系:互斥与同步关系。(不能让生产者或者消费者一方一直去访问临界资源,这样的话,如果生产者一直生产,会将容器塞满,满了之后,就不能继续生产了,通知消费者消费数据。当容器有容量时再继续生产。)
  • 两种角色
    • 生产者
    • 消费者
  • 一个容器
    • 阻塞队列(生产者和消费者之间数据的交易场所)

基于阻塞队列的生产者消费者模型

在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元 素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程 操作时会被阻塞)
image.png
先实现一个阻塞队列

#pragma once
#include <iostream>
#include <queue>
#include <unistd.h>
#include <pthread.h>
using namespace std;

const int DEFAULT = 5; // 队列默认容量大小

template <class T>
class BlockQueue
{
public:
    // BlockQueue(){}
    BlockQueue(int capacity = DEFAULT)
        : _capacity(capacity)
    {
        pthread_mutex_init(&_mtx, nullptr);
        pthread_cond_init(&_full, nullptr);
        pthread_cond_init(&_empty, nullptr);
    }
    void push(T &in)
    {
        // 1. 访问临界资源保证线程安全要加锁 这里的阻塞队列就是临界资源
        pthread_mutex_lock(&_mtx);
        // 2. 检查临界资源是否准备完成
        if (IsFull())
        {
            pthread_cond_wait(&_full, &_mtx);
        }
        _bqueue.push(in);
        pthread_mutex_unlock(&_mtx);
        pthread_cond_signal(&_empty);
    }
    void pop(T *out)
    {
        pthread_mutex_lock(&_mtx);
        if (IsEmpty())
        {
            pthread_cond_wait(&_empty, &_mtx);
        }
        *out = _bqueue.front();
        _bqueue.pop();
        pthread_mutex_unlock(&_mtx);
        pthread_cond_signal(&_full);
    }
    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mtx);
        pthread_cond_destroy(&_full);
        pthread_cond_destroy(&_empty);
    }

private:
    bool IsFull()
    {
        return _bqueue.size() == _capacity;
    }
    bool IsEmpty()
    {
        return _bqueue.size() == 0;
    }

private:
    queue<T> _bqueue;      // 阻塞队列
    size_t _capacity;      // 队列容量
    pthread_mutex_t _mtx;  // 互斥量
    pthread_cond_t _full;  // 判满的条件变量
    pthread_cond_t _empty; // 判空的条件变量
};

注意: :

  • 上面的阻塞队列代码,在检查临界资源是否准备完成时,使用的时if进行判断的,但是,pthread_cond_wait是一个函数,这就意味着这个函数有可能会被调用失败,一旦调用失败,就会继续执行后面的代码,会去访问临界资源。
  • 拿生产者线程来说,如果阻塞队列已经满了,但是等待函数调用失败,继续会去向阻塞队列中生产数据,这样会造成阻塞队列实际的容量和自己设定的容量不一样,后续在进行判空判满的话就会出错。
  • 其次,如果在多消费者的情况下,当生产者生产了一个数据后如果使用pthread_cond_broadcast函数唤醒消费者,就会一次性唤醒多个消费者,但待消费的数据只有一个,此时其他消费者就被伪唤醒了。
  • 为了避免这种特殊情况的出现, 应该让线程在被唤醒后,再次进行判断,确认是否真的满足生产消费条件,因此这里必须要用while进行判断。

修改后的阻塞队列:

// 基于阻塞队列的生产者消费者模型
#pragma once
#include <iostream>
#include <queue>
#include <unistd.h>
#include <pthread.h>
using namespace std;

const int DEFAULT = 5; // 队列默认容量大小

template <class T>
class BlockQueue
{
public:
    BlockQueue(int capacity = DEFAULT)
        : _capacity(capacity)
    {
        pthread_mutex_init(&_mtx, nullptr);  // 初始化互斥量
        pthread_cond_init(&_full, nullptr);  // 初始化条件变量
        pthread_cond_init(&_empty, nullptr); // 初始化条件变量
    }
    void push(T &in)
    {
        // 1. 访问临界资源保证线程安全要加锁 这里的阻塞队列就是临界资源
        pthread_mutex_lock(&_mtx);
        // 2. 检查临界资源是否准备完成(如果当前阻塞队列满了,生产者线程就不想继续向队列中push数据。就在满的条件变量下进行等待)
        while (IsFull())//必须使用while进行判断
        {
            pthread_cond_wait(&_full, &_mtx);
        }
        _bqueue.push(in);
        pthread_mutex_unlock(&_mtx);
        pthread_cond_signal(&_empty);
    }
    void pop(T *out)
    {
        // 1. 访问临界资源保证线程安全要加锁 这里的阻塞队列就是临界资源
        pthread_mutex_lock(&_mtx);
        // 2. 检查临界资源是否准备完成(如果当前阻塞队列为空,消费者线程就不能继续从队列中pop数据。在空的条件变量下进行等待)
        while (IsEmpty())//必须使用while进行判断
        {
            pthread_cond_wait(&_empty, &_mtx);
        }
        *out = _bqueue.front();
        _bqueue.pop();
        pthread_mutex_unlock(&_mtx);
        pthread_cond_signal(&_full);
    }
    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mtx);  // 销毁互斥量
        pthread_cond_destroy(&_full);  // 销毁条件变量
        pthread_cond_destroy(&_empty); // 销毁条件变量
    }

private:
    //  检查当前阻塞队列是否为满
    bool IsFull()
    {
        return _bqueue.size() == _capacity;
    }
    // 检查当前阻塞队列是否为空
    bool IsEmpty()
    {
        return _bqueue.size() == 0;
    }

private:
    queue<T> _bqueue;      // 阻塞队列
    size_t _capacity;      // 队列容量
    pthread_mutex_t _mtx;  // 互斥量
    pthread_cond_t _full;  // 判满的条件变量
    pthread_cond_t _empty; // 判空的条件变量
};

条件变量使用规范

  • 等待条件代码
pthread_mutex_lock(&mutex);
while (条件为假)
	pthread_cond_wait(&cond, &mutex);
修改条件
pthread_mutex_unlock(&mutex);
  • 等待一定是在加锁和解锁之间进行等待的。
  • 给条件发送信号代码
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);

这里可以使用RAII机制对锁进行设计。如下:

// 使用RAII机制对锁进行设计
#pragma once
#include <iostream>
#include <pthread.h>
using namespace std;

class MUTX
{
public:
    MUTX(pthread_mutex_t *pmtx)
        : _pmtx(pmtx)
    {
    }
    void lock()
    {
        pthread_mutex_lock(_pmtx);
    }
    void unlock()
    {
        pthread_mutex_unlock(_pmtx);
    }
    ~MUTX()
    {
    }

private:
    pthread_mutex_t *_pmtx;
};
// RAII风格的加锁方式
class LockGroud
{
public:
    LockGroud(pthread_mutex_t *pmtx) : _pmtx(pmtx)
    {
        _pmtx.lock();
    }
    ~LockGroud()
    {
        _pmtx.unlock();
    }

private:
    MUTX _pmtx;
};

这样在阻塞队列中使用锁如下(以push为例):

 void push(T &in)
{
    // RAII机制的加锁方式
    LockGroud lockgroud(&_mtx);
    while (IsFull()) // 必须使用while进行判断
    {
        pthread_cond_wait(&_full, &_mtx);
    }
    _bqueue.push(in);
    pthread_cond_signal(&_empty);
    // 函数调用结束,lockgrou局部对象生命周期结束自动调用析构函数进行解锁
}

有关线程的其他概念

可重入&&线程安全

  • 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作, 如果没有锁保护的情况下,会出现线程安全问题。
  • 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,称之为重入。(上面demo代码中TouchThread函数就是可重入函数)。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

线程安全讨论的是线程执行代码时是否安全,重入讨论的是函数被重入进入。

常见的线程不安全情况

  • 不保护共享变量的函数
  • 函数状态随着被调用,状态发生变化的函数
  • 返回指向静态变量指针的函数。
  • 调用线程不安全函数的函数

常见的线程安全的情况

  • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的。
  • 类或者接口对于线程来说都是原子操作。
  • 多个线程之间的切换不会导致该接口的执行结果存在二义性。

常见的不可重入的情况

  • 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的。
  • 调用了标准I/O库函数,标准I/O可以的很多实现都是以不可重入的方式使用全局数据结构。
  • 可重入函数体内使用了静态的数据结构。

常见可重入情况

  • 不使用全局变量或静态变量。
  • 不使用malloc或者new开辟出的空间。
  • 不调用不可重入函数。
  • 不返回静态或全局数据,所有数据都由函数的调用者提供。
  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。

可重入与线程安全的联系

  • 函数是可重入的,那就是线程安全的。
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题。
  • 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

可重入与线程安全的区别

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

死锁

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

发生死锁的必要条件

  • 互斥条件:一个资源每次只能被一个执行流使用。
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺。
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。

避免死锁

  • 破坏死锁的四个必要条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配
CMOS传输门实战:如何用模拟开关优化你的电路设计(附常见问题排查) 本文深入探讨了CMOS传输门在电路设计中的实战应用与优化策略。文章从核心参数选型入手,详细分析了导通电阻、电荷注入等关键特性对电路性能的影响,并提供了多路复用器构建、精密电路应用等实战架构。同时,重点分享了PCB布局、电源去耦的工程细节,以及开关不导通、信号失真、高频性能不足等常见问题的系统性排查指南,旨在帮助硬件工程师充分发挥CMOS模拟开关的效能,提升电路设计的可靠性与精度。 阅读详情

相关推荐

简单的图书管理系统---SpringBoot+mysql通过前后端分离实现增删查改

SpringBoot+mysql通过前后端分离实现增删查改 上篇文章介绍了如何写后端接口 SpringBoot连接MySQL获取数据写后端接口 这篇文章,我们就介绍一下怎么写前端 点击修改跳出修改界面,点击添加书籍跳出添加书籍界面 点击删除,直接删除一行数据 其他的也没什么解释了,直接上代码(看不懂的可以私聊我哟) <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>

爱笑的博客 2015

线程的同步与互斥

线程的同步与互斥: 互斥:当一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源。如:当线程A在使用打印机时,其他线程都需要等待。 同步:两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后次序运行。如:A任务的运行依赖B任务产生的数据。 同步保证了线程运行的顺序性,互斥保证了线程运行的安全性。 同步是一种更为复杂的互斥,而互斥是一种特殊的同步。 互斥具有唯一性和

Nessie_zhao的博客 4012

合成生物学中试:花钱只是开始,烧钱才是常态

2026年合成生物学行业面临严峻产业化挑战:多家企业终止发酵项目或延期投产,显示从实验室到工厂的转化困境。核心问题并非基因合成成本,而是工艺放大难题——67%项目在中试阶段因工艺问题失败。AI辅助无法替代工程经验,菌株-配方-工艺的三元适配需要长期数据积累。中试成本惊人,涉及设备折旧、原料消耗和稀缺工艺人才,而行业普遍回避讨论这些工程现实,导致认知偏差。建议创业者提前布局中试,将其视为核心竞争力而非简单成本项。合成生物学的真正瓶颈在于工程化能力,这需要时间沉淀而非技术叙事。

AIexander的博客 943

线程的同步与互斥:互斥锁

什么是线程的同步与互斥? 同步:假设现有线程A和线程B,线程A需要往缓冲区写数据,线程B需要从缓冲区读数据,但他们之间存在一种制约关系,即当线程A写的时候,B不能来拿数据;B在拿数据的时候A不能往缓冲区写,也就是说,只有当A写完数据(或B取走数据),B才能来读数据(或A才能往里写数据)。这种关系就是一种线程的同步关系。 互斥:对于线程A和线程B来讲,在同一时刻,只允许一个线程对临界资源进行操作,即当

YANG 2万+

【Latex】【附安装文件】Miktex和TexStudio的安装与配置

这代表“构建并运行”,一般来说,我们只需要点这个按钮来运行Latex代码。保存一下代码文件,再运行一次,PDF文件会出现在与代码文件相同的文件夹下。安装完成后,你可能会跳转到一个页面中,你看到这个页面就意味着安装成功了。其实不需要你敲,你只要将下面的代码复制粘贴到你刚刚的文件中即可。成功安装MikTex后,我们需要进行更新,减少后续使用的麻烦。如果懂得将安装路径设置在你想要的盘中,你可以自己设置。双击上文的.exe文件后,你会看到如下画面。如果不懂,为避免不必要的麻烦,请直接点击“

未来就在脚下。 1万+

Unity2019编译器下使用Ump无错误,打包exe不播放 或 将exe文件在其他电脑下运行不播放

一、unity编译器下使用UMP可以播放视频,打包后查看debug 出现错误 UnloadTime: 1.070900 ms Fallback handler could not load library F:/PCTest/AccessControlSysteam/jfmx_Data/Mono/libdl Fallback handler could not load library F:/PCTest/AccessControlSysteam/jfmx_Data/Mono/libdl.dll Fa.

qq_42345116的博客 4700

线程互斥、同步

多线程执行流共享的资源叫做临界资源每个线程内部访问临界资源的代码,被称为临界区任何时刻,互斥保证有且只有一个执行流进入临界区访问临界资源,通常对临界资源起保护作用不会被任何调度机制打断的操作,该操作只有两态:要么完成,要么未完成下面模拟实现一个抢票系统,将记录票的剩余张数的变量定义为全局变量,主线程创建四个新线程进行抢票,当票被抢完后这四个线程自动退出//抢票所耗费的时间

GG_Bruse的博客 7931

把外部系统一键装进 SAP Build Work Zone:从零开始创建 HTML5 URL App

HTML5 URL App 是一种轻量级解决方案,可将外部网页或系统入口快速集成到SAP企业门户中。开发者只需在manifest.json中配置目标URL、导航模式等参数,无需开发完整前端应用。该方案利用SAP BTP的HTML5 Application Repository集中存储静态资源,通过HTML5 Apps Content Provider将应用暴露给子账号,最终在SAP Build Work Zone中以Fiori磁贴形式呈现。文章详细介绍了manifest.json的关键配置、三种基础设施组件

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 243

线程及线程的同步互斥

目录线程的简单介绍信号量互斥量条件变量1.线程的简单介绍 线程的简单介绍 信号量 互斥量 条件变量 1.线程的简单介绍

weixin_45738899的博客 2977

C# WPS操作PPT,全屏,缩率图,备注,跳转播放

本文介绍了一个使用C#开发的PPT操作工具,主要功能包括检测当前打开的PPT文件、获取选中PPT路径、指定播放PPT、获取PPT页面序号和缩略图、提取备注内容等。文章展示了操作界面效果图,并重点解决了用户手动退出PPT后重新获取路径时报错的问题。核心代码实现了通过WPS API进行PPT文件的打开、全屏播放、页面跳转、获取当前页位置等功能,同时支持生成PPT缩略图预览和备注信息提取。当用户未通过工具按钮打开PPT时,程序会自动初始化PPT信息确保功能正常使用。

遇见--专栏 312

【AI大模型:架构实战】31、工业级AI大模型系统全景解析:从架构设计到提示语引擎实践

本文系统解析了工业级AI大模型的核心架构与技术体系。整体采用分层解耦设计,包含数据工程、模型训练、模型部署、应用集成和监控运维五大核心模块,以及分布式计算、高速网络、异构硬件三大支撑体系。数据工程构建高质量数据处理流水线;模型训练实现从预训练到业务适配的全流程;模型部署优化服务性能与可用性;应用集成连接业务场景;监控运维保障系统持续优化。三大支撑能力专注于千亿参数管理、高并发低延迟处理和持续学习进化。该架构设计既满足当前需求,又为技术演进预留空间,为企业在数字化转型中提供AI工业化落地的完整解决方案。

2025博客之星Top81。专注AI工程化与架构实战。从分布式思维到模型部署,用工程化视角为你厘清AI落地的真实路径。 1990

[Linux]线程的互斥与同步

在多线程编程中,互斥与同步机制至关重要。互斥,是指同一时刻只允许一个线程访问共享资源,防止数据竞争。实现互斥常依赖互斥锁接口,通过加锁、解锁操作控制对资源的访问,其本质是线程获取和释放对共享资源的独占权。然而,若使用不当,可能产生死锁,即多个线程相互等待对方释放资源而陷入僵局。同步则着重于线程间的协作,按特定顺序访问资源。条件变量是同步的重要工具,通过其接口,线程可在条件满足时被唤醒,从而实现高效的线程协作,保障多线程程序的正确运行。

m0_74207969的博客 1578

【YOLO11改进 - C3k2融合】C3k2融合DWRSeg二次创新C3k2_DWR:扩张式残差分割网络,提高特征提取效率和多尺度信息获取能力,助力小目标检测

【YOLO11改进 - C3k2融合】C3k2DWRSeg二次创新C3k2_DWR:扩张式残差分割网络,提高特征提取效率和多尺度信息获取能力,助力小目标检测

专注于图像领域,主要研究内容包括计算机视觉和深度学习,特别是在图像分类、目标检测和图像生成等方面有深入的研究和实践经验。 1056

【Linux学习】线程互斥与同步

线程互斥,临界资源,互斥锁,死锁问题,避免死锁,多线程编程,临界线程互斥是一种同步机制,用于控制对共享资源的访问,以确保在任意给定的时刻只有一个线程可以访问该资源。在多线程编程中,当多个线程同时竞争访问某个共享资源时,如果没有适当的同步机制,可能会导致竞争条件和数据不一致性的问题。线程互斥通过引入互斥锁等机制,使得在任意时刻只能有一个线程持有资源的访问权限,从而避免了竞争条件和数据不一致性的发生。进程线程间的互斥相关背景概念多线程执行流共享的资源叫做临界资源。每个线程内部,访问临界资源的代码,就叫做临界区。

Chris在Coding的博客 1457

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

Linux线程同步与互斥:【线程互斥】【线程同步】【线程池】

生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区, 平衡了生产者和消费者的处理能力。因为我在当前的进程没有进行释放锁的操作,我是持有锁被切换的,相当于拿着钥匙,钥匙不给其他线程,其他线程也只能干等着,等我回来释放锁,其他线程才能展开锁的竞争,进入临界区。• 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完 成线程之间的交互。

2301_81699364的博客 1289

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

Linux线程-互斥与同步

本章主要讲解学习Linux中对多线程的执行中的同步与互斥

~ 1624

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

Linux多线程——线程互斥与同步

摘要:多线程并发访问共享资源时存在数据竞争问题,需通过互斥锁和同步机制解决。 临界资源与竞争问题:多线程同时操作全局变量(如g_val--)时,由于CPU指令的非原子性,可能导致数据不一致。例如,线程切换时未完成的操作会使变量值异常。 互斥锁保护:通过pthread_mutex_t实现加锁(pthread_mutex_lock)和解锁(pthread_mutex_unlock),确保临界区代码的原子性。锁的粒度应尽量细以减少性能损失。 线程同步:条件变量(pthread_cond_t)解决线程间执行顺序问题

2401_83603768的博客 1455

【Linux】线程的同步与互斥

本文深入探讨了线程互斥与同步的概念及其在多线程编程中的应用。首先,文章回顾了互斥的基本概念,包括临界资源、临界区、互斥和原子性,并通过抢票系统的代码示例展示了多线程环境下数据不一致的问题。接着,文章引入了互斥锁(Mutex)的概念,详细介绍了其接口、使用方法以及如何通过加锁机制解决数据竞争问题。此外,文章还探讨了条件变量(Cond)的使用,解释了如何通过条件变量实现线程的同步,避免线程饥饿现象。最后,文章介绍了生产者/消费者模型,并通过阻塞队列(BlockingQueue)的实现展示了如何在实际应用中结合互

2301_81577798的博客 1万+

Linux线程同步与互斥

上图中,线程1因为某些原因需要线程切换,进入等待队列。那么此时线程1需要把自己的上下文数据带走,其实就是把寄存器当中保存的值带走,并且没有对内存交换的0进行写回,也就是说此时内存中的mutex是0,那么线程2在交换mutex到寄存器当中,就会进行状态检测,此时检测到状态为0,说明当前已经有人占用锁了,则线程2进入到挂起状态,后来的线程依旧会如此,直到第一个线程执行完毕将锁释放。大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。

欢迎来到阿熊的blog~~ 1161

线程同步与线程互斥

1、获取线程ID功能:获取线程号返回值:线程ID2、分离线程函数功能:使线程分离,线程退出时让系统自动回收线程资源参数:线程ID子线程在退出的时候,主线程没有给子线程回收资源的话,线程会变成僵尸线程。使用方式:1)在主线程中调用pthread_detach(tid);2)在子线程中调用pthread_detach(pthread_self());注意:1)如果线程已经设置了分离状态则再调用pthread_join时就会失败。

WBGZSL的博客 1460

Linux_线程的同步与互斥

在Linux下,线程是一个很重要的概念,他可以提高多执行流的并发度,而同步与互斥是对线程的一种约束行为,比如当多个线程都访问同一个资源时,若不对该资源加以保护则会导致意料之外的错误。具体的保护措施是让线程访问共享资源时具有互斥性,即当一个线程访问时别的线程无法访问,通常用互斥锁来实现。而同步是为了让多个线程具有一定的顺序来访问共享内存,保障每个线程访问资源的机会是一样的。

安权_code的博客 2053

【Linux】线程同步与互斥

线程同步与互斥

f_2789889770的博客 1091

线程互斥与线程同步

线程互斥与线程同步

2301_80001343的博客 1055
上一篇: C++ STL -->模拟实现string
下一篇: 算法->位运算
C++下等马
博客等级 码龄5年 776粉丝 79原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

C++下等马

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值