Linux_线程

  • 线程与进程
  • 多级页表
  • 线程控制
  • 线程互斥
  • 线程同步
  • 生产者消费者模型
  • 常见概念

下面选取32位系统举例。

一.线程与进程

image.png
上图是曾经我们认为进程所占用的资源的集合。

1.1 线程概念
  1. 线程是一个执行分支,执行粒度比进程细,调度成本比进程低
  2. 线程是cpu调度的基本单位,进程是分配资源的基本单位
  3. 线程是进程内部的一个执行流

通常我们创建线程是为了执行程序的一部分代码,所以执行粒度一定比进程更细,我们知道进程=内核数据结构+代码和数据。引入线程之后,这个概念就应该修正为进程=许多内核数据结构+代码和数据。这些数据结构指向同一个程序地址空间,如图:
image.png
在操作系统的概念中,这样的数据结构叫做线程控制块(TCB),但由于创建新的数据结构还要设计新的调度逻辑,所以Linux在内核中并没有设计这样的数据结构,而是复用进程的数据结构PCB。故此在Linux中并没有真正的线程,而是用进程模拟的线程,线程在Linux中叫轻量级进程(LWP)。

1.2 理解线程
  1. 创建线程只需要执行一部分代码,所以执行粒度比进程细
  2. 当发生调度时,操作系统会识别是进程间的调度还是线程间的调度,如果是线程间的调度,那么只需要改变部分寄存器的值即可,而cache不需要重新加载代码。所以线程的调度成本是比进程小的。
  3. cpu每次调度都是以线程为单位的,操作系统分配资源是以进程为单位的。
  4. 线程有自己的栈
  5. 线程共享进程的大部分数据,但也有私有数据
    1. **线程ID **
    2. **一组寄存器 **
    3. **栈 **
    4. **errno **
    5. **信号屏蔽字 **
    6. 调度优先级
1.3 线程优点
  • 创建一个新线程的代价要比创建一个新进程小得多
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  • 线程占用的资源要比进程少很多
  • 能充分利用多处理器的可并行数量
  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  • I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
1.4 线程缺点
  • 健壮性减低:主线程退出,全部线程退出,一个线程收到信号,所有线程收到信号
  • 缺乏访问控制:相比与进程,线程之间的通信成本是极低的,但是线程之间的通信缺乏访问控制,即同步机制。当一个线程正在写入,还没有写完,另一个线程就可能会读走数据。

二.多级页表

文件系统指出磁盘和内存的数据交换是以块(4KB)为单位,因此内存的管理也要以4KB为单位,在内存中这样的结构叫做页。

如果页表只有一张,并且页表保存的是字节间的映射关系,那么一个页表需要保存232行,这样肯定是不行的,一是查找速度慢,二是内存可能不够。因此,Linux中采用多级页表,并且将32虚拟地址拆分成3部分使用:前10位用来在页目录中使用,中间10位在页表项中使用,最后12位做为页内偏移地址使用。(212 = 4KB)
image.png

页目录共有1024行,每个页表项也有1024行,共可以映射2^20页地址,这恰好是内存中页的数量,页表映射出物理页号,物理页号和虚拟地址中的12位页内偏移组合,构成物理字节地址。

由于程序不可能使用整个内存,所以页表不会一次全部创建,而是创建一部分。

三.线程控制

3.1 线程库的理解

在Linux中,没有真正意思上线程,所以系统不会提供线程相关的接口,但是有控制轻量级进程的相关接口。可是用户只认线程,所以有了用户级线程库pthread,这个库底层封装了轻量级进程的相关接口,给上层提供了线程相关的接口。pthread库在任何一个Linux系统上都自带了,因此pthread库也叫做原生线程库。由于pthread库属于第三方库,所以在使用gcc/g++编译时,需要指定库名称。gcc xxxx -lpthread

3.2 创建线程
  • pthread_creat
  • pthread_t:整数,输出型参数,返回给用户线程id,以后操作线程用线程id。
  • attr:可以定义线程的属性,一般设置为空
  • void*(start_routine)(void) :函数指针,设置线程指向的函数
  • void* arg:线程执行函数的参数
  • 返回值:成功返回0,失败返回-1

