当前位置: 首页 > news >正文

Linux多线程【线程互斥】

文章目录

    • Linux线程互斥
      • 进程线程间的互斥相关背景概念
      • 互斥量mutex
        • 模拟抢票代码
    • 互斥量的接口
      • 初始化互斥量
      • 销毁互斥量
      • 互斥量加锁和解锁
        • 改进模拟抢票代码(加锁)
        • 小结
        • 对锁封装 lockGuard.hpp
    • 互斥量实现原理探究
    • 可重入VS线程安全
        • 概念
        • 常见的线程不安全的情况
        • 常见的线程安全的情况
        • 常见不可重入的情况
        • 常见可重入的情况
        • 可重入与线程安全联系
        • 可重入与线程安全区别
    • 常见锁概念
      • 死锁
      • 死锁四个必要条件
      • 避免死锁
      • 避免死锁算法
    • Linux线程同步
      • 条件变量
      • 同步概念与竞态条件
    • 条件变量函数
      • 初始化
      • 销毁
      • 等待条件满足
      • 唤醒等待
      • 案例:
    • 生产者消费者模型
      • 基于BlockingQueue的生产者消费者模型
    • C++ queue模拟阻塞队列的生产消费模型
      • blockqueue.hpp
      • Task.hpp
      • Task.hpp(改进版本)
      • main.cc
    • POSIX信号量
      • 初始化信号量
      • 销毁信号量
      • 等待信号量
      • 发布信号量
    • 基于环形队列的生产消费模型(代码验证)
      • makefile
      • RingQueue.hpp
      • Task.hpp
      • Main.cc
    • 线程池
      • makefile
      • ThreadPool.hpp
      • Main.cpp
      • 简单封装一下pthread库
    • STL、智能指针和线程安全
      • STL中的容器不是线程安全的
      • 智能指针是否是线程安全的
      • 常见的锁
    • 读者写者问题
      • 代码实现的较为简单的读者写者问题

Linux线程互斥

进程线程间的互斥相关背景概念

  • 临界资源:多线程执行流共享的资源就叫做临界资源
  • 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
  • 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
  • 原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成

互斥量mutex

  • 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个 线程,其他线程无法获得这种变量。
  • 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之 间的交互。
  • 多个线程并发的操作共享变量,会带来一些问题。

模拟抢票代码
  • 我们看下面这段代码:
