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

Linux从0到1——线程同步和互斥【互斥量/条件变量/信号量/PC模型】

Linux从0到1——线程同步和互斥

  • 1. Linux线程互斥
    • 1.1 问题引入
    • 1.2 互斥相关概念
    • 1.3 多执行流并发访问公共资源的数据不一致问题
    • 1.4 互斥量(锁)
    • 1.5 改进抢票系统
    • 1.6 锁的简单封装
    • 1.7 锁的实现原理
    • 1.8 可重入VS线程安全
    • 1.9 死锁
  • 2. Linux线程同步
    • 2.1 理解同步
    • 2.2 条件变量
    • 2.3 认识接口
    • 2.4 改进抢票系统
  • 3. POSIX信号量
    • 3.1 理解信号量运用场景
    • 3.2 快速认识接口
  • 4. 生产者消费者模型
    • 4.1 理解
    • 4.2 模型实现
      • 4.2.1 基于阻塞队列——单生产单消费
      • 4.2.2 给生产消费模型传任务,模拟实际应用场景
      • 4.2.3 基于阻塞队列,改造多生产多消费
      • 4.2.4 基于环形队列——单生产单消费
      • 4.2.5 基于环形队列,改造多生产多消费


1. Linux线程互斥


本章内容会直接使用到之前章节自定义封装的线程库,链接在此:https://blog.csdn.net/weixin_73870552/article/details/144543376?spm=1001.2014.3001.5501,如果学过C++11线程库,则可以跳过这部分。


1.1 问题引入


实现一个抢票逻辑,观察现象

#include <iostream>
#include <unistd.h>
#include <string>
#include "Thread.hpp"int ticket = 10000;     // 一共1万张票std::string GetThreadName()
{static int number = 1;  // 生命周期随进程,且只在当前作用域有效char name[64];snprintf(name, sizeof(name), "Thread-%d", number++);return name;
}void GetTicket(std::string name)
{while(true){if(ticket > 0){usleep(1000);   // 充当抢票时间printf("%s get a ticket: %d\n", name.c_str(), ticket);ticket--;}else{// ticket == 0了就不抢了break;}// 实际情况,还有后续的动作}
}int main()
{std::string name1 = GetThreadName();Thread<std::string> t1(GetTicket, name1, name1);std::string name2 = GetThreadName();Thread<std::string> t2(GetTicket, name2, name2);std::string name3 = GetThreadName();Thread<std::string> t3(GetTicket, name3, name3);std::string name4 = GetThreadName();Thread<std::string> t4(GetTicket, name4, name4);t1.Start();t2.Start();t3.Start();t4.Start();t1.Join();t2.Join();t3.Join();t4.Join();return 0;
}

在这里插入图片描述

正常来说,ticket抢到0就应该停止抢票了,可是现在票被抢到了负数,这肯定是出问题了。


1.2 互斥相关概念


1. 临界资源:多线程执行流共享的资源就叫做临界资源

  • 上例中,临界资源就是全局变量ticket

2. 临界区:每个线程内部,访问临界资源的代码,就叫做临界区

在这里插入图片描述

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

4. 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成


1.3 多执行流并发访问公共资源的数据不一致问题


1. 理解多执行流并发访问公共资源的数据不一致问题

int cnt = 0;
cnt++;	// 这段代码是原子性的吗?		
  • 实际上cnt++这句代码,会转化成三句汇编代码,1)将0从内存中换入寄存器,2)对寄存器中0数据++,3)将++后的数据1换回内存。所以cnt++这句代码不是原子的,它可能在3步中的任何一步处被打断。
  • 假设现在有两个线程在访问cnt,执行cnt++的动作,第一个线程在执行完步骤1)后,时间片到了,从CPU上被剥离下来,保存自己的硬件上下文数据。第二个线程此时被处理机调度,上CPU执行,一直将cnt加到了100,时间片才到,被从CPU上剥离,此时内存中,cnt的值已经变成了100。
  • 这时线程一又被调度了,带着自己之前的硬件上下文,将寄存器中cnt的值覆盖成0。线程一之前已经执行过步骤1)了,接下来会直接执行步骤2),将cnt++,值变为1。此时问题来了,线程一将执行步骤3),将1这个值写入内存,cnt的值由100变为了1。线程二之前做的努力全部白费了。

2. 回顾1.1

  • 在1.1例子中的if语句处,并发问题已经出现了:

在这里插入图片描述

  • ticket==1时,四个线程可能同时执行if判断操作,也就是说,在ticket--还没来得及执行的时候,四个线程就都进入临界区了,if条件均成立。这时ticket就可能被--多次,直接干成负数。

1.4 互斥量(锁)


1. 初始化锁

  • 静态分配,一般用于创建全局锁:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
  • 动态分配,用于创建局部锁:
    • mutex:要初始化的锁;
    • attr:传nullptr即可。
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); 

2. 销毁锁

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

3. 加锁和解锁

  • 返回值:成功返回0,失败返回错误码。
int pthread_mutex_lock(pthread_mutex_t *mutex); 
int pthread_mutex_unlock(pthread_mutex_t *mutex); 
  • 调用 pthread_mutex_lock 时,可能会遇到以下情况:
    • 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功;
    • 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。

1.5 改进抢票系统


1. 全局锁保护临界区