代码示例:

#include<iostream>
#include<pthread.h>
#include <unistd.h>


using namespace std;

void* threadRoutine(void* s)
{
    const char* str = (const char*)s;
    cout << str << endl;

}
int main()
{
    pthread_t p1;

    pthread_create(&p1, nullptr, threadRoutine, (void*)"thread 1");
    
    sleep(3);
    return 0;
}

通过命令ps -aL可以查看当前用户创建的线程,线程不分父子,分主次:主线程的PID=LWP。操作系统判断是进程间切换还是线程间切换通常使用PID来区分的,看两个结构体的PID是否相同,相同即是线程间切换。

  • 如果主线程退出,当前进程的所有线程立即退出。
  • 如果任一线程收到信号,那么所有线程都会收到信号。

image.png

3.3 线程等待

如果主线程不等待新线程,那么新线程会产生类似僵尸进程的情况,导致内存泄漏。

  • pthread_join

image.png

  • 等待指定id的线程
  • void** retval:输出型参数,已知线程执行的任务返回值为void*,因此要获得这个值就需要void*的地址,即void**

代码示例:

#include<iostream>
#include<pthread.h>
#include <unistd.h>
#include <cstring>


using namespace std;

void* threadRoutine(void* s)
{
    const char* str = (const char*)s;
    cout << str << endl;

    return (void*)1;  

}
int main()
{
    pthread_t p1;
    pthread_create(&p1, nullptr, threadRoutine, (void*)"thread 1");

    void *retval = nullptr;
    int n = pthread_join(p1, &retval);
    if (n != 0)
    {
        cerr << "errno: " << errno << strerror(errno) <<endl;
    }
    // 当前环境是64位的,int是32位的,如果强转为32位会报错。
    cout << "wait successful!" << " retval: " << (int64_t)retval << endl;
    
    return 0;
}

3.4 线程分离

如果新线程不用被等待获取返回值,可以在主线程中分离该线程

  • pthread_detach

image.png

  • 分离指定id的线程
#include<iostream>
#include<pthread.h>
#include <unistd.h>
#include <cstring>


using namespace std;

void* threadRoutine(void* s)
{
    const char* str = (const char*)s;
    cout << str << endl;  
}
int main()
{
    pthread_t p1;

    pthread_create(&p1, nullptr, threadRoutine, (void*)"thread 1");

    pthread_detach(p1);

    sleep(3);
    
    return 0;
}

3.5 线程退出

线程退出有三种方式:

  1. return,线程正常执行完退出
  2. pthread_exit, 谁调用该函数谁退出,retval为返回值。image.png
  3. pthread_cancel:让指定id的线程退出,退出码为PTHREAD_CANCELED(-1)image.png
3.6 获取线程id
  • pthread_self:可以获取当前线程的id

image.png

#include<iostream>
#include<pthread.h>
#include <unistd.h>
#include <cstring>


using namespace std;

void* threadRoutine(void* s)
{
    const char* str = (const char*)s;
    cout << str << endl;
    
}
int main()
{
    pthread_t p1;

    pthread_create(&p1, nullptr, threadRoutine, (void*)"thread 1");

    pthread_detach(p1);

    cout << pthread_self() << endl;

    sleep(3);
    
    return 0;
}

3.7 线程id与LWP的区别

pthread库是共享库,也会被映射到进程地址空间中的共享区。在pthread库中创建的所有线程会被组织成数组。而线程id就是数组中线程的地址。LWP是轻量级进程使用的,被封装进struct pthread字段中。这种结构类似文件,struct FILE中封装了文件描述符fd。
image.png

  • 每一个线程都拥有自己的栈,主线程的栈是进程地址空间的栈,而其他线程的栈都在共享区中
  • 所有线程共享大部分数据,如果创建了一个全局变量,但是想让每个线程都拥有这个变量,那么就需要在前面加__thread,然后每个线程都会在自己的局部存储里面创建这个全局变量。其生命周期是全局的。