#define NUM 4// 线程名字
class ThreadData
{
public:ThreadData(int num){threadname = "thread-" + to_string(num);}public:string threadname;
};int tickets = 1000;// 买票操作
void *getTicket(void *args)
{ThreadData *td = static_cast<ThreadData *>(args);const char *name = td->threadname.c_str();while (true){if (tickets > 0){usleep(1000);printf("who=%s, get a ticket: %d\n", name, tickets); // ?tickets--;}elsebreak;}printf("%s ... quit\n", name);return nullptr;
}int main()
{vector<pthread_t> tids;vector<ThreadData *> thread_datas;for (int i = 1; i <= NUM; i++){pthread_t tid;ThreadData *td = new ThreadData(i);thread_datas.push_back(td);pthread_create(&tid, nullptr, getTicket, thread_datas[i - 1]);tids.push_back(tid);}// 等待for (auto e : tids){pthread_join(e, nullptr);}// 释放for (auto e : thread_datas){delete e;}return 0;
}

在这里插入图片描述

  • 造成了数据不一致问题,肯定是和多线程并发有关系

  • 一个全局变量进行多线程并发++或者--操作是否安全?–>不安全!

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

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

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


取出ticket--部分的汇编代码

objdump -d mythread > test.objdump
14e6:	8b 05 24 5b 00 00    	mov    0x5b24(%rip),%eax        # 7010 <tickets>
14ec:	83 e8 01             	sub    $0x1,%eax
14ef:	89 05 1b 5b 00 00    	mov    %eax,0x5b1b(%rip)        # 7010 <tickets>

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

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

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

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

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

在这里插入图片描述

互斥量的接口

  • 在Ubuntu下查看需要先安装手册
sudo apt-get install manpages-posix-dev

初始化互斥量

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

在这里插入图片描述

  • 第一个是动态分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
  • 第二个是静态分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

销毁互斥量

#include <pthread.h>int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

在这里插入图片描述

销毁互斥量需要注意:

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

互斥量加锁和解锁

#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);

在这里插入图片描述

调用 pthread_ lock 时,可能会遇到以下情况:

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

加锁的本质:是用时间来换取安全
加锁的表现:线程对于临界区代码串执行
加锁原则:尽量的要保证临界区代码,越少越好


改进模拟抢票代码(加锁)
  • 然后就可以使用上面的性质来进行改进模拟抢票代码
#define NUM 4
pthread_mutex_t lock;// 线程名字
class ThreadData
{
public:ThreadData(int num, pthread_mutex_t *mutex){lock = mutex; // 初始化锁threadname = "thread-" + to_string(num);}public:string threadname;pthread_mutex_t *lock; // 定义一个锁指针
};int tickets = 1000;// 买票操作
void *getTicket(void *args)
{ThreadData *td = static_cast<ThreadData *>(args);const char *name = td->threadname.c_str();while (true){// 加锁 -->pthread_mutex_lock(td->lock); // 申请锁成功,才能往后执行,不成功,阻塞等待。if (tickets > 0){usleep(1000);printf("who=%s, get a ticket: %d\n", name, tickets);tickets--;// 解锁 -->pthread_mutex_unlock(td->lock);}else{// 解锁 --> 在else执行流要注意break,之前要解锁pthread_mutex_unlock(td->lock);break;}usleep(13); // 执行得到票之后的后续动作,如果不加usleep会导致 饥饿问题}printf("%s ... quit\n", name);return nullptr;
}int main()
{vector<pthread_t> tids;vector<ThreadData *> thread_datas;// 初始化锁pthread_mutex_init(&lock, nullptr);for (int i = 1; i <= NUM; i++){pthread_t tid;ThreadData *td = new ThreadData(i, &lock);thread_datas.push_back(td);pthread_create(&tid, nullptr, getTicket, thread_datas[i - 1]);tids.push_back(tid);}// 等待for (auto e : tids){pthread_join(e, nullptr);}// 释放for (auto e : thread_datas){delete e;}return 0;
}

在这里插入图片描述


  • 或者使用全局初始化也可以
#define NUM 4// 直接初始化锁
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;// 线程名字
class ThreadData
{
public:ThreadData(int num){threadname = "thread-" + to_string(num);}public:string threadname;
};int tickets = 1000;// 买票操作
void *getTicket(void *args)
{ThreadData *td = static_cast<ThreadData *>(args);const char *name = td->threadname.c_str();while (true){pthread_mutex_lock(&lock);if (tickets > 0){usleep(1000);printf("who=%s, get a ticket: %d\n", name, tickets);tickets--;// 解锁 -->pthread_mutex_unlock(&lock);}else{// 解锁 -->pthread_mutex_unlock(&lock);break;}usleep(13); // 执行得到票之后的后续动作,如果不加usleep会导致 饥饿问题}printf("%s ... quit\n", name);return nullptr;
}int main()
{vector<pthread_t> tids;vector<ThreadData *> thread_datas;// 初始化锁pthread_mutex_init(&lock, nullptr);for (int i = 1; i <= NUM; i++){pthread_t tid;ThreadData *td = new ThreadData(i);thread_datas.push_back(td);pthread_create(&tid, nullptr, getTicket, thread_datas[i - 1]);tids.push_back(tid);}// 等待for (auto e : tids){pthread_join(e, nullptr);}// 释放for (auto e : thread_datas){delete e;}return 0;
}

在这里插入图片描述

  • 如果纯互斥环境,如果锁分配不够合理,容易被其他线程的饥饿问题!
  • 不是说只要有互斥,必有饥饿
  • 适合纯互斥的场景,就用互斥
  • 如果是按照一定的顺序性获取资源,就叫同步

  • 其中在临界区中,线程可以被切换吗? 当然可以

    • 在线程被切出去的时候,是持有锁走的
    • 不在临界区之间,照样没有线程可以进入临界区访问临界资源
  • 对于其他线程来讲,一个线程要么没有锁,要么释放锁。

  • 当线程访问临界资源区的过程,对于其他线程是原子的。


小结
  1. 所以,加锁的范围,粒度一定要小
  2. 任何线程,要进行抢票,都得先申请锁,原则上,不应该有例外
  3. 所有线程申请锁,前提是所有线程都得看到这把锁,锁本身也是共享资源,加锁的过程,必须是原子的!原子性:要么不做,要做就做完,没有中间状态,就是原子性
  4. 如果线程申请锁失败了,我的线程要被阻塞
  5. 如果线程申请成功了,继续向后运行
  6. 如果线程申请锁成功了,执行临界区的代码了,执行临界区的代码是可以被切换的,其他线程无法进入,因为被切换了,但是没有释放,可以放心的执行完毕,没有任何线程能打扰。

结论:所有对于其他线程,要么没有申请锁,要么释放了锁,对于其他线程才有意义


什么是线程互斥,为什么需要互斥:

  • 线程互斥指的是在多个线程间对临界资源进行争抢访问时有可能会造成数据二义,因此通过保证同一时间只有一个线程能够访问临界资源的方式实现线程对临界资源的访问安全性

对锁封装 lockGuard.hpp

lockGuard.hpp

#pragma once
#include <pthread.h>
class Mutex
{public:Mutex(pthread_mutex_t *lock) : _lock(lock){}void Lock(){pthread_mutex_lock(_lock);}void Unlock(){pthread_mutex_unlock(_lock);}~Mutex(){}private:pthread_mutex_t *_lock;
};class LockGuard
{
public:LockGuard(pthread_mutex_t *lock) : _mutex(lock){_mutex.Lock();}~LockGuard(){_mutex.Unlock();}private:Mutex _mutex;
};

mythread.cc

void *getTicket(void *args)
{ThreadData *td = static_cast<ThreadData *>(args);const char *name = td->threadname.c_str();while (true){{ // 对锁进行临时对象控制// 定义了一个临时的锁对象LockGuard lockGuard(&lock); // RAII风格的锁if (tickets > 0){usleep(1000);printf("who=%s, get a ticket: %d\n", name, tickets);tickets--;}elsebreak;}usleep(13); // 执行得到票之后的后续动作,如果不加usleep会导致 饥饿问题}printf("%s ... quit\n", name);return nullptr;
}

互斥量实现原理探究

  • 经过上面的例子,已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题
  • 为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一 个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 现在我们把lock和unlock的伪代码改一下

在这里插入图片描述

  1. CPU的寄存器只有一套,被所有的线程共享,但是寄存器里面的数据,属于执行流的上下文,属于执行流私有的数据
  2. CPU在执行代码的时候,一定要有对应的执行载体,线程和进程
  3. 数据在内存中没被所有线程共享

结论:把数据从内存移动到CPU寄存器中,本质是把数据从共享变成线程私有!!


  • 竞争锁本质是在谁先把xchgb做完

  • mutex简单理解就是一个0/1的计数器,用于标记资源访问状态:

    • 0表示已经有执行流加锁成功,资源处于不可访问,
    • 1表示未加锁,资源可访问。

可重入VS线程安全

概念
  • 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作, 并且没有锁保护的情况下,会出现该问题。
  • 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们 称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重 入函数,否则,是不可重入函数。
常见的线程不安全的情况
  • 不保护共享变量的函数
  • 函数状态随着被调用,状态发生变化的函数
  • 返回指向静态变量指针的函数
  • 调用线程不安全函数的函数
常见的线程安全的情况
  • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
  • 类或者接口对于线程来说都是原子操作
  • 多个线程之间的切换不会导致该接口的执行结果存在二义性
常见不可重入的情况
  • 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
  • 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
  • 可重入函数体内使用了静态的数据结构
常见可重入的情况
  • 不使用全局变量或静态变量
  • 不使用用malloc或者new开辟出的空间
  • 不调用不可重入函数 不返回静态或全局数据,所有数据都有函数的调用者提供
  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
可重入与线程安全联系
  • 函数是可重入的,那就是线程安全的
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
  • 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
可重入与线程安全区别
  • 可重入函数是线程安全函数的一种
  • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
  • 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

常见锁概念

死锁

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

死锁四个必要条件

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

避免死锁

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

避免死锁算法

  • 死锁检测算法
  • 银行家算法

Linux线程同步

条件变量

  • 当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
  • 例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情 况就需要用到条件变量。

同步概念与竞态条件

  • 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
  • 竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解

条件变量函数

初始化

#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;
  • cond:要初始化的条件变量
  • attr:NULL

在这里插入图片描述

销毁

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);
  • cond:要在这个条件变量上等待
  • mutex:互斥量,后面详细解释

在这里插入图片描述

唤醒等待

#include <pthread.h>int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

在这里插入图片描述

案例:

#include <iostream>
#include <unistd.h>
#include <pthread.h>// 临界资源
int cnt = 0;// 初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;void *Count(void *args)
{uint64_t number = (uint64_t)args;std::cout << "pthread: " << number << " create sucess! " << std::endl;while (true){// 加锁pthread_mutex_lock(&mutex);// 必须先加锁后等待pthread_cond_wait(&cond, &mutex);std::cout << "pthread: " << number << ", cnt: " << cnt++ << std::endl;// 解锁pthread_mutex_unlock(&mutex);}// 分离线程pthread_detach(pthread_self());
}int main()
{for (uint64_t i = 0; i < 5; i++){pthread_t tid;pthread_create(&tid, nullptr, Count, (void *)i); // 注意这里细节不能传i的地址usleep(1000);                                    // 创建出来的线程打印就保持一致了}sleep(3);std::cout << "main thread ctrl begin: " << std::endl;while (true){sleep(1);// 唤醒单个线程pthread_cond_signal(&cond);std::cout << "signal thread..." << std::endl;}return 0;
}

在这里插入图片描述

  • 也还可以唤醒多个线程
#include <iostream>
#include <unistd.h>
#include <pthread.h>// 临界资源
int cnt = 0;// 初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;void *Count(void *args)
{uint64_t number = (uint64_t)args;std::cout << "pthread: " << number << " create sucess! " << std::endl;while (true){// 加锁pthread_mutex_lock(&mutex);// 你怎么知道临界资源是就绪还是不就绪的?你判断出来的!判断是访问临界资源吗?必须是的,也就是判断必须在加锁之后!!!// 必须先加锁后等待pthread_cond_wait(&cond, &mutex); // pthread_cond_wait让线程等待的时候,会自动释放锁!std::cout << "pthread: " << number << ", cnt: " << cnt++ << std::endl;// 解锁pthread_mutex_unlock(&mutex);}// 分离线程pthread_detach(pthread_self());
}int main()
{for (uint64_t i = 0; i < 5; i++){pthread_t tid;pthread_create(&tid, nullptr, Count, (void *)i); // 注意这里细节不能传i的地址usleep(1000);                                    // 创建出来的线程打印就保持一致了}sleep(3);std::cout << "main thread ctrl begin: " << std::endl;while (true){sleep(1);// 唤醒多个线程pthread_cond_broadcast(&cond);std::cout << "boardcast thread..." << std::endl;}return 0;
}

在这里插入图片描述

为什么 pthread_cond_wait需要互斥量?

  • 条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。
  • 条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据。

在这里插入图片描述

按照上面的说法,我们设计出如下的代码:先上锁,发现条件不满足,解锁,然后等待在条件变量上不就行了

  • 由于解锁和等待不是原子操作。调用解锁之后, pthread_cond_wait 之前,如果已经有其他线程获取到 互斥量,摒弃条件满足,发送了信号,那么 pthread_cond_wait 将错过这个信号,可能会导致线程永远 阻塞在这个 pthread_cond_wait 。所以解锁和等待必须是一个原子操作
  • int pthread_cond_wait(pthread_cond_ t *cond,pthread_mutex_ t * mutex);进入该函数后, 会去看条件量等于0不?等于,就把互斥量变成1,直到cond_ wait返回,把条件量改成1,把互斥量恢复成原样。

生产者消费者模型

  • 3种关系、2种角色、1个交易场所:

    • 3种关系:生产者和生产者(互斥),消费者和消费者(互斥),生产者和消费者(互斥 / 同步)

    • 2种角色:生产者、消费者(线程承担)

    • 1个交易场所:内存中特定的一种内存结构。

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

优点:

  1. 支持忙闲不闲
  2. 生产和消费进行解耦
  3. 支持并发

在这里插入图片描述

  • 生产者vs生产者:互斥
  • 消费者和消费者:互斥
  • 生产者和消费者:互斥,同步

基于BlockingQueue的生产者消费者模型

BlockingQueue

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

在这里插入图片描述

C++ queue模拟阻塞队列的生产消费模型

blockqueue.hpp

#include <iostream>
#include <queue>template <class T>
class BlockQueue
{static const int defaultnum = 5;public:BlockQueue(int maxcap = defaultnum): maxcap_(maxcap){// 初始化锁pthread_mutex_init(&mutex_, nullptr);pthread_cond_init(&c_cond_, nullptr);pthread_cond_init(&p_cond_, nullptr);}T Pop(){// 加锁pthread_mutex_lock(&mutex_);while (q_.size() == 0) // 因为判断临界资源调试是否满足,也是在访问临界资源!判断资源是否就绪,是通过再临界资源内部判断的。{// 进行等待,调用的时候,自动释放锁,因为唤醒而返回的时候,重新持有锁pthread_cond_wait(&c_cond_, &mutex_);}// 取出队头数据,进行处理T out = q_.front();q_.pop();// 唤醒生产pthread_cond_signal(&p_cond_);// 如果线程wait时,被误唤醒了呢?? -->// pthread_cond_broadcast(&c_cond_, &mutex_); // 唤醒所有线程// 解锁pthread_mutex_unlock(&mutex_);return out;}void Push(const T &in){// 加锁pthread_mutex_lock(&mutex_);while (q_.size() == maxcap_) // 因为判断临界资源调试是否满足,也是在访问临界资源!判断资源是否就绪,是通过再临界资源内部判断的。{pthread_cond_wait(&p_cond_, &mutex_);}// 入队q_.push(in);// 唤醒消费pthread_cond_signal(&c_cond_);// 解锁pthread_mutex_unlock(&mutex_);}// 析构~BlockQueue(){pthread_cond_destroy(&c_cond_);pthread_cond_destroy(&p_cond_);pthread_mutex_destroy(&mutex_);}private:// 一个队列std::queue<T> q_;// 一个极值,队列到达极值就不可入队int maxcap_;// 一个消费者,一个生产者pthread_cond_t c_cond_;pthread_cond_t p_cond_;// 锁pthread_mutex_t mutex_;
};

Task.hpp

#pragma once
#include <iostream>
#include <string>std::string opers = "+-*/%";enum
{DivZero = 1,ModZero,Unknown
};class Task
{
public:Task(int x, int y, char op): data1_(x), data2_(y), oper_(op){}void Run(){switch (oper_){case '+':result_ = data1_ + data2_;break;case '-':result_ = data1_ - data2_;break;case '*':result_ = data1_ * data2_;break;case '/':{if (data2_ == 0)exitcode_ = DivZero;elseresult_ = data1_ / data2_;}break;case '%':{if (data2_ == 0)exitcode_ = ModZero;elseresult_ = data1_ % data2_;}break;default:exitcode_ = Unknown;break;}}void operator()(){Run();}std::string GetResult() // 这里可以使用stringstream{std::string r = std::to_string(data1_);r += oper_;r += std::to_string(data2_);r += "=";r += std::to_string(result_);r += "[code: ";r += std::to_string(exitcode_);r += "]";return r;}std::string GetTask(){std::string r = std::to_string(data1_);r += oper_;r += std::to_string(data2_);r += "=?";return r;}~Task(){}private:int data1_; // 操作数int data2_; // 操作数char oper_; // 操作符int result_;   // 结果int exitcode_; // 错误码
};

Task.hpp(改进版本)

  • 我们也可以使用包装器stringstream来改进一下
#pragma once
#include <iostream>
#include <string>
#include <sstream>
#include <functional>
#include <map>std::string opers = "+-*/%";enum
{DivZero = 1,ModZero,Unknown
};class Task
{
public:Task(int x, int y, char op): data1_(x), data2_(y), oper_(op){}void Run(){std::map<char, std::function<void()>> CmdOp{{'+', [this](){ result_ = data1_ + data2_; }},{'-', [this](){ result_ = data1_ - data2_; }},{'*', [this](){ result_ = data1_ * data2_; }},{'/', [this](){ if (data2_ == 0) exitcode_ = DivZero; else result_ = data1_ / data2_; }},{'%', [this](){ if (data2_ == 0) exitcode_ = ModZero; else result_ = data1_ % data2_; }}};// auto it = CmdOp.find(oper_);std::map<char, std::function<void()>>::iterator it = CmdOp.find(oper_);if (it != CmdOp.end())it->second(); // 调用lambda函数elseexitcode_ = Unknown; // 如果没有找到操作,设置错误代码}void operator()(){Run();}std::string GetResult() // 这里可以使用stringstream{std::stringstream s;s << data1_ << oper_ << data2_ << "=" << result_ << " [code: " << exitcode_ << "]";std::string r;r = s.str();return r;}std::string GetTask(){std::stringstream s;s << data1_ << oper_ << data2_ << "=?";std::string r;r = s.str();return r;}~Task(){}private:int data1_; // 操作数int data2_; // 操作数char oper_; // 操作符int result_;   // 结果int exitcode_; // 错误码
};

main.cc

#include "blockqueue.hpp"
#include "Task.hpp"
#include <unistd.h>
#include <ctime>std::string TOHEX(pthread_t x)
{char buffer[128];snprintf(buffer, sizeof(buffer), "%p", x);return buffer;
}void *Consumer(void *args)
{BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(args);while (true){// 消费Task t = bq->Pop();// t.Run(); // 之间调用t(); // 仿函数std::cout << "处理任务: " << t.GetTask() << " 运算结果是: " << t.GetResult() << " thread id: " << TOHEX(pthread_self()) << std::endl;// sleep(1);}
}void *Productor(void *args)
{int len = opers.size();BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(args);while (true){// 生产int data1 = rand() % 10 + 1;int data2 = rand() % 10 + 1;usleep(10);char op = opers[rand() % len];Task t(data1, data2, op);bq->Push(t); // 入队std::cout << "生产了一个任务: " << t.GetTask() << " thread id: " << TOHEX(pthread_self()) << std::endl;sleep(1);}
}int main()
{srand(time(nullptr));// 在堆上上创建对象BlockQueue<Task> *bq = new BlockQueue<Task>();pthread_t c[3], p[5];for (int i = 0; i < 3; i++){pthread_create(c + i, nullptr, Consumer, bq);}for (int i = 0; i < 5; i++){pthread_create(p + i, nullptr, Productor, bq);}for (int i = 0; i < 3; i++){pthread_join(c[i], nullptr);}for (int i = 0; i < 5; i++){pthread_join(p[i], nullptr);}delete bq;return 0;
}

POSIX信号量

  • 信号量的本质是一把计数器,那么这把计数器的本质是什么??

    • 来描述资源数目的,把资源是否就绪放在了临界区之外,申请信号量时,其实就间接的已经在做判断了!

    • –P->原子的->申请资源

    • ++V->原子的->归还资源

  • 信号量申请成功了,就一定保证会拥有一部分临界资源吗?

    • 只要信号量申请成功,就一定会获得指定的资源。

    • 申请mutex,只要拿到了锁,就可以获得临界资源,并且不担心被切换。

  • 临界资源可以当成整体,可不可以看成一小部分一小部分呢?

    • 结合场景,一般是可以的

信号量:

  • –p:1->0 ---- 加锁

  • ++v: 0->1 ---- 释放锁

  • 这样的叫做二元信号量 == 互斥锁

初始化信号量

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);

在这里插入图片描述

  • 参数:
    • pshared:0表示线程间共享,非零表示进程间共享
    • value:信号量初始值

销毁信号量

在这里插入图片描述

int sem_destroy(sem_t *sem);

等待信号量

在这里插入图片描述

  • 功能:等待信号量,会将信号量的值减1

int sem_wait(sem_t *sem); //P()

发布信号量

在这里插入图片描述

  • 功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。
int sem_post(sem_t *sem);//V()

基于环形队列的生产消费模型(代码验证)

  • 环形队列中的生产者和消费者什么时候会访问同一个位置?

    • 当这两个同时指向同一个位置的时候,只有满or空的时候(互斥and同步)

    • 其它时候都指向的是不同的位置(并发)

在这里插入图片描述

在这里插入图片描述

因此,操作的基本原则:

① 空:消费者不能超过生产者 -->生产者先运行

② 满:生产者不能把消费者套一个圈里,继续再往后写入 -->消费者先运行

  • 谁来保证这个基本原则呢?

    • 信号量来保证。
  • 生产者最关心的是什么资源?

    • 空间
  • 消费者最关心的是什么资源?

    • 数据
  • 怎么保证,不同的线程,访问的是临界资源中不同的区域呢?

    • 通过程序员编码保证

makefile

myRingQueue:Main.ccg++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:rm -rf myRingQueue

RingQueue.hpp

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <vector>
#include <semaphore.h>// 默认队列容量
const static int defaultcap = 5;template <class T>
class RingQueue
{
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);}public:RingQueue(int cap = defaultcap): ringqueue_(cap), cap_(cap), c_step_(0), p_step_(0){sem_init(&cdata_sem_, 0, 0);    // 消费者初始值为0sem_init(&pspace_sem_, 0, cap); // 生产者初始值为容量pthread_mutex_init(&c_mutex_, nullptr);pthread_mutex_init(&p_mutex_, nullptr);}// 生产void Push(const T &in){// 先申请空间--P(pspace_sem_); // P-  V+// 加锁Lock(p_mutex_);// 将数据存放ringqueue_[p_step_] = in;p_step_++;p_step_ %= cap_;// 解锁UnLock(p_mutex_);// data资源++V(cdata_sem_);}// 消费void Pop(T *out){// 申请资源--P(cdata_sem_);// 加锁Lock(c_mutex_);*out = ringqueue_[c_step_];c_step_++;c_step_ %= cap_;// 解锁UnLock(c_mutex_);// 释放空间++V(pspace_sem_);}~RingQueue(){sem_destroy(&cdata_sem_);sem_destroy(&pspace_sem_);pthread_mutex_destroy(&c_mutex_);pthread_mutex_destroy(&p_mutex_);}private:std::vector<T> ringqueue_; // 队列int cap_;                  // 队列的容量int c_step_; // 消费者下标int p_step_; // 生产者下标sem_t cdata_sem_;  // 消费者关注的数据资源sem_t pspace_sem_; // 生产者关注的空间资源pthread_mutex_t c_mutex_; // 消费者锁pthread_mutex_t p_mutex_; // 生产者锁
};

Task.hpp

#pragma once
#include <iostream>
#include <string>
#include <sstream>
#include <functional>
#include <map>std::string opers = "+-*/%";enum
{DivZero = 1,ModZero,Unknown
};class Task
{
public:Task(){}Task(int x, int y, char op): data1_(x), data2_(y), oper_(op){}void Run(){std::map<char, std::function<void()>> CmdOp{{'+', [this](){ result_ = data1_ + data2_; }},{'-', [this](){ result_ = data1_ - data2_; }},{'*', [this](){ result_ = data1_ * data2_; }},{'/', [this](){ if (data2_ == 0) exitcode_ = DivZero; else result_ = data1_ / data2_; }},{'%', [this](){ if (data2_ == 0) exitcode_ = ModZero; else result_ = data1_ % data2_; }}};// auto it = CmdOp.find(oper_);std::map<char, std::function<void()>>::iterator it = CmdOp.find(oper_);if (it != CmdOp.end())it->second(); // 调用lambda函数elseexitcode_ = Unknown; // 如果没有找到操作,设置错误代码}void operator()(){Run();}std::string GetResult() // 这里可以使用stringstream{std::stringstream s;s << data1_ << oper_ << data2_ << "=" << result_ << " [code: " << exitcode_ << "]";std::string r;r = s.str();return r;}std::string GetTask(){std::stringstream s;s << data1_ << oper_ << data2_ << "=?";std::string r;r = s.str();return r;}~Task(){}private:int data1_; // 操作数int data2_; // 操作数char oper_; // 操作符int result_;   // 结果int exitcode_; // 错误码
};

Main.cc

#include <iostream>
#include <pthread.h>
#include <ctime>
#include "RingQueue.hpp"
#include "Task.hpp"using namespace std;// 创建的线程数量
#define ProductorNum 5
#define ConsumerNum 5struct ThreadData
{RingQueue<Task> *rq;std::string threadname;
};void *Productor(void *args)
{ThreadData *td = static_cast<ThreadData *>(args);RingQueue<Task> *rq = td->rq;std::string name = td->threadname;int len = opers.size();while (true){// 1. 获取数据int data1 = rand() % 10 + 1;usleep(10);int data2 = rand() % 10 + 1;char op = opers[rand() % len];Task t(data1, data2, op);t();// 2. 生产数据rq->Push(t);cout << "Productor task done, task is : " << t.GetTask() << " who: " << name << endl;sleep(1);}return nullptr;
}
void *Consumer(void *args)
{ThreadData *td = static_cast<ThreadData *>(args);RingQueue<Task> *rq = td->rq;std::string name = td->threadname;while (true){// 消费数据Task t;rq->Pop(&t);t(); // 处理数据cout << "Consumer get task, task is : " << t.GetTask() << " who: " << name << " result: " << t.GetResult() << endl;// sleep(1);}return nullptr;
}int main()
{srand(time(nullptr) ^ getpid());RingQueue<Task> *rq = new RingQueue<Task>(5);pthread_t c[ConsumerNum], p[ProductorNum];for (int i = 0; i < ProductorNum; i++){ThreadData *td = new ThreadData();td->rq = rq;td->threadname = "Productor-" + std::to_string(i);pthread_create(p + i, nullptr, Productor, td);}for (int i = 0; i < ConsumerNum; i++){ThreadData *td = new ThreadData();td->rq = rq;td->threadname = "Consumer-" + std::to_string(i);pthread_create(c + i, nullptr, Consumer, td);}for (int i = 0; i < ProductorNum; i++){pthread_join(p[i], nullptr);}for (int i = 0; i < ConsumerNum; i++){pthread_join(c[i], nullptr);}return 0;
}
  • 一个线程查看直观一些:

在这里插入图片描述

  • 多个线程一起跑,打印就会显示的很乱

在这里插入图片描述

线程池

  • 一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。

线程池的应用场景:

  • ① 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。

  • ② 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。

  • ③ 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误。

线程池示例:

  1. 创建固定数量线程池,循环从任务队列中获取任务对象

  2. 获取到任务对象后,执行任务对象中的任务接口


  • 下列是基于单例模式(懒汉模式)来创建的线程池

makefile

ThreadPool:Main.cppg++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:rm -f ThreadPool

ThreadPool.hpp

#pragma once
#include <iostream>
#include <queue>
#include <vector>
#include <pthread.h>
#include <unistd.h>static const int defaultnum = 5;struct ThreadInfo
{pthread_t tid;std::string name;
};template <class T>
class ThreadPool
{
private:void Lock(){pthread_mutex_lock(&mutex_);}void Unlock(){pthread_mutex_unlock(&mutex_);}void WakeUp(){pthread_cond_signal(&cond_);}std::string GetThreadName(pthread_t tid){for (const auto &ti : thread_){if (ti.tid == tid)return ti.name;}return "None";}bool IsQueueEmpty(){return task_.empty();}void ThreadSleep(){pthread_cond_wait(&cond_, &mutex_);}public:static void *HandlerTask(void *args){ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);std::string name = tp->GetThreadName(pthread_self()); // 获取名字while (true){// 加锁tp->Lock();// 如果队列为空就将线程放到条件变量下等待while (tp->IsQueueEmpty()){tp->ThreadSleep();}// 取出节点T t = tp->Pop();// 解锁tp->Unlock();// 执行任务t();std::cout << name << " run, " << "result: " << t.GetResult() << std::endl;}}void Start(){for (int i = 0; i < thread_.size(); i++){thread_[i].name = "Thread-" + std::to_string(i);pthread_create(&(thread_[i].tid), nullptr, HandlerTask, this);}}void Push(T &t){Lock(); // 先加锁task_.push(t);WakeUp(); // 唤醒Unlock(); // 解锁}T Pop(){T t = task_.front();task_.pop();return t;}// 单例模式创建对象static ThreadPool<T> *GetInstance(){// 其他线程就不会每次执行加锁和解锁了~if (nullptr == tp_){// 加锁pthread_mutex_lock(&lock_);if (nullptr == tp_){std::cout << "log: singleton create done first!" << std::endl;tp_ = new ThreadPool<T>();}// 解锁pthread_mutex_unlock(&lock_);}return tp_;}public:ThreadPool(int num = defaultnum): thread_(num){pthread_mutex_init(&mutex_, nullptr);pthread_cond_init(&cond_, nullptr);}~ThreadPool(){pthread_mutex_destroy(&mutex_);pthread_cond_destroy(&cond_);}private:std::vector<ThreadInfo> thread_; // 任务线程std::queue<T> task_;             // 任务队列pthread_mutex_t mutex_; // 锁pthread_cond_t cond_;   // 条件变量// 单例模式,只能创建一个对象static ThreadPool<T> *tp_;static pthread_mutex_t lock_;
};// 类外进行初始化
template <class T>
ThreadPool<T> *ThreadPool<T>::tp_ = nullptr;template <class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER;

Main.cpp

#include <iostream>
#include <ctime>
#include "Task.hpp"
#include "ThreadPool.hpp"int main()
{// 创建线程ThreadPool<Task> *tp = new ThreadPool<Task>(5);// 启动线程ThreadPool<Task>::GetInstance()->Start();srand(time(nullptr) ^ getpid());while (true){// 1. 构建任务int x = rand() % 10 + 1;usleep(10);int y = rand() % 10 + 1;char op = opers[rand() % opers.size()];Task t(x, y, op);// 2. 交给线程池进行处理ThreadPool<Task>::GetInstance()->Push(t);std::cout << "main thread make task: " << t.GetTask() << std::endl;sleep(1);}return 0;
}
  • 这样就创建起来跑起来了~~

在这里插入图片描述

简单封装一下pthread库

#include <iostream>
#include <string>
#include <ctime>
#include <pthread.h>typedef void (*callback_t)();
static int num = 1;class Thread
{
private:void Entery(){cb_();}static void *Routine(void *args){Thread *thread = static_cast<Thread *>(args);thread->Entery();return nullptr;}public:Thread(callback_t cb): tid_(0), name_(""), start_timestamp_(0), isrunning_(false), cb_(cb){}void Run(){name_ = "thread-" + std::to_string(num++);start_timestamp_ = time(nullptr);isrunning_ = true;pthread_create(&tid_, nullptr, Routine, this);}void Join(){pthread_join(tid_, nullptr);isrunning_ = false;}std::string Name(){return name_;}uint64_t StartTimestamp(){return start_timestamp_;}bool IsRunning(){return isrunning_;}private:pthread_t tid_;std::string name_;uint64_t start_timestamp_;bool isrunning_;// 回调函数callback_t cb_;
};

STL、智能指针和线程安全

STL中的容器不是线程安全的

原因:

  • STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响。

  • 而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶)。

  • 因此 STL 默认不是线程安全. 如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全。

智能指针是否是线程安全的

  • 对于 unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题.

  • 对于 shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题. 但是标准库实现的时候考虑到了这个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数。

常见的锁

  • 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。

  • 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。

  • CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。

  • 挂起等待锁:当某个线程没有申请到锁的时候,此时该线程会被挂起,即加入到等待队列等待。当锁被释放的时候,就会被唤醒,重新竞争锁。当临界区运行的时间较长时,我们一般使用挂起等待锁。我们先让线程PCB加入到等待队列中等待,等锁被释放时,再重新申请锁。

之前所学的互斥锁就是挂起等待锁

  • 自旋锁:当某个线程没有申请到锁的时候,该线程不会被挂起,而是每隔一段时间检测锁是否被释放。如果锁被释放了,那就竞争锁;如果没有释放,过一会儿再来检测。如果这里使用挂起等待锁,可能线程刚加入等待队列,锁就被释放了,因此,当临界区运行的时间较短时,我们一般使用自旋锁
pthread_spin_lock();
  • 自旋锁只需要把mutex变成spin

读者写者问题

  • 在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁

在这里插入图片描述

  • 3种关系:写者和写者(互斥),读者和读者(没有关系),读者和写者(互斥关系)

  • 2种角色:读者、写者

  • 1个交易场所:读写场所


  • 读者写者 vs 生产者消费者
    • 本质区别:消费者会把数据拿走,而读者不会

初始化:

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);

销毁:

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

加锁和解锁:

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);  // 读者加锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); // 写者加锁int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); // 读写者解锁
  • 设置读写优先:

    • 分为读者优先和写者优先。
  • 读者写者进行操作的时候,读者非常多,频率特别高,写者比较少,频率不高

  • 存在写者饥饿的情况

代码实现的较为简单的读者写者问题

#include <iostream>
#include <unistd.h>
#include <pthread.h>int board = 0;pthread_rwlock_t rw;using namespace std;void *reader(void *args)
{const char *name = static_cast<const char*>(args);cout << "run..." << endl;while(true){pthread_rwlock_rdlock(&rw);cout << "reader read : " << board << "tid: " << pthread_self() << endl;sleep(10);pthread_rwlock_unlock(&rw);}
}void *writer(void *args)
{const char *name = static_cast<const char*>(args);sleep(1);while(true){pthread_rwlock_wrlock(&rw);board++;cout << "I am writer" << endl;sleep(10);pthread_rwlock_unlock(&rw);}
}int main()
{pthread_rwlock_init(&rw, nullptr);pthread_t r1,r2,r3,r4,r5,r6, w;pthread_create(&r1, nullptr, reader, (void*)"reader");pthread_create(&r2, nullptr, reader, (void*)"reader");pthread_create(&r3, nullptr, reader, (void*)"reader");pthread_create(&r4, nullptr, reader, (void*)"reader");pthread_create(&r5, nullptr, reader, (void*)"reader");pthread_create(&r6, nullptr, reader, (void*)"reader");pthread_create(&w, nullptr, writer, (void*)"writer");pthread_join(r1, nullptr);pthread_join(r2, nullptr);pthread_join(r3, nullptr);pthread_join(r4, nullptr);pthread_join(r5, nullptr);pthread_join(r6, nullptr);pthread_join(w, nullptr);pthread_rwlock_destroy(&rw);return 0;
}
struct rwlock_t
{int readers = 0;int who;mutex_t mutex;
}

读者:

ptjread_rwlock_rdlock()
lock(mutex)
readers++
(unlock)mutex

read操作:

lock(mutex)
readers--;
unlock(mutex) 

写者:

pthread_rwlock_wrlock()
lock(mutex)
while(readers > 0) 释放锁, wait

write操作:

unlock(mutex)

Linux系统编程到此结束end…

相关文章:

Linux多线程【线程互斥】

文章目录 Linux线程互斥进程线程间的互斥相关背景概念互斥量mutex模拟抢票代码 互斥量的接口初始化互斥量销毁互斥量互斥量加锁和解锁改进模拟抢票代码&#xff08;加锁&#xff09;小结对锁封装 lockGuard.hpp 互斥量实现原理探究可重入VS线程安全概念常见的线程不安全的情况常…...

os实训课程模拟考试(大题复习)

目录 一、Linux操作系统 &#xff08;1&#xff09;第1关&#xff1a;Linux初体验 &#xff08;2&#xff09;第2关&#xff1a;Linux常用命令 &#xff08;3&#xff09;第3关&#xff1a;Linux 查询命令帮助语句 二、Linux之进程管理—&#xff08;重点&#xff09; &…...

QT/QML国际化:中英文界面切换显示(cmake方式使用)

目录 前言 实现步骤 1. 准备翻译文件 2. 翻译字符串 3.设置应用程序语言 cmake 构建方式 示例代码 总结 1. 使用 file(GLOB ...) 2. 引入其他资源文件 再次生成翻译文件 5. 手动更新和生成.qm文件 其他资源 前言 在当今全球化的软件开发环境中&#xff0c;应用程…...

设计模式在Java项目中的实际应用

设计模式在Java项目中的实际应用 大家好&#xff0c;我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01; 引言 设计模式是软件开发中重要的思想工具&#xff0c;它提供了解决特定问题…...

js制作随机四位数验证码图片

<div class"lable lable2"><div class"l"><span>*</span>验证码</div><div class"r"><input type"number" name"vercode" placeholder"请输入验证码"></div>&l…...

[开源软件] 支持链接汇总

“Common rules: 1- If the repo is on github, the support/bug link is also on the github with issues”" label; 2- Could ask questions by email list;" 3rd party software support link Note gcc https://gcc.gnu.org openssh https://bugzilla.mindrot.o…...

从零开始搭建spring boot多模块项目

一、搭建父级模块 1、打开idea,选择file–new–project 2、选择Spring Initializr,选择相关java版本,点击“Next” 3、填写父级模块信息 选择/填写group、artifact、type、language、packaging(后面需要修改)、java version(后面需要修改成和第2步中版本一致)。点击“…...

Iot解决方案开发的体系结构模式和技术

前言 Foreword 计算机技术起源于20世纪40年代&#xff0c;最初专注于数学问题的基本原理&#xff1b;到了60年代和70年代&#xff0c;它以符号系统为中心&#xff0c;该领域首先开始面临复杂性问题&#xff1b;到80年代&#xff0c;随着个人计算的兴起和人机交互的问题&#x…...

02.C1W1.Sentiment Analysis with Logistic Regression

目录 Supervised ML and Sentiment AnalysisSupervised ML (training)Sentiment analysis Vocabulary and Feature ExtractionVocabularyFeature extractionSparse representations and some of their issues Negative and Positive FrequenciesFeature extraction with freque…...

Stable Diffusion秋叶AnimateDiff与TemporalKit插件冲突解决

文章目录 Stable Diffusion秋叶AnimateDiff与TemporalKit插件冲突解决描述错误描述&#xff1a;找不到模块imageio.v3解决&#xff1a;参考地址 其他文章推荐&#xff1a;专栏 &#xff1a; 人工智能基础知识点专栏&#xff1a;大语言模型LLM Stable Diffusion秋叶AnimateDiff与…...

PCL 渐进形态过滤器实现地面分割

点云地面分割 一、代码实现二、结果示例🙋 概述 渐进形态过滤器:采用先腐蚀后膨胀的运算过程,可以有效滤除场景中的建筑物、植被、车辆、行人以及交通附属设施,保留道路路面及路缘石点云。 一、代码实现 #include <iostream> #include <pcl/io/pcd_io.h> #in…...

第十四届蓝桥杯省赛C++B组E题【接龙数列】题解(AC)

需求分析 题目要求最少删掉多少个数后&#xff0c;使得数列变为接龙数列。 相当于题目要求求出数组中的最长接龙子序列。 题目分析 对于一个数能不能放到接龙数列中&#xff0c;只关系到这个数的第一位和最后一位&#xff0c;所以我们可以先对数组进行预处理&#xff0c;将…...

Ubuntu 20.04.4 LTS 离线安装docker 与docker-compose

Ubuntu 20.04.4 LTS 离线安装docker 与docker-compose 要在Ubuntu 20.04.4 LTS上离线安装Docker和Docker Compose&#xff0c;你需要首先从有网络的环境下载Docker和Docker Compose的安装包&#xff0c;然后将它们传输到离线的服务器上进行安装。 在有网络的环境中&#xff1a…...

vue3+ts 写echarts 中国地图

需要引入二次封装的echarts和在ts文件写的option <template><div class"contentPage"><myEcharts :options"chartOptions" class"myEcharts" id"myEchartsMapId" ref"mapEcharts" /></di…...

【设计模式】【行为型模式】【责任链模式】

系列文章目录 可跳转到下面链接查看下表所有内容https://blog.csdn.net/handsomethefirst/article/details/138226266?spm1001.2014.3001.5501文章浏览阅读2次。系列文章大全https://blog.csdn.net/handsomethefirst/article/details/138226266?spm1001.2014.3001.5501 目录…...

超越所有SOTA达11%!媲美全监督方法 | UC伯克利开源UnSAM

文章链接&#xff1a;https://arxiv.org/pdf/2406.20081 github链接&#xff1a;https://github.com/frank-xwang/UnSAM SAM 代表了计算机视觉领域&#xff0c;特别是图像分割领域的重大进步。对于需要详细分析和理解复杂视觉场景(如自动驾驶、医学成像和环境监控)的应用特别有…...

享元模式(设计模式)

享元模式&#xff08;Flyweight Pattern&#xff09;是一种结构型设计模式&#xff0c;它通过共享细粒度对象来减少内存使用&#xff0c;从而提高性能。在享元模式中&#xff0c;多个对象可以共享相同的状态以减少内存消耗&#xff0c;特别适合用于大量相似对象的场景。 享元模…...

【机器学习】大模型训练的深入探讨——Fine-tuning技术阐述与Dify平台介绍

目录 引言 Fine-tuning技术的原理阐 预训练模型 迁移学习 模型初始化 模型微调 超参数调整 任务设计 数学模型公式 Dify平台介绍 Dify部署 创建AI 接入大模型api 选择知识库 个人主页链接&#xff1a;东洛的克莱斯韦克-CSDN博客 引言 Fine-tuning技术允许用户根…...

【Linux从入门到放弃】探究进程如何退出以进程等待的前因后果

&#x1f9d1;‍&#x1f4bb;作者&#xff1a; 情话0.0 &#x1f4dd;专栏&#xff1a;《Linux从入门到放弃》 &#x1f466;个人简介&#xff1a;一名双非编程菜鸟&#xff0c;在这里分享自己的编程学习笔记&#xff0c;欢迎大家的指正与点赞&#xff0c;谢谢&#xff01; 进…...

QT5 static_cast实现显示类型转换

QT5 static_cast实现显示类型转换&#xff0c;解决信号重载情况...

【ES】--Elasticsearch的翻页详解

目录 一、前言二、from+size浅分页1、from+size导致深度分页问题三、scroll深分页1、scroll原理2、scroll可以返回总计数量四、search_after深分页1、search_after避免深度分页问题一、前言 ES的分页常见的主要有三种方式:from+size浅分页、scroll深分页、search_after分页。…...

3.js - 纹理的重复、偏移、修改中心点、旋转

你瞅啥 上字母 // ts-nocheck // 引入three.js import * as THREE from three // 导入轨道控制器 import { OrbitControls } from three/examples/jsm/controls/OrbitControls // 导入lil.gui import { GUI } from three/examples/jsm/libs/lil-gui.module.min.js // 导入twee…...

RS232隔离器的使用

RS232隔离器在通信系统中扮演着至关重要的角色&#xff0c;其主要作用可以归纳如下&#xff1a; 一、保护通信设备 电气隔离&#xff1a;RS232隔离器通过光电隔离技术&#xff0c;将RS-232接口两端的设备电气完全隔离&#xff0c;从而避免了地线回路电压、浪涌、感应雷击、静电…...

一切为了安全丨2024中国应急(消防)品牌巡展武汉站成功召开!

消防品牌巡展武汉站 6月28日&#xff0c;由中国安全产业协会指导&#xff0c;中国安全产业协会应急创新分会、应急救援产业网联合主办&#xff0c;湖北消防协会协办的“一切为了安全”2024年中国应急(消防)品牌巡展-武汉站成功举办。该巡展旨在展示中国应急&#xff08;消防&am…...

【面试系列】PHP 高频面试题

欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;欢迎订阅相关专栏&#xff1a; ⭐️ 全网最全IT互联网公司面试宝典&#xff1a;收集整理全网各大IT互联网公司技术、项目、HR面试真题. ⭐️ AIGC时代的创新与未来&#xff1a;详细讲解AIGC的概念、核心技术、…...

JAVA极简图书管理系统,初识springboot后端项目

前提条件&#xff1a; 具备基础的springboot 知识 Java基础 废话不多说&#xff01; 创建项目 配置所需环境 将application.properties>application.yml 配置以下环境 数据库连接MySQL 自己创建的数据库名称为book_test server:port: 8080 spring:datasource:url:…...

MySQL 重新初始化实例

1、关闭mysql服务 service mysqld stop 2、清理datadir(本例中指定的是/var/lib/mysql)指定的目录下的文件&#xff0c;将该目录下的所有文件删除或移动至其他位置 cd /var/lib/mysql mv * /opt/mysql_back/ 3、初始化实例 /usr/local/mysql/bin/mysqld --initialize --u…...

VCS编译bug汇总

‘typedef’ is not expected to be used in this contex 注册前少了分号。 Scope resolution error resolution : 声明指针时 不能与类名同名&#xff0c;即 不能声明为adapter. cannot find member "type_id" 忘记注册了 拼接运算符使用 关键要加上1b&#xff0…...

【2024LLM应用-数据预处理】之如何从PDF,PPT等非结构化数据提取有效信息(结构化数据JSON)?

&#x1f970;大家知道吗,之前在给AI大模型"喂数据"的时候,我们往往需要把非结构化数据(比如PDF、PPT、Excel等)自己手动转成结构化的格式,这可真是太累人儿了。&#x1f975; 幸好现在有了Unstructured这个神级库,它内置的数据提取函数可以帮我们快速高效地完成这个…...

冯雷老师:618大退货事件分析

近日冯雷老师受邀为某头部电商36名高管进行培训&#xff0c;其中聊到了今年618退货潮的问题。以下内容整理自冯雷老师的部分授课内容。 一、引言 随着电子商务的蓬勃发展&#xff0c;每年的618大促已成为消费者和商家共同关注的焦点。然而&#xff0c;在销售额不断攀升的同时…...

JAVA基础教程DAY0-基础知识

JAVA语言的特点 简单性、面向对象、安全性、跨平台性、支持多线程、分布性 面向对象编程&#xff08;Object-Oriented Programming&#xff0c;简称OOP&#xff09;是一种编程范式&#xff0c;它通过将数据和操作这些数据的方法封装在一起&#xff0c;以创建对象的形式来组织代…...

鸿蒙开发Ability Kit(程序访问控制):【安全控件概述】

安全控件概述 安全控件是系统提供的一组系统实现的ArkUI组件&#xff0c;应用集成这类组件就可以实现在用户点击后自动授权&#xff0c;而无需弹窗授权。它们可以作为一种“特殊的按钮”融入应用页面&#xff0c;实现用户点击即许可的设计思路。 相较于动态申请权限的方式&am…...

【信息系统项目管理师】18年~23年案例概念型知识

文章目录 18上18下19上19下20上20下21上21下22年上22年下23年上 18上 请简述 ISO 9000 质量管理的原则 领导作用、 过程方法、 管理的系统方法、 与供方互利的关系、 基于事实的决策方法、 持续改进、 全员参与、 以顾客为关注焦点 概念 国家标准(GB/T 1 9000 2008)对质量的定…...

什么是字符串常量池?如何利用它来节省内存?

字符串常量池是Java中一个非常重要的概念&#xff0c;尤其对于理解内存管理和性能优化至关重要。想象一下&#xff0c;你正在管理一家大型图书馆&#xff0c;每天都有无数读者来借阅书籍。 如果每本书每次借阅都需要重新印刷一本&#xff0c;那么图书馆很快就会陷入混乱&#…...

Selenium自动化测试20条常见异常+处理方案

常见的Selenium异常 以下是所有Selenium WebDriver代码中可能发生的一些常见Selenium异常。 1、ElementClickInterceptedException 由于以某种方式隐藏了接收到click命令的元素&#xff0c;因此无法正确执行Element Click命令。 2、ElementNotInteractableException 即使目…...

verilog将信号和常数拼接起来

正确的拼接 1 s_axis_data_tdata {32b0000_0000_0000_0000_0000_0000_0000_0000,32b0011_1111_1000_0000_0000_0000_0000_0000}; 2 注意&#xff0c;信号的两部分都要用{}花括号括起来 s_axis_data_tdata {{32{1b1}},{32b0100_0000_0000_0000_0000_0000_0000_0000}}; 3…...

OpenSSH远程代码执行漏洞 (CVE-2024-6387)

1. 前言 OpenSSH是一套基于安全外壳&#xff08;SSH&#xff09;协议的安全网络实用程序&#xff0c;它提供强大的加密功能以确保隐私和安全的文件传输&#xff0c;使其成为远程服务器管理和安全数据通信的必备工具。 OpenSSH 自 1995 年问世近 20 年来&#xff0c;首次出现了…...

高薪程序员必修课-java并发编程的bug源头

前言 Java并发编程虽然强大&#xff0c;但也容易引发复杂的bug。并发编程的bug主要源自以下几个方面&#xff1a;竞态条件、死锁、内存可见性问题和线程饥饿。了解这些bug的源头及其原理&#xff0c;可以帮助开发者避免和解决这些问题。以下是详细的讲解和相应的示例。 1. 竞态…...

c++:#include 某文件.h底层如何寻找其.cpp实现

在C中&#xff0c;当你编写了一个头文件&#xff08;如MyLibrary.h&#xff09;和对应的实现文件&#xff08;如MyLibrary.cpp&#xff09;时&#xff0c;其他源文件&#xff08;如main.cpp&#xff09;只需要包含头文件&#xff08;#include "MyLibrary.h"&#xff…...

uniapp中如何进行微信小程序的分包

思路&#xff1a;在uniapp中对微信小程序进行分包&#xff0c;和原生微信小程序进行分包的操作基本上没区别&#xff0c;主要就是在pages.json中进行配置。 如图&#xff0c;我新增了一个包diver-page 此时需要在pages.json中的subPackages数组中新增一项 root代表这个包的根…...

win10下安装PLSQL14连接Oracle数据库

问题背景 在使用Oracle开发过程中&#xff0c;经常会使用工具来连接数据库&#xff0c;方便查询、处理数据。其中有很多工具可以使用&#xff0c;比如dbeaver、plsql等。本文主要介绍在win10环境下&#xff0c;plsql14的安装步骤以及安装过程中遇到的一些问题。 安装步骤及问题…...

高考失利咨询复读,银河补习班客服开挂回复

补习班的客服在高考成绩出来后&#xff0c;需要用专业的知识和足够的耐心来回复各种咨询&#xff0c;聊天宝快捷回复软件&#xff0c;帮助客服开挂回复。 ​ 前言 高考成绩出来&#xff0c;几家欢喜几家愁&#xff0c;对于高考失利的学生和家长&#xff0c;找一个靠谱的复读补…...

java 代码块

Java中的代码块主要有三种类型&#xff1a;普通代码块、静态代码块、构造代码块。它们的用途和执行时机各不相同。 普通代码块&#xff1a;在方法内部定义&#xff0c;使用一对大括号{}包围的代码片段。它的作用域限定在大括号内&#xff0c;每当程序执行到该代码块时就会执行其…...

vue中避免多次请求字典接口

vuex缓存所有字典项 背景vuex管理所有字典项调用字典接口处理字典项数据的filter页面中使用字典 背景 每次用到字典都需要通过对应的字典type调用一次字典接口&#xff0c;当一个页面用到字典项很多时&#xff0c;接口请求炒鸡多&#xff0c;会导致接口响应超时。 本篇文章改为…...

Snappy使用

Snappy使用 Snappy是谷歌开源的压缩和解压的开发包&#xff0c;目标在于实现高速的压缩而不是最大的压缩 项目地址&#xff1a;GitHub - google/snappy&#xff1a;快速压缩器/解压缩器 Cmake版本升级 该项目需要比较新的cmake&#xff0c;CMake 3.16.3 or higher is requi…...

跨越重洋:在Heroku上配置Pip镜像源的终极指南

&#x1f310; 跨越重洋&#xff1a;在Heroku上配置Pip镜像源的终极指南 Heroku是一个支持多种编程语言的云平台即服务&#xff08;PaaS&#xff09;&#xff0c;它允许开发者部署和管理应用程序。然而&#xff0c;由于Heroku的服务器位于海外&#xff0c;直接使用Python的包管…...

SpringBoot + 虚拟线程,性能炸裂!

一、什么是虚拟线程 虚拟线程是Java19开始增加的一个特性&#xff0c;和Golang的携程类似&#xff0c;一个其它语言早就提供的、且如此实用且好用的功能&#xff0c;作为一个Java开发者&#xff0c;早就已经望眼欲穿了。 二、虚拟线程和普通线程的区别 “虚拟”线程&#xf…...

Java Character类

Character是char的包装类 转义序列 Character类的方法...

Python中的爬虫实战:猫眼电影爬虫

随着互联网技术的快速发展&#xff0c;网络上的信息量越来越庞大。猫眼电影作为国内领先的电影数据平台&#xff0c;为用户提供了全面的电影信息服务。本文将介绍如何利用python编写简单的猫眼电影爬虫&#xff0c;获取电影相关数据。 爬虫概述 爬虫&#xff0c;即网络爬虫&a…...

WAIC2024 | 华院计算邀您共赴2024年世界人工智能大会,见证未来科技革新

在智能时代的浪潮汹涌澎湃之际&#xff0c;算法已成为推动社会进步的核心力量。作为中国认知智能技术的领军企业&#xff0c;华院计算在人工智能的广阔天地中&#xff0c;不断探索、创新&#xff0c;致力于将算法的潜力发挥到极致。在过去的时日里&#xff0c;华院计算不断探索…...