#include <iostream>
#include <unistd.h>
#include <string>
#include "Thread.hpp"int ticket = 10000;     // 一共1万张票(全局共享资源)
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;  // 定义锁std::string GetThreadName()
{static int number = 1;  // 生命周期随进程,且只在当前作用域有效char name[64];snprintf(name, sizeof(name), "Thread-%d", number++);return name;
}// 加锁:
//  1. 我们要尽可能的给少的代码加锁
//  2. 一般加锁,都是给临界区加锁
void GetTicket(std::string name)
{while(true){pthread_mutex_lock(&mutex);    // 申请锁本身是安全的,原子的if(ticket > 0)      // 一个线程在临界区中访问临界资源的时候,可能发生切换{usleep(1000);   // 充当抢票时间printf("%s get a ticket: %d\n", name.c_str(), ticket);ticket--;pthread_mutex_unlock(&mutex);}else{// ticket == 0了就不抢了pthread_mutex_unlock(&mutex);break;}// 实际情况,还有后续的动作}
}int main()
{std::string name1 = GetThreadName();Thread<std::string> t1(GetTicket, name1, name1);std::string name2 = GetThreadName();Thread<std::string> t2(GetTicket, name2, name2);std::string name3 = GetThreadName();Thread<std::string> t3(GetTicket, name3, name3);std::string name4 = GetThreadName();Thread<std::string> t4(GetTicket, name4, name4);t1.Start();t2.Start();t3.Start();t4.Start();t1.Join();t2.Join();t3.Join();t4.Join();return 0;
}

在这里插入图片描述

  • 可以发现,运行的时间更长了,但是抢票的逻辑正确了。

在一些OS中,可能会出现票全被一个线程抢完的现象,这样其他两个进程就迟迟不能上处理机调度,出现线程饥饿问题。出现这种问题,单纯的互斥是解决不了的,还需要引入同步机制,让各线程的执行具有一定的顺序性。

2. 局部锁保护临界区

  • 将局部锁作为函数参数传递给GetTicket
#include <iostream>
#include <unistd.h>
#include <string>
#include "Thread.hpp"int ticket = 10000;     // 一共1万张票(全局共享资源)std::string GetThreadName()
{static int number = 1;  // 生命周期随进程,且只在当前作用域有效char name[64];snprintf(name, sizeof(name), "Thread-%d", number++);return name;
}// 加锁:
//  1. 我们要尽可能的给少的代码加锁
//  2. 一般加锁,都是给临界区加锁
void GetTicket(pthread_mutex_t *mutex)
{while(true){pthread_mutex_lock(mutex);    // 申请锁本身是安全的,原子的if(ticket > 0)      // 一个线程在临界区中访问临界资源的时候,可能发生切换{usleep(1000);   // 充当抢票时间printf("get a ticket: %d\n", ticket);ticket--;pthread_mutex_unlock(mutex);}else{// ticket == 0了就不抢了pthread_mutex_unlock(mutex);break;}// 实际情况,还有后续的动作}
}int main()
{pthread_mutex_t mutex;pthread_mutex_init(&mutex, nullptr);std::string name1 = GetThreadName();Thread<pthread_mutex_t *> t1(GetTicket, name1, &mutex);std::string name2 = GetThreadName();Thread<pthread_mutex_t *> t2(GetTicket, name2, &mutex);std::string name3 = GetThreadName();Thread<pthread_mutex_t *> t3(GetTicket, name3, &mutex);std::string name4 = GetThreadName();Thread<pthread_mutex_t *> t4(GetTicket, name4, &mutex);t1.Start();t2.Start();t3.Start();t4.Start();t1.Join();t2.Join();t3.Join();t4.Join();pthread_mutex_destroy(&mutex);return 0;
}

1.6 锁的简单封装


1. LockGurad.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;
};

2. 改进抢票代码

  • 这里使用的是局部锁,记得要destroy
#include <iostream>
#include <unistd.h>
#include <string>
#include "Thread.hpp"
#include "LockGuard.hpp"...void GetTicket(pthread_mutex_t *mutex)
{while (true){// 非临界区代码{// 临界区LockGuard Lockguard(mutex);if (ticket > 0) {usleep(1000); // 充当抢票时间printf("get a ticket: %d\n", ticket);ticket--;}else{break;}}// 实际情况,还有后续的动作}
}...

1.7 锁的实现原理


1. 上伪代码

  • 为了实现互斥锁操作,大多数体系结构都提供了swapexchange汇编指令,该指令的作用是把寄存器和内存单元的数据相交换。

在这里插入图片描述

2. 理解lock

  • 大家可以将mutex简单理解成一个结构体:
struct XXX
{int mutex = 1;
}
  • movb $0, %al,表示将数据0,放入寄存器%al中。xchgb %al, mutex,表示将寄存器%al中的数据和内存中mutex的数据做交换。
  • 假设现在线程1申请锁成功了,也就是成功执行了movb $0, %al,和xchgb %al, mutex操作。此时内存中mutex的值已经从1被换成0了,数据1就被换入了线程1的硬件上下文数据中,if条件成立,return 0
  • 这时如果线程2再来申请锁,执行xchgb %al, mutex操作后,数据0被换入线程2的%al寄存器,执行if判断后,线程2被挂起等待。

3. 理解unlock

  • movb $1, mutex,其实就是将数据1,写入内存mutex。这样其他线程再申请锁时,就可以拿到1,进而满足if条件,成功申请锁了。

1.8 可重入VS线程安全


1. 概念

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

2. 常见的线程不安全的情况

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

3. 常见的线程安全的情况

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

4. 常见不可重入的情况

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

5. 常见可重入的情况

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

6. 可重入与线程安全联系

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

7. 可重入与线程安全区别

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

1.9 死锁


1. 概念

  • 死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
  • 例子:
    • A和B互相申请对方的锁,并且都不释放自己已经有的锁,就会陷入无止境的循环等待。

在这里插入图片描述

2. 死锁的四个必要条件

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

3. 避免死锁

  • 破坏死锁的四个必要条件。
  • 加锁顺序一致。比如上面的例子中,线程A和线程B,都先申请锁1,再申请锁2。
  • 避免锁未释放的场景。
  • 资源一次性分配,避免对分配的资源频繁上锁解锁。

2. Linux线程同步


2.1 理解同步


在这里插入图片描述

假设现在有一个场景:一群学生在轮流使用一个自习室,这个自习室每次只能容纳下一个人,且只有一把钥匙,只有拿到钥匙的人才能进入。

现在有一个学生已经进去自习了,其他学生都在排队等待这个自习室。这个学生学习完了,准备走了,刚出自习室大门,突然又不想走了,又回去了。就这样来来回回,这名同学一直进进出出,其他同学一直没办法进去学习,而且这个同学也不学习,大家都不愿意了。

这时管理层出了一个规定,一旦你出了自习室的门,就必须把钥匙交出去,给后面排队的学生。你要是还想自习,就必须去队伍末尾重新排队。就这样,大家的自习行为有了一定的顺序性。

钥匙就是我们之前说的“锁”,而学生就是一个一个的“执行流”,让这些执行流的执行,具有一定的顺序性,就是“同步”。


2.2 条件变量


1. 故事时刻

在这里插入图片描述

  • 一个人向盘子中放苹果,一个向盘子中拿苹果。其中这个拿苹果的人,是个瞎子,他看不到,每次想拿苹果时,只能去盘子中摸。如果盘子中有苹果,就可以摸到苹果,将苹果拿走。但是如果盘子中没有苹果,就只能无功而返。
  • 瞎子在拿苹果的时候,会把盘子拿走,放苹果的人就放不进去。如果这个瞎子每隔几秒钟,就去盘子里摸一摸有没有苹果,这就会导致放苹果的人即使有苹果也放不进去。
  • 这还只是一个瞎子的情况,如果有多个瞎子呢?每个瞎子都抢着去摸苹果,放苹果的人就更放不进去了。

在这里插入图片描述

  • 为了避免这种情况的发生,聪明的人想到了利用一个铃铛。当放苹果的人,将苹果放入盘子中后,会敲响这个铃铛。瞎子在听到铃铛响后,才会去盘子中摸苹果,铃铛不响,瞎子就不会去摸。这就很好的解决了瞎子抢盘子的问题。
  • 在上面这个例子中,瞎子和放苹果的人都是执行流,盘子就是一个临界资源,苹果是数据,铃铛就充当条件变量的角色。

不难看出,条件变量很好的解决了互斥情况下,可能存在的饥饿问题,提高了互斥的效率。

2. 理解条件变量

struct cond
{// 条件是否成立int flag;// 维护一个线程等待队列tcb_queue;
}
  • flag用来标记条件是否成立;tcb_queuecond内部维护的一个等待队列。
  • 当条件不满足时,线程加入等待队列;当条件满足时,线程从等待队列中被激活。

2.3 认识接口


1. 初始化条件变量

  • 静态分配,创建全局条件变量:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
  • 动态分配,创建局部条件变量:
    • cond:要初始化的条件变量;
    • attrNULL
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr); 

2. 销毁

  • 使用 PTHREAD_COND_INITIALIZER 初始化的条件变量不需要销毁。
int pthread_cond_destroy(pthread_cond_t *cond);

3. 等待条件满足

  • cond:要在这个条件变量上等待;
  • mutex:互斥量,后面详细解释。
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex); 