#include<iostream>
#include<pthread.h>
#include <unistd.h>
#include <cstring>

using namespace std;
__thread int _gval = 0;

void* threadRoutine(void* s)
{
    const char* str = (const char*)s;
    
    cout << str << "_gval: " << _gval << " &_gval: " << &_gval << endl;  
}

void* threadRoutine1(void* s)
{
    sleep(3);
    const char* str = (const char*)s;
    
    cout << str << "_gval: " << _gval << " &_gval: " << &_gval << endl;  
}
int main()
{
    pthread_t p1, p2;

    pthread_create(&p1, nullptr, threadRoutine, (void*)"thread 1");
    pthread_create(&p2, nullptr, threadRoutine1, (void*)"thread 2");

    sleep(4);
    
    return 0;
}
  • 上面两个线程打印出来的地址一定不同。

四.线程互斥

  • 临界资源:多个执行流共享的资源就是临界资源
  • 临界区:访问临界资源的代码就是临界区
  • 互斥:任一时刻,只有一个执行流进入临界区。
  • 原子性:一个操作只有两种状态,要么还没开始,要么就已完成。

在多线程的场景下,不同线程访问临界资源会引发线程安全的问题,例如:对临界资源做自增操作。

#include<iostream>
#include<pthread.h>
#include <unistd.h>
#include <cstring>

int _gval = 0;

void* threadRoutine(void* s)
{	
    int cnt = 0;
    while (cnt < 100)
    {
        cnt++;
    }
}

int main()
{
    pthread_t p1, p2;
    pthread_create(&p1, nullptr, threadRoutine, nullptr);
    pthread_create(&p2, nullptr, threadRoutine1, nullptr);

    sleep(4);   
    return 0;
}
  • cnt++,这一条代码会被解释为三条汇编指令:image.png

image.png
如果一个线程执行到第二步的时候时间片到了,切换为另一个线程,然后再切换回第一个线程,那么第二个线程对cnt的操作就会被覆盖。如果要保证对cnt的操作是线程安全的,即没有并发访问问题,就需要对该临界区加锁,让临界区任何时刻只能有一个线程进入。

4.1 互斥锁接口

锁的接口定义在pthread.h头文件中。

  1. 定义

pthread_mutex_t mutex;

  • 定义mutex变量
  1. 初始化

image.png

  • pthread_mutex_t* mutex :传入外部定义的锁。
  • const pthread_mutexattr_t* attr:锁的属性,一般设置为nullptr
  1. 销毁

image.png

  • pthread_mutex_t* mutex :传入外部定义的锁

如果定义的锁是全局变量,可以使用PTHREAD_MUTEX_INITIALIZER初始化,不需要调用pthread_mutex_init() 和 pthread_mutex_destroy()

  1. 加锁

image.png

  1. 解锁

image.png

  1. 代码示例
#include<iostream>
#include<pthread.h>
#include <unistd.h>
#include <cstring>

int _gval = 0;

// 写法一;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void* threadRoutine(void* s)
{	
    int cnt = 0;
    pthread_mutex_lock(&mutex);
    while (cnt < 100)
    {
        cnt++;
        pthread_mutex_unlock(&mutex);
    }
    
}

int main()
{
    pthread_t p1, p2;
    pthread_create(&p1, nullptr, threadRoutine, nullptr);
    pthread_create(&p2, nullptr, threadRoutine1, nullptr);

    sleep(4);   
    return 0;
}


// 写法二:


void* threadRoutine(void* s)
{	
    pthread_mutex_t* mutex = (pthread_mutex_t*)s;
    int cnt = 0;
    pthread_mutex_lock(mutex);
    while (cnt < 100)
    {
        cnt++;
        pthread_mutex_unlock(mutex);
    }
    
}

int main()
{
    pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, nullptr);
    pthread_t p1, p2;
    pthread_create(&p1, nullptr, threadRoutine, (void*)&mutex);
    pthread_create(&p2, nullptr, threadRoutine1, (void*)&mutex);
    pthread_mutex_destroy(&mutex);
    sleep(4);   
    return 0;
}

4.2 锁的原理

锁也是共享资源,访问共享资源就会有线程安全问题。为了防止这种套娃情况的发生,加锁和解锁操作具有原子性,大多数体系结构都提供了swap或exchange指令,这两条指令都保证了加锁的原子性。

加锁: 伪代码
lock:
movb $0, %al
xchgb %al, mutex ;这条指令是核心,将锁转移到当前线程的上下文中,这也就相当于线程取得了锁资源
if al > 0 then return 0
else 挂起等待
goto lock
解锁:
movb $1 ,mutex
唤醒等待mutex的线程
return 0;

在上面代码中,1不会增多,只会在不同线程之间流转,从而保证了多个线程只有一个锁资源

4.3 死锁
  死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。<br />**四个条件之一:**
  1. 互斥
  2. 请求与保持:线程请求资源的过程中,不放弃持有资源
  3. 不剥夺:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
  4. 环路等待:若干执行流之间形成一种头尾相接的循环等待资源的关系

预防死锁:破坏四个条件之一

  1. 不加锁可以破坏互斥条件
  2. 主动释放锁可以破坏请求与保持条件
  3. 控制线程释放锁资源可以破坏不剥夺条件
  4. 按照顺序申请锁可以破坏环路等待条件

五.线程同步

如果一个线程释放锁之后又立即申请锁,那么可能会导致其他线程一直申请不到锁,这样就会形成饥饿问题。这样明显是不合理的,我们可以使用条件变量来防止发生饥饿问题。一般条件变量是配合互斥锁使用的。条件变量的接口定义在pthread.h头文件中。

5.1 条件变量
  1. 定义

pthread_cond_t

  • 定义一个条件变量
  1. 初始化

image.png

  • pthread_cond_t* cond :传入外部定义的条件变量。
  • const pthread_condxattr_t* attr:条件变量的属性,一般设置为nullptr
  1. 销毁

image.png

  1. 阻塞

image.png

  • pthread_cond_t* cond:阻塞cond条件变量
  • pthread_mutex_t *mutex:当线程被阻塞时会释放锁,等到线程被唤醒时会重新申请锁
  1. 唤醒

image.png

  • 唤醒在条件变量下等待的线程有两种方式:1.唤醒一个线程 2.唤醒所有线程
  • pthread_cond_singal() :唤醒在条件变量cond下等待的一个线程
  • pthread_cond_broadcast() :唤醒在条件变量cond下等待的所有线程
5.2 posix信号量
  POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 POSIX可以用于线程间同步。信号量是资源的一种预定机制,将共享资源看作多份。信号量相关的接口在semaphore.h头文件中。
  1. 定义信号量

sem_t

  • 信号量类型,可以定义信号量
  1. 初始化

image.png

  • pshared:0表示线程间共享,非零表示进程间共享
  • value:信号量初始值,资源的数量
  • 当value=1时,为二元信号量,效果等同于互斥锁。
  1. 销毁

image.png

  1. 申请信号量资源

image.png

  • 对信号量初始值(value)-1
  1. 归还信号量资源

image.png

  • 对信号量初始值(value)+1

六.生产者消费者模型

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
image.png
如果我们想要维护好生产者消费者模型,就要研究3种关系,两种角色,一个缓冲区

  • 三种关系
    • 生产者与生产者:具有互斥关系
    • 消费者与消费者:具有互斥关系
    • 生产者与消费者:具有互斥和同步关系。
      • 同步:当缓冲区满时,让消费者消费,当缓冲区空时,让生产者生产
  • 两种角色
    • 生产者
    • 消费者
  • 一个缓冲区
    • 基于阻塞队列
    • 基于环形队列

生产者消费者模型的优点:

  1. 效率高
    1. 效率高体现在可以并发生产任务和并发处理任务
  2. 支持忙闲不均
    1. 生产者生产任务之后不必等消费者消费,而是将任务放到缓冲区,可以继续进行生产任务。