4. 唤醒等待

int pthread_cond_broadcast(pthread_cond_t *cond);	// 唤醒等待队列中的所有线程 
int pthread_cond_signal(pthread_cond_t *cond);		// 唤醒等待队列头部的一个线程 

2.4 改进抢票系统


1. 提出问题

  • 一共有1000张票,票抢完之后,线程不break,而是打印一句“没票了”。相当于线程轮询检测tickets,有票了就抢,没有就打印信息,以便之后tickets又有票了,可以第一时间抢到。
#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;int tickets = 1000;  // 1000张票void *threadRoutine(void *args)
{std::string name = static_cast<const char*>(args);while(true){pthread_mutex_lock(&mutex);if(tickets > 0){std::cout << name << ", get a ticket: " << tickets-- << std::endl;usleep(1000);   // 模拟抢票时间}else{std::cout << "没有票了, " << name << std::endl;}pthread_mutex_unlock(&mutex);}
}// 主线程
int main()
{pthread_t t1, t2, t3;pthread_create(&t1, nullptr, threadRoutine, (void*)"thread-1");pthread_create(&t2, nullptr, threadRoutine, (void*)"thread-2");pthread_create(&t3, nullptr, threadRoutine, (void*)"thread-3");pthread_join(t1, nullptr);pthread_join(t2, nullptr);pthread_join(t3, nullptr);return 0;
}
  • 发现一个现象,tickets减到0后,这三个线程还不断去访问tickets,且频率非常之高。如果我们还有一个线程用来隔一段时间更新一下tickets,放一些票出来,访问tickets时,怎么可能抢的过这三个线程?这就是对系统资源极大的浪费,没有票就别老是去访问tickets了,老老实实等着不好吗。

在这里插入图片描述

这不就是之前提到的,瞎子抢盘子,导致放苹果的人放不进去苹果的情况吗?

2. 使用条件变量,升级抢票系统

#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;int tickets = 1000;  // 1000张票void *threadRoutine(void *args)
{std::string name = static_cast<const char*>(args);while(true){pthread_mutex_lock(&mutex);if(tickets > 0){std::cout << name << ", get a ticket: " << tickets-- << std::endl;usleep(1000);   // 模拟抢票时间}else{std::cout << "没有票了, " << name << std::endl;pthread_cond_wait(&cond, &mutex);	// 等待条件变量满足}pthread_mutex_unlock(&mutex);}
}// 主线程
int main()
{pthread_t t1, t2, t3;pthread_create(&t1, nullptr, threadRoutine, (void*)"thread-1");pthread_create(&t2, nullptr, threadRoutine, (void*)"thread-2");pthread_create(&t3, nullptr, threadRoutine, (void*)"thread-3");while(true){// 每隔5s,放1000支票sleep(5);pthread_mutex_lock(&mutex);tickets += 1000;pthread_mutex_unlock(&mutex);pthread_cond_broadcast(&cond);  // 唤醒全部线程}pthread_join(t1, nullptr);pthread_join(t2, nullptr);pthread_join(t3, nullptr);return 0;
}

在这里插入图片描述

3. 关于pthread_cond_wait的一些细节

  • 线程在等待时,会自动释放锁;
  • 线程被唤醒时,是在临界区内唤醒的,线程在pthread_cond_wait返回时,要重新申请并持有锁;
  • 当线程被唤醒时,重新申请并持有锁本质也是要参与锁的竞争的。

4. 总结

  • 单纯的互斥,可以保证数据的安全,但是不一定合理或者高效!

3. POSIX信号量


3.1 理解信号量运用场景


在之前的学习中,我们其实接触过信号量:进程间通信。这里带着大家把之前学过的知识,和今天要讲的知识,平滑的连接一下。

1. 问题引入

  • 基于BlockQueue实现的PC模型,整个阻塞队列都是临界资源,PushPop的时候整个队列都是互斥的,任何执行流都进不来。聪明的你可能发现了,但是队头插入和队尾pop,理论上好像可以并发执行啊,怎么上面的模型做不到呢?所以说这个模型不是很高效。

2. 信号量运用场景

  • 就好比一个大自习室中,每进来一个同学或者出去一个同学,都要把这个大的自习室控制起来,保证只有这一个同学能进来或者出去。这不是扯淡吗?凭什么他进来的时候我不能出去。
  • 为了解决这个问题,我们设计了预定机制。自习室中只有100个座位,100个座位预定满了,就不让人进了,但是此时里面的人还是可以出去的。出去一个人,就要放出一个预定名额。
  • 将一个大的临界区资源,拆分成小的临界区资源管理起来,这就是信号量能做的事。

3.2 快速认识接口


信号量有自己的头文件<semaphore.h>

1. 初始化信号量

int sem_init(sem_t *sem, int pshared, unsigned int value); 
  • pshared:0表示线程间共享,非零表示进程间共享;
  • value:信号量初始值。

2. 销毁信号量

int sem_destroy(sem_t *sem); 

3. 等待信号量

  • 等待信号量,会将信号量的值减1(就是我们常说的P操作)。
int sem_wait(sem_t *sem); //P()

4. 发布信号量

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

4. 生产者消费者模型


4.1 理解


1. 故事时刻

在这里插入图片描述

  • 有一个超市,超市里有一个展架,供货商作为生产者,向展架上放商品。顾客作为消费者,从展架上拿商品。
  • 展架在同一时间,只能被一个人使用。供货商和供货商不能同时使用一个展架,顾客也不能同时跑到一个展架上去拿东西。当展架上已经被放满货物时,供货商就不能再往展架上放东西了。当展架上没有东西时,顾客就不能去展架上拿东西了。

2. 思考一下这几个角色之间的关系

  • 生产者和生产者之间,是竞争关系——互斥;
  • 消费者和消费者之间,是竞争关系——互斥;
  • 生产者和消费者之间,是同步加互斥关系。

3. 总结:321原则

  • 3种关系;
  • 2种角色(消费者和生产者可以有多个);
  • 1个交易场所(内存空间)。

4.2 模型实现


4.2.1 基于阻塞队列——单生产单消费


1. 阻塞队列的实现(blockQueue.hpp)

  • 思路:
    • 生产者向队列中放数据,消费者从队列中拿数据,队列就相当于一个公共资源,需要被保护起来,满足互斥。
    • 同时还需要维护生产者和消费者之间的同步关系。
#pragma once#include <iostream>
#include <pthread.h>
#include <queue>const int defaultcap = 5;  // 默认容量template<class T>
class BlockQueue
{
public:BlockQueue(int cap = defaultcap):_capacity(cap){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_p_cond, nullptr);pthread_cond_init(&_c_cond, nullptr);}bool IsFull(){return _q.size() == _capacity;}bool IsEmpty(){return _q.size() == 0;}// 生产bool Push(const T &in){pthread_mutex_lock(&_mutex);// 队列满了while(IsFull())		// 不能用 if(IsFull()){// 生产者阻塞等待pthread_cond_wait(&_p_cond, &_mutex);}// 队列不满_q.push(in);// 生产一个数据后,唤醒消费者pthread_cond_signal(&_c_cond);pthread_mutex_unlock(&_mutex);}// 消费bool Pop(T *out){pthread_mutex_lock(&_mutex);// 队列为空while(IsEmpty()){// 阻塞等待pthread_cond_wait(&_c_cond, &_mutex);}// 队列不为空*out = _q.front();_q.pop();// 消费者消费后,唤醒生产者pthread_cond_signal(&_p_cond);pthread_mutex_unlock(&_mutex);}~BlockQueue(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_p_cond);pthread_cond_destroy(&_c_cond);}private:std::queue<T> _q;int _capacity;      // 容量pthread_mutex_t _mutex;pthread_cond_t _p_cond;     // 生产者条件变量pthread_cond_t _c_cond;     // 消费者条件变量
};
  • 细节:
    • Push方法来说, pthread_cond_signal(&_c_cond);,应该放在pthread_mutex_unlock(&_mutex);的之前,还是之后呢?原则上来说都可以,都不算错,但是习惯将 pthread_cond_signal(&_c_cond);放在之前。
    • 判断队列是否为空为满时,不能使用if,因为pthread_cond_wait存在伪唤醒的情况:今天我们是一次唤醒一个线程,如果一次唤醒多个就会出问题。拿生产动作来说,如果当前队列已满,且使用if(IsFull())判断队列是否为满,肯定会先判断成立,然后让一批线程阻塞在这里。此时如果消费者消费了一个数据,就将唤醒这阻塞的一批线程。这些线程都从pthread_cond_wait处开始向后执行,向队列中插入多个数据,出现问题。为了避免这种情况,我们使用while来判断队列是否为空为满,这样一来,只有在队列真正为空为满时,才会跳出while循环,完美解决伪唤醒问题。

2. 测试代码

  • 让消费者每隔一秒消费一次:
#include "BlockQueue.hpp"
#include <pthread.h>
#include <unistd.h>
#include <ctime>void *consumer(void *args)
{BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args); // 生产者和消费者同时看到了一份公共资源while(true){sleep(1);	// 隔一秒消费一次// 1. 消费数据int data = 0;bq->Pop(&data);// 2. 处理std::cout << "consumer data: " << data << std::endl;}return nullptr;
}void *productor(void *args)
{BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args); // 生产者和消费者同时看到了一份公共资源while(true){// 1. 准备数据int data = rand() % 10 + 1;     // [1, 10] // 2. 进行生产bq->Push(data);std::cout << "productor data: " << data << std::endl;}return nullptr;
}int main()
{// 种下随机数种子srand((uint16_t)time(nullptr) ^ getpid() ^ pthread_self()); BlockQueue<int> *bq = new BlockQueue<int>();pthread_t c, p;     // 消费者和生产者pthread_create(&c, nullptr, consumer, bq);pthread_create(&p, nullptr, productor, bq);pthread_join(c, nullptr);pthread_join(p, nullptr);return 0;
}
  • 事实上,生产者和消费者这两个线程,谁先跑我们是不知道的。但是我们让消费者在运行的时候先等了一秒钟,如果同步逻辑正确,就可以看到如下现象:生产者瞬间将队列生产满,然后等待消费者消费。

在这里插入图片描述

  • 大家也可以让生产者先跑,自行观察同步现象。

4.2.2 给生产消费模型传任务,模拟实际应用场景


1. 任务模块(Task.hpp)

  • 实现一个给线程传计算加减乘除任务,线程跑任务的场景。
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>const int defaultvalue = 0;enum
{ok = 0,div_zero, // 除0错误mod_zero, // 模0错误unknow    // 未知错误
};const std::string opers = "+-*/%)(&";class Task
{
public:Task(){}Task(int x, int y, char op): data_x(x), data_y(y), oper(op), result(defaultvalue), code(ok){}void Run(){switch (oper){case '+':result = data_x + data_y;break;case '-':result = data_x - data_y;break;case '*':result = data_x * data_y;break;case '/':{if (data_y == 0)code = div_zero;elseresult = data_x / data_y;}break;case '%':{if (data_y == 0)code = mod_zero;elseresult = data_x % data_y;}break;default:code = unknow;break;}}std::string PrintTask(){std::string s;s = std::to_string(data_x);s += oper;s += std::to_string(data_y);s += "=?";return s;}std::string PrintResult(){std::string s;s = std::to_string(data_x);s += oper;s += std::to_string(data_y);s += "=";s += std::to_string(result);s += " [";s += std::to_string(code);s += "]";return s;}~Task(){}private:int data_x;int data_y;char oper; // + - * / %int result;int code; // 结果码,0: 结果可信 !0: 结果不可信,1,2,3,4
};

2. 测试程序——单生产单消费

#include "BlockQueue.hpp"
#include "Task.hpp"
#include <pthread.h>
#include <unistd.h>
#include <ctime>void *consumer(void *args)
{BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(args); // 生产者和消费者同时看到了一份公共资源while(true){sleep(1);Task t;bq->Pop(&t);t.Run();std::cout << "consumer data: " << t.PrintResult() << std::endl;}return nullptr;
}void *productor(void *args)
{BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(args);while(true){// 1. 准备数据int data1 = rand() % 10;usleep(rand() % 123);int data2 = rand() % 10;usleep(rand() % 123);char oper = opers[rand() % opers.size()];Task t(data1, data2, oper);// 2. 进行生产bq->Push(t);// for debugstd::cout << "productor data: " << t.PrintTask() << std::endl;}return nullptr;
}int main()
{// 种下随机数种子srand((uint16_t)time(nullptr) ^ getpid() ^ pthread_self()); BlockQueue<Task> *bq = new BlockQueue<Task>();pthread_t c, p;     // 消费者和生产者pthread_create(&c, nullptr, consumer, bq);pthread_create(&p, nullptr, productor, bq);pthread_join(c, nullptr);pthread_join(p, nullptr);return 0;
}

3. 测试结果

在这里插入图片描述


4.2.3 基于阻塞队列,改造多生产多消费


阻塞队列的实现模块是不用改的,复用单生产单消费的代码即可。下面我们就拿多生产多消费的测试代码测试一下:

#include "BlockQueue.hpp"
#include "Task.hpp"
#include <pthread.h>
#include <unistd.h>
#include <time.h>void *Consumer(void *args)
{BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(args); // 生产者和消费者同时看到了一份公共资源while(true){sleep(1);Task t;bq->Pop(&t);t.Run();std::cout << "consumer data: " << t.PrintResult() << std::endl;}return nullptr;
}void *Productor(void *args)
{BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(args);while(true){// 1. 准备数据int data1 = rand() % 10;usleep(rand() % 123);int data2 = rand() % 10;usleep(rand() % 123);char oper = opers[rand() % opers.size()];Task t(data1, data2, oper);// 2. 进行生产bq->Push(t);// for debugstd::cout << "productor data: " << t.PrintTask() << std::endl;}return nullptr;
}int main()
{// 种下随机数种子srand((uint16_t)time(nullptr) ^ getpid() ^ pthread_self()); pthread_t c[3], p[2];	// 3个消费者,两个生产者BlockQueue<Task> *rq = new BlockQueue<Task>();pthread_create(&p[0], nullptr, Productor, (void*)rq);pthread_create(&p[1], nullptr, Productor, (void*)rq);pthread_create(&c[0], nullptr, Consumer, (void*)rq);pthread_create(&c[1], nullptr, Consumer, (void*)rq);pthread_create(&c[2], nullptr, Consumer, (void*)rq);pthread_join(c[0], nullptr);pthread_join(c[1], nullptr);pthread_join(c[2], nullptr);pthread_join(p[0], nullptr);pthread_join(p[1], nullptr);return 0;
}

在这里插入图片描述


4.2.4 基于环形队列——单生产单消费


1. 算法思路

在这里插入图片描述

  • 生产者和消费者往一个环形队列中写入数据,需要满足两个条件:
    • 生产者不能把消费者套一个圈,出现这种情况意味着队列已经满;
    • 消费者不能超过生产者,出现这种情况意味着队列为空了。
  • 当队列为满时,只能让消费者向前跑;当队列为空时,只能让生产者向前跑。

2. 代码实现思路

  • 用一个vector逻辑抽象成环形队列。
  • 使用信号量管理资源:
    • 对于生产者,空间是资源;
    • 对于消费者,数据是资源。
  • 伪代码:
    • 开始时sem_space = Nsem_data = 0,N表示环形队列容量。

在这里插入图片描述

3. 环形队列模块(RingQueue.hpp)

#pragma once#include <iostream>
#include <vector>
#include <semaphore.h>const int defaultsize = 5;template<class T>
class RingQueue
{
private:void P(sem_t &sem){sem_wait(&sem);}void V(sem_t &sem){sem_post(&sem);}public:RingQueue(int size = defaultsize):_size(size),_ringqueue(size),_p_step(0),_c_step(0){sem_init(&_space_sem, 0, size);sem_init(&_data_sem, 0, 0);}// 生产void Push(const T &in){P(_space_sem);_ringqueue[_p_step] = in;_p_step++;_p_step %= _size;V(_data_sem);}// 消费void Pop(T *out){P(_data_sem);*out = _ringqueue[_c_step];_c_step++;_c_step %= _size;V(_space_sem);}~RingQueue(){sem_destroy(&_space_sem);sem_destroy(&_data_sem);}private:int _size;std::vector<T> _ringqueue;int _p_step;     // 消费者位置int _c_step;     // 消费者位置sem_t _space_sem;   // 空间信号量,生产者申请sem_t _data_sem;    // 数据信号量,消费者申请
};

4. 测试程序——单生产单消费

#include "RingQueue.hpp"
#include <pthread.h>
#include <unistd.h>void *Productor(void *args)
{sleep(3);   // 生产者先不生产,此时消费者会阻塞等待,观察同步RingQueue<int> *rq = static_cast<RingQueue<int> *>(args);int cnt = 100;while(true){rq->Push(cnt);std::cout << "product done, data is :" << cnt << std::endl;cnt--;sleep(1);}
}void *Consumer(void *args)
{RingQueue<int> *rq = static_cast<RingQueue<int> *>(args);while(true){ int data = 0;rq->Pop(&data);std::cout << "consumer done, data is: " << data << std::endl;}
}int main()
{pthread_t c, p;RingQueue<int> *rq = new RingQueue<int>();pthread_create(&p, nullptr, Productor, (void*)rq);pthread_create(&c, nullptr, Consumer, (void*)rq);pthread_join(c, nullptr);pthread_join(p, nullptr);return 0;
}

在这里插入图片描述

上一节生产者-消费者的例子是基于queue实现的,其空间可以动态分配,现在是基于固定大小的环形队列,不能动态分配。


4.2.5 基于环形队列,改造多生产多消费


1. 思考一下,能否复用之前单生产单消费的代码?

  • 不行,在单生产单消费的场景中,生产者与生产者之间,消费者和消费者之间,天然就是互斥的,不需要我们去维护这个关系。
  • 但是在多生产多消费场景下,我们需要再设计两个锁,维持生产者与生产者之间,消费者与消费者之间的互斥关系。

2. 环形队列改造(RingQueue.hpp)

#pragma once#include <iostream>
#include <vector>
#include <semaphore.h>const int defaultsize = 5;template<class T>
class RingQueue
{
private:void P(sem_t &sem){sem_wait(&sem);}void V(sem_t &sem){sem_post(&sem);}public:RingQueue(int size = defaultsize):_size(size),_ringqueue(size),_p_step(0),_c_step(0){sem_init(&_space_sem, 0, size);sem_init(&_data_sem, 0, 0);pthread_mutex_init(&_p_mutex, nullptr);pthread_mutex_init(&_c_mutex, nullptr);}// 生产void Push(const T &in){P(_space_sem);pthread_mutex_lock(&_p_mutex);_ringqueue[_p_step] = in;_p_step++;_p_step %= _size;pthread_mutex_unlock(&_p_mutex);V(_data_sem);}// 消费void Pop(T *out){   // 先申请信号量,再申请锁P(_data_sem);pthread_mutex_lock(&_c_mutex);*out = _ringqueue[_c_step];_c_step++;_c_step %= _size;pthread_mutex_unlock(&_c_mutex);V(_space_sem);}~RingQueue(){sem_destroy(&_space_sem);sem_destroy(&_data_sem);pthread_mutex_destroy(&_p_mutex);pthread_mutex_destroy(&_c_mutex);}private:int _size;std::vector<T> _ringqueue;int _p_step;     // 消费者位置int _c_step;     // 消费者位置sem_t _space_sem;   // 空间信号量,生产者申请sem_t _data_sem;    // 数据信号量,消费者申请pthread_mutex_t _p_mutex;     // 生产锁pthread_mutex_t _c_mutex;     // 消费锁
};
  • 细节:
    • 先申请信号量,还是先申请锁?结论是先申请信号量。
    • 举一个看电影的例子,先申请信号量,再申请锁,就相当于先并发的把票卖出去,然后再让有票的人一个一个进来;先申请锁,再申请信号量,就相当于先让人一个一个的进来,然后再买票,这时票只能一张一张卖了,这样无疑是比较慢的。

3. 测试程序

#include "RingQueue.hpp"
#include "Task.hpp"
#include <pthread.h>
#include <unistd.h>
#include <time.h>void *Consumer(void *args)
{RingQueue<Task> *rq = static_cast<RingQueue<Task> *>(args); // 生产者和消费者同时看到了一份公共资源while(true){sleep(1);Task t;rq->Pop(&t);t.Run();std::cout << "consumer data: " << t.PrintResult() << std::endl;}return nullptr;
}void *Productor(void *args)
{RingQueue<Task> *rq = static_cast<RingQueue<Task> *>(args);while(true){// 1. 准备数据int data1 = rand() % 10;usleep(rand() % 123);int data2 = rand() % 10;usleep(rand() % 123);char oper = opers[rand() % opers.size()];Task t(data1, data2, oper);// 2. 进行生产rq->Push(t);// for debugstd::cout << "productor data: " << t.PrintTask() << std::endl;}return nullptr;
}int main()
{// 种下随机数种子srand((uint16_t)time(nullptr) ^ getpid() ^ pthread_self()); pthread_t c[3], p[2];RingQueue<Task> *bq = new RingQueue<Task>();pthread_create(&p[0], nullptr, Productor, (void*)bq);pthread_create(&p[1], nullptr, Productor, (void*)bq);pthread_create(&c[0], nullptr, Consumer, (void*)bq);pthread_create(&c[1], nullptr, Consumer, (void*)bq);pthread_create(&c[2], nullptr, Consumer, (void*)bq);pthread_join(c[0], nullptr);pthread_join(c[1], nullptr);pthread_join(c[2], nullptr);pthread_join(p[0], nullptr);pthread_join(p[1], nullptr);return 0;
}

在这里插入图片描述


相关文章:

Linux从0到1——线程同步和互斥【互斥量/条件变量/信号量/PC模型】

Linux从0到1——线程同步和互斥 1. Linux线程互斥1.1 问题引入1.2 互斥相关概念1.3 多执行流并发访问公共资源的数据不一致问题1.4 互斥量&#xff08;锁&#xff09;1.5 改进抢票系统1.6 锁的简单封装1.7 锁的实现原理1.8 可重入VS线程安全1.9 死锁 2. Linux线程同步2.1 理解同…...

无人机驾驶证对入伍有帮助吗?

无人机驾驶证对入伍确实有一定的帮助&#xff0c;主要体现在以下几个方面&#xff1a; 一、提升专业技能 无人机操作是一项高度专业化的技能&#xff0c;需要掌握飞行原理、航电系统、任务规划、紧急处理等多方面的知识。通过考取无人机驾驶证&#xff0c;个人可以系统地学习这…...

【贪心算法】贪心算法七

贪心算法七 1.整数替换2.俄罗斯套娃信封问题3.可被三整除的最大和4.距离相等的条形码5.重构字符串 点赞&#x1f44d;&#x1f44d;收藏&#x1f31f;&#x1f31f;关注&#x1f496;&#x1f496; 你的支持是对我最大的鼓励&#xff0c;我们一起努力吧!&#x1f603;&#x1f…...

MySQL 锁概述

1.锁的分类 根据不同的分类角度可将锁分为&#xff1a; 按是否共享分&#xff1a;S 锁、X 锁按粒度分&#xff1a;表级锁、行级锁、全局锁&#xff08;锁整个库&#xff09;、页锁&#xff08;锁数据页&#xff09;意向锁&#xff1a;意向 S 锁、意向 X 锁&#xff1a;都是表…...

springboot502基于WEB的牙科诊所管理系统(论文+源码)_kaic

牙科诊所管理系统的设计与实现 摘要 近年来&#xff0c;信息化管理行业的不断兴起&#xff0c;使得人们的日常生活越来越离不开计算机和互联网技术。首先&#xff0c;根据收集到的用户需求分析&#xff0c;对设计系统有一个初步的认识与了解&#xff0c;确定牙科诊所管理系统的…...

overleaf中出现TeX capacity exceeded PDF object stream buffer=5000000的原因和解决方案

在插入pdf 配图后&#xff0c;编译出错提示信息如图&#xff0c;很可能的一个原因是pdf文件大小太大了&#xff0c;最好压缩一下&#xff0c;压缩到1MB以内。...

LabVIEW神经肌肉电刺激与记录系统

神经肌肉电刺激技术在康复医学和神经科学领域占有重要地位。基于LabVIEW开发了神经肌肉电刺激与记录系统&#xff0c;该系统具备可控电脉冲输出与高效数据采集功能&#xff0c;适用于临床和科研领域。 项目背景 神经肌肉电刺激技术用于治疗各类神经和肌肉系统疾病&#xff0c;…...

【CSS in Depth 2 精译_096】16.4:CSS 中的三维变换 + 16.5:本章小结

当前内容所在位置&#xff08;可进入专栏查看其他译好的章节内容&#xff09; 第五部分 添加动效 ✔️【第 16 章 变换】 ✔️ 16.1 旋转、平移、缩放与倾斜 16.1.1 变换原点的更改16.1.2 多重变换的设置16.1.3 单个变换属性的设置 16.2 变换在动效中的应用 16.2.1 放大图标&am…...

Label-Studio X SAM 半自动化标注

教程&#xff1a;playground/label_anything/readme_zh.md at main open-mmlab/playground GitHub B站视频&#xff1a;Label Studio x Segment Anything Model 半自动化标注_哔哩哔哩_bilibili 需要注意&#xff1a; 1.LINUX上跑比较方便 2.中文路径、文件名闯大祸 3. 4…...

Mono里运行C#脚本8—mono_image_storage_open打开EXE文件

Mono里运行C#脚本8—mono_image_storage_open打开EXE文件 前面分析哈希表的实现,以及文件打开的底层函数,还有保存到HASH表里的数据结构。 static MonoImageStorage * mono_image_storage_open (const char *fname) { char *key = NULL; key = mono_path_resolve_symlinks…...

【WebSocket】tomcat内部处理websocket的过程

websocket请求格式 浏览器请求 GET /webfin/websocket/ HTTP/1.1。 Host: localhost。 Upgrade: websocket。 Connection: Upgrade。 Sec-WebSocket-Key: xqBt3ImNzJbYqRINxEFlkg。 Origin: http://服务器地址。 Sec-WebSocket-Version: 13。 服务器响应 HTTP/1.1 101 Swi…...

将一个组件的propName属性与父组件中的variable变量进行双向绑定的vue3(组件传值)

封装组件看这个&#xff0c;然后理解父子组件传值 应用场景&#xff1a; 1.使用v - model语法实现双向绑定&#xff08;传值两边都不用定义方法接数据&#xff09; 1.子组件 1. update:modelValue事件是MultiSelect组件对象自带的事件 2.:options"countries" opti…...

Linux下通用型shellcode的编写

实验目的及要求 目的&#xff1a; 通过对本实验执行过程的理解&#xff0c;认真分析总结&#xff0c;能够独立的在 Linux 下进行 shellcode 的编写。 要求&#xff1a; &#xff08;1&#xff09;%70&#xff1a;完成对 shellcode 的分析及提取 &#xff08;2&#xff09;%…...

STM32F103RCT6学习之二:GPIO开发

GPIO基础 1.简介 2.GPIO基本结构 3.种模式 GPIO基本功能 1.输出功能--LED灯闪烁 1)进行基本配置 2)编辑代码 主要在main.c中编辑。 int main(void) {/* USER CODE BEGIN 1 *//* USER CODE END 1 *//* MCU Configuration------------------------------------------------…...

kong网关使用pre-function插件,改写接口的返回数据

一、背景 kong作为api网关&#xff0c;除了反向代理后端服务外&#xff0c;还可对接口进行预处理。 比如本文提及的一个小功能&#xff0c;根据http header某个字段的值&#xff0c;等于多少的时候&#xff0c;返回一个固定的报文。 使用到的kong插件是pre-function。 除了上…...

C++、Python有哪些相同和不同

C 和 Python 是两种流行的编程语言&#xff0c;设计理念和应用场景各有不同&#xff0c;但也有一些相似之处。以下是它们在语言特性、性能、语法等方面的相同点和不同点的比较&#xff1a; 相同点 支持多种编程范式&#xff1a; 面向对象编程 (OOP)&#xff1a;两者都支持类、继…...

Spring Boot 自动配置:从 spring.factories 到 AutoConfiguration.imports

Spring Boot 提供了强大的自动配置功能&#xff0c;通过约定优于配置的方式大大简化了应用开发。随着版本迭代&#xff0c;自动配置的实现方式也逐渐优化&#xff0c;从早期的 spring.factories 文件到最新的 META-INF/spring/org.springframework.boot.autoconfigure.AutoConf…...

HarmonyOS Next 应用元服务开发-应用接续动态配置迁移按需退出

按需退出&#xff0c;支持应用动态选择迁移成功后是否退出迁移源端应用&#xff08;默认迁移成功后退出迁移源端应用&#xff09;。如果应用不想让系统自动退出迁移源端应用&#xff0c;则可以设置不退出&#xff0c;参数定义见SUPPORT_CONTINUE_SOURCE_EXIT_KEY。 示例&#x…...

SQL-leetcode-180. 连续出现的数字

180. 连续出现的数字 表&#xff1a;Logs -------------------- | Column Name | Type | -------------------- | id | int | | num | varchar | -------------------- 在 SQL 中&#xff0c;id 是该表的主键。 id 是一个自增列。 找出所有至少连续出现三次的数字。 返回的…...

使用Python pickle模块进行序列化

使用Python pickle模块进行序列化 在Python中&#xff0c;pickle模块是一个用于实现数据序列化与反序列化的强大工具。与json模块不同的是&#xff0c;pickle支持将几乎所有的Python对象进行序列化&#xff0c;包括字典、列表、类实例&#xff0c;甚至函数。这使得它在处理复杂…...

责任链模式(ChainofResponsibilityPattern)

文章目录 1.定义2.结构3.问题描述代码实现 1.定义 允许你将请求沿着处理者链进行发送。 收到请求后&#xff0c; 每个处理者均可对请求进行处理&#xff0c; 或将其传递给链上的下个处理者。 2.结构 处理者(Handler)&#xff1a;声明了所有具体处理者的通用接口。 该接口通常…...

自定义 Element Plus 树状表格图标

在开发使用 Element Plus 的树状表格时&#xff0c;默认的展开/收起图标可能不能满足设计需求。为了更符合项目要求&#xff0c;可以通过覆盖样式的方式来自定义这些图标。以下记录了实现自定义树状表格图标的完整过程。 实现效果 有子节点且未展开时&#xff1a;显示一个加号…...

Vue2:用一个例子理解一下vue template中属性前面的冒号“:“

常用写法 table中绑定数据,我们通常这么写: ​​​​​​​<el-table :data="tableData" style="width: 100%">data() {tableData:[], } data绑定变量名可变的动态对象 但是上一篇文中,因为要生成15组相似的table,它们的格式都一样,只是数据…...

AF3 partition_tensor函数源码解读

该函数将输入张量按原子维度 (n_atoms) 分块,以局部窗口方式滑动,生成 滑动窗口张量。 在神经网络中,尤其是处理大规模序列数据(例如蛋白质原子特征)时,直接对整个序列执行操作可能会导致计算和内存效率问题。partition_tensor 函数通过对输入张量进行分块(partitions)…...

C++类与对象上

1.面向过程和面向对象初步认识 C语言是面向过程的&#xff0c;关注的是过程&#xff0c;分析出求解问题的步骤&#xff0c;通过函数调用逐步解决问题 例如洗衣服&#xff1a; C是基于面向对象的&#xff0c;关注的是对象&#xff0c;讲一件事拆分成不同的对象&#xff0c;靠对…...

中巨伟业推出高安全高性能32位智能卡内核可编程加密芯片SMEC88SP/ST

1、产品特性  以最高安全等级的智能卡芯片内核为基础&#xff0c;具有极高的软硬件安全性  实现客户关键功能或算法代码下载&#xff0c;用户可以灵活实现自有知识产权的保护  标准 SOP8、SOT23-6 封装形式&#xff0c;器件封装小  标准 I2C 接口&#xff0c;具有接…...

Python微博动态爬虫

本文是刘金路的《语言数据获取与分析基础》第十章的扩展&#xff0c;详细解释了如何利用Python进行微博爬虫&#xff0c;爬虫内容包括微博指定帖子的一级评论、评论时间、用户名、id、地区、点赞数。 整个过程十分明了&#xff0c;就是用户利用代码模拟Ajax请求&#xff0c;发…...

包管理工具npm、yarn、pnpm、cnpm详解

1. 包管理工具 1.1 npm # 安装 $ node 自带 npm# 基本用法 npm install package # 安装包 npm install # 安装所有依赖 npm install -g package # 全局安装 npm uninstall package # 卸载包 npm update package # 更新包 npm run script #…...

Docker和Kubernetes(K8s)区别

目录 1. Docker Docker 的核心概念&#xff1a; Docker 的功能&#xff1a; Docker 常见使用场景&#xff1a; 2. Kubernetes (K8s) Kubernetes 的核心概念&#xff1a; Kubernetes 的功能&#xff1a; Kubernetes 常见使用场景&#xff1a; 3.Docker 和 Kubernetes 的…...

龙智出席2024零跑智能汽车技术论坛,分享功能安全、需求管理、版本管理、代码扫描等DevSecOps落地实践

龙智快讯 2024年12月5日&#xff0c;由零跑和盖世汽车主办的“2024零跑智能汽车技术论坛”在杭州零跑总部圆满落幕。此次技术论坛聚焦AI语言大模型、AUTOSAR AP平台、DevOps、端到端自动驾驶等热点话题展开探讨&#xff0c;旨在推动智能汽车技术的创新与发展。 龙智作为国内领先…...

SQL进阶技巧:如何分析双重职务问题?

目录 0 背景描述 1 数据准备 2 问题分析 方法2&#xff1a;利用substr函数&#xff0c;充分利用数据特点【优秀解法】 3 小结 0 背景描述 在 CompuServe 刚成立时&#xff0c;Nigel Blumenthal 遇到一个应用程序中的困难。他需要获取公司人员所担任角色的源表&#xff0c;…...

SAQ问卷的定义,SAQ问卷是什么?

SAQ问卷&#xff0c;全称为可持续发展评估问卷&#xff08;Sustainability Assessment Questionnaire&#xff09;&#xff0c;是一种在线自评工具&#xff0c;其深远意义与广泛应用在当今商业环境中愈发凸显。它不仅是一种衡量企业在环境、社会和治理&#xff08;ESG&#xff…...

Express.js 有哪些常用的中间件?

在使用 Express.js 开发应用程序时&#xff0c;中间件&#xff08;Middleware&#xff09;是处理请求和响应的关键组件。它们可以执行各种任务&#xff0c;如解析请求体、添加HTTP头部、记录日志等。以下是一些常用的中间件&#xff1a; body-parser 用于解析传入的请求体。它…...

K8s DaemonSet的介绍

1. 什么是 DaemonSet&#xff1f; DaemonSet 是 Kubernetes 中的一种控制器&#xff0c;用于确保每个&#xff08;或某些指定的&#xff09;节点上运行一个 Pod 副本。它是为部署守护进程设计的&#xff0c;例如需要在每个节点上运行的任务或工具。 特点&#xff1a; Pod 会随…...

同步异步日志系统:设计模式

设计模式是前辈们对代码开发经验的总结&#xff0c;是解决特定问题的⼀系列套路。它不是语法规定&#xff0c;⽽是⼀ 套⽤来提⾼代码可复⽤性、可维护性、可读性、稳健性以及安全性的解决⽅案。 为什么会产生设计模式这样的东西呢&#xff1f;就像人类历史发展会产生兵法。最开…...

【GO基础学习】Gin 框架中间件的详解

文章目录 中间件详解中间件执行全局中间件路由级中间件运行流程中间件的链式执行中断流程 代码示例 gin框架总结 中间件详解 Gin 框架中间件是其核心特性之一&#xff0c;主要用于对 HTTP 请求的处理进行前置或后置的逻辑插入&#xff0c;例如日志记录、身份认证、错误处理等。…...

ubuntu停止.netcore正在运行程序的方法

在Ubuntu系统中停止正在运行的.NET Core程序&#xff0c;你可以使用以下几种方法&#xff1a; 使用kill命令&#xff1a; 如果你知道.NET Core程序的进程ID&#xff08;PID&#xff09;&#xff0c;你可以直接使用kill命令来停止它。首先&#xff0c;使用ps命令配合grep来查找.…...

图神经网络_图嵌入_Struc2Vec

0 背景 之前的node embedding方式&#xff0c;都是基于近邻关系&#xff0c;但是有些节点没有近邻&#xff0c;也有结构相似性。如图中的u、v节点。 struc2vec算法适用于捕获结构相似性。 1 相似度&#xff08;距离&#xff09;计算 1.1 公式 f k ( u , v ) f k − 1 ( u …...

LabVIEW应用在工业车间

LabVIEW作为一种图形化编程语言&#xff0c;以其强大的数据采集和硬件集成功能广泛应用于工业自动化领域。在工业车间中&#xff0c;LabVIEW不仅能够实现快速开发&#xff0c;还能通过灵活的硬件接口和直观的用户界面提升生产效率和设备管理水平。尽管其高成本和初期学习门槛可…...

js-000000000000

1、js书写的位置 - 内部 <body> <!-- 习惯把 js 放到 /body 的后面 --> <script> console.log(这是内部 js 的书写位置) alert(内部js) </script> </body> <body><!-- 习惯把 js 放到 /body 的后面 --><script>console.log(这…...

【微信小程序】3|首页搜索框 | 我的咖啡店-综合实训

首页-搜索框-跳转 引言 在微信小程序中&#xff0c;首页的搜索框是用户交互的重要入口。本文将通过“我的咖啡店”小程序的首页搜索框实现&#xff0c;详细介绍如何在微信小程序中创建和处理搜索框的交互。 1. 搜索函数实现 onClickInput函数在用户点击搜索框时触发&#x…...

虚幻引擎是什么?

Unreal Engine&#xff0c;是一款由Epic Games开发的游戏引擎。该引擎主要是为了开发第一人称射击游戏而设计&#xff0c;但现在已经被成功地应用于开发模拟游戏、恐怖游戏、角色扮演游戏等多种不同类型的游戏。虚幻引擎除了被用于开发游戏&#xff0c;现在也用于电影的虚拟制片…...

分布式光纤传感|分布式光纤测温|线型光纤感温火灾探测器DTS|DTS|DAS|BOTDA的行业16年的总结【2024年】

背景&#xff1a; 从2008年&#xff0c;从事分布式光纤传感行业已经过了16年时间了&#xff0c;依稀记得2008年&#xff0c;看的第一遍论文就是中国计量大学张在宣老爷子的分布式光纤测温综述&#xff0c;我的经历算是行业内极少数最丰富的之一。混过学术圈&#xff1a; 发表…...

【无标题】学生信息管理系统界面

网页是vue框架&#xff0c;后端直接python写的没使用框架...

ES7+ React/Redux/GraphQL/React-Native snippets 使用指南

VS Code React Snippets 使用指南 目录 简介基础方法React 相关React Native 相关Redux 相关PropTypes 相关控制台相关React 组件相关 简介 ES7 React/Redux/GraphQL/React-Native snippets 是一个用于 VS Code 的代码片段插件&#xff0c;它提供了大量用于 React 开发的代…...

Java中三大构建工具的发展历程(Ant、Maven和Gradle)

&#x1f438; 背景 我们要写一个Java程序&#xff0c;一般的步骤是编译&#xff0c;测试&#xff0c;打包。 这个构建的过程&#xff0c;如果文件比较少&#xff0c;我们可以手动使用java, javac,jar命令去做这些事情。但当工程越来越大&#xff0c;文件越来越多&#xff0c…...

【国产NI替代】32振动/电压(配置复合型)高精度终端采集板卡,应用于复杂的大型测量场景

32振动/电压&#xff08;配置复合型&#xff09;高精度终端采集板卡 采用 EP4CE115F29I7 型号的 FPGA &#xff0c;是一款 高精度&#xff0c;多通道动态信号采集器&#xff0c;主要应用 在复杂的大型测量并对成本要求不敏感的场 合&#xff0c;默认具备 8 个测量板卡&#…...

服务器上加入SFTP------(小白篇 1)

在服务器上配置 SFTP (基于 SSH 的文件传输协议) 通常比传统 FTP 更安全&#xff0c;因为它默认加密通信。以下是详细的配置步骤&#xff0c;以 Ubuntu 或 CentOS 为例。 1.服务器上加入SFTP------(小白篇 1) 2.加入SFTP 用户------(小白篇 2) 3.代码加入SFTP JAVA —&#…...

突围边缘:OpenAI开源实时嵌入式API,AI触角延伸至微观世界

当OpenAI宣布开源其名为openai-realtime-embedded-sdk的实时嵌入式API时&#xff0c;整个科技界都为之震惊。这一举动意味着&#xff0c;曾经遥不可及的强大AI能力&#xff0c;如今可以被嵌入到像ESP32这样的微型控制器中&#xff0c;真正地将AI的触角延伸到了物联网和边缘计算…...

【含开题报告+文档+PPT+源码】基于SpringBoot+Vue的影视网站系统的设计与实现

开题报告 随着互联网的快速发展和普及&#xff0c;人们对于娱乐和信息的需求越来越大。影视网站作为一种提供短视频、影视、电视剧、综艺节目等视频资源的网站&#xff0c;受到了广大用户的喜爱。然而&#xff0c;现有的影视网站系统仍然存在着一些安全性不强&#xff0c;用户…...