6.1 基于阻塞队列(BlockQueue)式的cp问题

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

  • 主要用到互斥锁和条件变量

生产者可以生产一系列任务让消费者执行任务。下面是模拟的一个计算器任务。


#pragma once

#include <iostream>
#include <string>
#include <unordered_map>
#include <functional>

class Task
{
public:
    Task() = default;
    Task(int x, int y, char op) : _x(x), _y(y), _op(1, op)
    { }

    void run()
    {
        std::unordered_map<std::string, std::function<void(void)>> cal = {
            {"+", [this]
             { this->_result = this->_x + this->_y; }},
            {"-", [this]
             { this->_result = this->_x - this->_y; }},
            {"*", [this]
             { this->_result = this->_x * this->_y; }},
            {"/", [this]
             { if (this->_y == 0) this->_exitStatus = 2; this->_result = this->_x/this->_y; }},
            {"%", [this]
             { if (this->_y == 0) this->_exitStatus = 1; this->_x%this->_y; }}};

        cal[_op]();
    }

    void formatExpress()
    {
        std::cout << "productor : " << _x << " " << _op << " " << _y << " = ?" << std::endl;
    }

    void formatRes()
    {
        std::cout << "consumer : " << _x << " " << _op << " " << _y << " = " << _result << std::endl;
    }

    int getExitCode()
    {
        return _exitStatus;
    }

    int getRes()
    {
        return _result;
    }

private:
    int _x;
    int _y;
    std::string _op;

    int _result;
    int _exitStatus;
};
#pragma once
#include <iostream>
#include <pthread.h>
#include <memory>
#include <vector>
#include <queue>
#include <unistd.h>

using std::cout;
using std::endl;

const int defaultCapacity = 5;

template <class T>
class BlockQueue
{

public:
    BlockQueue(int cap = defaultCapacity)
        : _cap(cap)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_consumerCond, nullptr);
        pthread_cond_init(&_productorCond, nullptr);
    }

    bool isFull()
    {
        return _bq.size() == _cap;
    }

    void push(const T &in)
    {
        // 生产者生产数据
        pthread_mutex_lock(&_mutex);

        // 如果缓冲区满,则生产者阻塞
        while (isFull()) 
            pthread_cond_wait(&_productorCond, &_mutex);  

        _bq.push(in);

        /// 可以采用一定策略唤醒消费者消费资源
        pthread_cond_signal(&_consumerCond);

        pthread_mutex_unlock(&_mutex);
    }

    bool isempty()
    {
        return _bq.empty();
    }

    void pop(T* out)
    {
        pthread_mutex_lock(&_mutex);

        while (isempty())
            pthread_cond_wait(&_consumerCond, &_mutex);


        *out = _bq.front();
        _bq.pop();

        pthread_mutex_unlock(&_mutex);
        pthread_cond_signal(&_productorCond);
    }

    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_consumerCond);
        pthread_cond_destroy(&_productorCond);
    }

private:
    // 缓冲区
    std::queue<T> _bq;
    // 缓冲区容量
    int _cap;

    // 互斥锁
    pthread_mutex_t _mutex;
    // 条件变量:当缓冲区满的时候,生产者阻塞,当缓冲区空的时候,消费者阻塞
    pthread_cond_t _consumerCond;
    pthread_cond_t _productorCond;
};
#include "bQueue.hpp"
#include "Task.hpp"
#include <cstring>

void *consume(void *args)
{
    BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(args);

    while (true)
    {
        // 从缓存区拿资源
        Task t;
        bq->pop(&t);

        // 使用资源
        t.run();
        t.formatRes();

        sleep(1);
    }

    return nullptr;
}

void *product(void *args)
{
    BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(args);
    const char* s = "+-*/%";
    while (true)
    {
        sleep(1);

        // 生产资源
        int x = rand()%10;   
        int y = rand()%10;   
        char op = s[rand()%strlen(s)];
        Task t(x, y, op);
        t.formatExpress();  

        // 将资源放到缓冲区中
        bq->push(t);
        
    }

    return nullptr;
}

int main()
{
    srand(time(nullptr));
    int n = 5;
    BlockQueue<Task> *bq = new BlockQueue<Task>(n);

    std::vector<pthread_t> consumers(3);
    std::vector<pthread_t> productors(4);

    for (auto &pth : consumers)
    {
        pthread_create(&pth, nullptr, consume, static_cast<void *>(bq));
    }

    for (auto &pth : productors)
    {
        pthread_create(&pth, nullptr, product, static_cast<void *>(bq));
    }

    for (auto tid : consumers)
        pthread_join(tid, nullptr);
    for (auto tid : productors)
        pthread_join(tid, nullptr);

    return 0;
}

6.2 基于环形队列的cp问题

下面实现一种基于环形队列的生产者消费者模型,采用数组模型环形队列。
image.png
在阻塞队列中,我们将缓冲区看作一个整体使用,因此也只定义了一个互斥锁就可以保证线程安全。但在环形队列中,我们将缓冲区看成n个小空间使用,因此使用信号量,只要消费者和生产者不指向同一个小空间,它们就可以并发执行。
消费者关心的是数据个数,生产者关心的是剩余空间个数,因此我们可以定义两个信号量,当剩余空间为0时,阻塞生产者;当数据个数为0时,阻塞消费者。
生产者与生产者之间互斥关系要靠一个互斥锁,消费者与消费者互斥关系也要靠一个互斥锁,故需要定义两个互斥锁。不能设置成一个互斥锁,因为生产者与消费者之间除非所占空间相同,不然没有互斥关系。

#pragma once

#include <iostream>
#include <pthread.h>
#include <memory>
#include <vector>
#include <queue>
#include <unistd.h>
#include <semaphore.h>

using std::cout;
using std::endl;
const int N = 5;

template <class T>
class RingQueue
{
public:
    RingQueue(int cap = N) : _cap(cap), _consumerIndex(0), _productorIndex(0), _vRingQueue(cap)
    {
        pthread_mutex_init(&_consumerMutex, nullptr);
        pthread_mutex_init(&_productorMutex, nullptr);
        
        sem_init(&_spaceSem, 0, _cap);
        sem_init(& _dataSem, 0, 0);
    }

    void push(const T &in)
    {
        // 生产者生产数据
        P(_spaceSem);
        lock(_productorMutex);

        _vRingQueue[_productorIndex++] = in;
        _productorIndex %= _cap;
        unlock(_productorMutex);
        V(_dataSem);
    }

    void pop(T *out)
    {

        P(_dataSem);
        lock(_consumerMutex);

        *out = _vRingQueue[_consumerIndex++];
        _consumerIndex %= _cap;

        unlock(_consumerMutex);
        V(_spaceSem);
    }

    ~RingQueue()
    {
        pthread_mutex_destroy(&_consumerMutex);
        pthread_mutex_destroy(&_productorMutex);

        sem_destroy(&_spaceSem);
        sem_destroy(&_dataSem);
    }

private:
    void lock(pthread_mutex_t &mutex)
    {
        pthread_mutex_lock(&mutex);
    }

    void unlock(pthread_mutex_t &mutex) 
    {   pthread_mutex_unlock(&mutex);}
    void P(sem_t &sem){  sem_wait(&sem);}

    void V(sem_t &sem){sem_post(&sem);}

private:
    std::vector<T> _vRingQueue;
    int _cap; // 环形队列容量

    sem_t _spaceSem; // 生产者关系的是空间资源
    sem_t _dataSem;  // 消费者关心的是数据资源

    pthread_mutex_t _consumerMutex;
    pthread_mutex_t _productorMutex;

    // 消费者访问空间
    int _consumerIndex;
    // 生产者访问空间
    int _productorIndex;
};

七.常见概念

  1. STL中的容器不是线程安全的
  2. 智能指针是线程安全的
    1. unique_ptr:不涉及全局变量,只在局部有效
    2. shared_ptr:引用计数有线程安全问题,但是设计者采用了CAS操作,保证了线程安全
  3. 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
  4. 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
  5. 自旋锁:非阻塞的申请锁资源。如果访问临界区的时间短可以采用自旋锁。
    1. pthread_spinlock_t:可以定义自旋锁

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SpringCloud-Eureka服务注册中心测试实践

5. Eureka服务注册中心 5.1 什么是Eureka Netflix在涉及Eureka时&#xff0c;遵循的就是API原则.Eureka是Netflix的有个子模块&#xff0c;也是核心模块之一。Eureka是基于REST的服务&#xff0c;用于定位服务&#xff0c;以实现云端中间件层服务发现和故障转移&#xff0c;服…

fast.ai 深度学习笔记(六)

深度学习 2&#xff1a;第 2 部分第 12 课 原文&#xff1a;medium.com/hiromi_suenaga/deep-learning-2-part-2-lesson-12-215dfbf04a94 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 来自 fast.ai 课程的个人笔记。随着我继续复习课程以“真正”理解它&#xff0c;…

Java 基于微信小程序的私家车位共享系统

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

LC 987. 二叉树的垂序遍历

987. 二叉树的垂序遍历 难度 : 困难 题目大意&#xff1a; 给你二叉树的根结点 root &#xff0c;请你设计算法计算二叉树的 垂序遍历 序列。 对位于 (row, col) 的每个结点而言&#xff0c;其左右子结点分别位于 (row 1, col - 1) 和 (row 1, col 1) 。树的根结点位于 …

爬虫2—用爬虫爬取壁纸(想爬多少张爬多少张)

先看效果图&#xff1a; 我这个是爬了三页的壁纸60张。 上代码了。 import requests import re import os from bs4 import BeautifulSoupcount0 img_path "./壁纸图片/"#指定保存地址 if not os.path.exists(img_path):os.mkdir(img_path) headers{ "User-Ag…

【STL】string的模拟实现

string类的模拟实现 一、接口函数总览二、默认成员函数1、构造函数2、拷贝构造函数&#xff08;1&#xff09;写法一&#xff1a;传统写法&#xff08;2&#xff09;写法二&#xff1a;现代写法 3、赋值运算符重载函数&#xff08;1&#xff09;写法一&#xff1a;传统写法&…

【开源】JAVA+Vue.js实现天然气工程运维系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 系统角色分类2.2 核心功能2.2.1 流程 12.2.2 流程 22.3 各角色功能2.3.1 系统管理员功能2.3.2 用户服务部功能2.3.3 分公司&#xff08;施工单位&#xff09;功能2.3.3.1 技术员角色功能2.3.3.2 材料员角色功能 2.3.4 安…

东风联手华为打造首款SUV,车长超5米,配纯电和增程双动力系统

在上个月&#xff08;2024年1月份&#xff09;&#xff0c;东风汽车和华为达成了战略合作计划&#xff0c;两家品牌将联手打造全新的汽车品牌——奕派汽车&#xff0c;而目前我们从相关渠道获悉&#xff0c;其首款SUV车型已经获得了实拍亮相&#xff0c;而新车的内部代号为S59&…

MySQL篇----第十四篇

系列文章目录 文章目录 系列文章目录前言一、MySQL 数据库作发布系统的存储,一天五万条以上的增量,预计运维三年,怎么优化?二、锁的优化策略三、索引的底层实现原理和优化四、什么情况下设置了索引但无法使用前言 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽…

Java使用opencsv完成对csv批量操作

文章目录 前言一、maven二、造数三、代码部分1.OpenCsvController2.OpenCsvUtil3.StudentInfo4.CodeToValue 三、效果展示1.download2.upload 总结 前言 csv文件是不同于excel文件的另一种文件&#xff0c;常常以,作为分隔符&#xff0c;本篇将通过JavaBean的形式完成对csv文件…

《Git 简易速速上手小册》第2章:理解版本控制(2024 最新版)

文章目录 2.1 本地仓库与版本历史2.1.1 基础知识讲解2.1.2 重点案例&#xff1a;回滚错误提交2.1.3 拓展案例 1&#xff1a;利用 git bisect 查找引入 bug 的提交2.1.4 拓展案例 2&#xff1a;合并提交历史 2.2 远程仓库的使用2.2.1 基础知识讲解2.2.2 重点案例&#xff1a;在 …

CSP-动态规划-最长公共子序列(LCS)

一、动态规划 动态规划&#xff08;Dynamic Programming&#xff0c;简称DP&#xff09;主要用于求解可以被分解为相似子问题的复杂问题&#xff0c;特别是在优化问题上表现出色&#xff0c;如最短路径、最大子数组和、编辑距离等。动态规划的核心思想是将原问题分解为较小的子…

Old Money 和 New Money

&#xff08;1&#xff09; 我想借用一下&#xff1a;Old Money 和 New Money这两个词&#xff0c;但不是欧洲那种Old Money 和 New Money的定义。 我定义的Old Money是&#xff1a; 人脉关系、资源 信息差、成本差 我定义的New Money是&#xff1a; 科技是第一生产力 2015年以…

计算机网络——09Web-and-HTTP

Web and HTTP 一些术语 Web页&#xff1a;由一些对象组成对象可以是HTML文件、JPEG图像&#xff0c;JAVA小程序&#xff0c;声音剪辑文件等Web页含有一个基本的HTML文件&#xff0c;该基本HTML文件又包含若干对象的引用&#xff08;链接&#xff09;通过URL对每个对象进行引用…

程序员与电脑:不眠之夜的背后故事

在这个数字化飞速发展的时代&#xff0c;程序员和他们的电脑成了不可分割的伙伴。 如果你有机会深夜走过城市的某个角落&#xff0c;透过窗户瞥见那些亮着的电脑屏幕&#xff0c;你可能会好奇&#xff1a;这些电脑为什么总是开着的&#xff1f; 难道程序员们都有失眠症吗&…

猫头虎分享已解决Bug ‍ || 修改mongodb3.0副本集用户密码遇到 BeanDefinitionParsingException

博主猫头虎的技术世界 &#x1f31f; 欢迎来到猫头虎的博客 — 探索技术的无限可能&#xff01; 专栏链接&#xff1a; &#x1f517; 精选专栏&#xff1a; 《面试题大全》 — 面试准备的宝典&#xff01;《IDEA开发秘籍》 — 提升你的IDEA技能&#xff01;《100天精通鸿蒙》 …

Solidworks:从草图到工程图纸,掌握正确的工作流程

1. 草图不及太在意构造线和尺寸标注的美观性&#xff0c;只要确保模型尺寸正确即可 因为草图不是最终输出的&#xff0c;这个阶段的工作重点是建立尺寸正确的实体模型&#xff0c;所以不要在意构造线和尺寸标注是否美观。 2. 工程图纸中标注尽量按照操作提示放置位置 工程图…

《Linux 简易速速上手小册》第10章: 性能监控与优化(2024 最新版)

文章目录 10.1 理解系统负载10.1.1 重点基础知识10.1.2 重点案例&#xff1a;服务器响应变慢10.1.3 拓展案例 1&#xff1a;多核 CPU 系统的负载解读10.1.4 拓展案例 2&#xff1a;分析具体时间段的系统负载 10.2 优化性能10.2.1 重点基础知识10.2.2 重点案例&#xff1a;优化 …

‘vue-cli-service‘ 不是内部或外部命令,也不是可运行的程序

遇到 vue-cli-service 不是内部或外部命令&#xff0c;也不是可运行的程序或批处理文件。 的错误时&#xff0c;通常意味着Vue CLI没有被正确安装或配置在项目中。这可能是因为node_modules目录缺失了必要的包&#xff0c;或者局部安装的Vue CLI没有被正确设置到系统的PATH环境…

springsecurity6使用

spring security 中的类 &#xff1a; AuthenticationManager : 实现类&#xff1a;ProviderManager 管理很多的 provider &#xff0c;&#xff0c;&#xff0c; 经常使用的&#xff0c;DaoAuthenticationProvider , 这个要设置一个 UserDetailService , 查找数据库&#xff…