[Linux]:线程(二)

img

✨✨ 欢迎大家来到贝蒂大讲堂✨✨

🎈🎈养成好习惯,先赞后看哦~🎈🎈

所属专栏:Linux学习
贝蒂的主页:Betty’s blog

Windows环境不同,我们在linux环境下需要通过指令进行各操作,以下是常见操作的指令:

1. 线程互斥

1.1 基本概念

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

进程之间进行通信需要先创建第三方资源,使得不同的进程能够看到同一份资源。由于这份第三方资源可以由操作系统中的不同模块提供,所以进程间通信的方式有很多种。在进程间通信中,这个第三方资源被称为临界资源,而访问第三方资源的代码则被称为临界区。

与之不同的是,多线程的大部分资源都是共享的。因此,线程之间进行通信并不需要像进程那样费力地去创建第三方资源。

例如,我们在代码中只需要在全局区定义一个count变量,新线程可以每隔一秒对该变量进行加一操作,主线程也可以每隔一秒获取count变量的值并进行打印。

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
using namespace std;
int count = 0;
void *Routine(void *args)
{while (true){count++;sleep(1);}pthread_exit((void *)0);
}
int main()
{pthread_t tid;pthread_create(&tid, nullptr, Routine, nullptr);while (true){cout << "The value of count is " << count << endl;sleep(1);}pthread_join(tid, nullptr);return 0;
}

在当前情境下,我们相当于实现了主线程和新线程之间的通信。其中,全局变量count起着关键作用,它被称为为临界资源,原因在于它被多个执行流所共享。而主线程中的 cout 操作以及新线程中的 count++ 操作,被称作临界区。这是因为这些代码片段对临界资源进行了访问。

但是我们同样观察到打印数据并没有 1,这就是多执行流对临界资源操作常引发的数据不一致问题。

同样我们也可以下面抢票程序的实现,具体演示如果不对临界资源进行限制,可能会出现的危害。

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <cstdio>
#include <cstdlib>
using namespace std;
int tickets = 1000;
void *getTickets(void *args)
{string name = "thread ";name += to_string((uint64_t)args);while (true){if (tickets > 0){usleep(1000);cout << "[" << name << "]" << "get a ticket,left: " << --tickets << endl;}else{break;}}cout << name << " is quit!" << endl;pthread_exit((void *)0);
}
int main()
{pthread_t tids[5];for (uint64_t i = 0; i < 5; i++){pthread_create(tids + i, nullptr, getTickets, (void *)i);}for (int i = 0; i < 5; i++){pthread_join(tids[i], nullptr);}return 0;
}

剩余票数出现负数,这明显不符合我们的常识与预期,之所以出现这种情况,本质就是 tickets就是我们的临界资源,--tickets也 并不是原子的,在多执行流同时执行时就可能会发生这种问题。

为什么 --tickets并不是原子的呢?

因为从汇编角度看,我们的 --操作其实是不安全的,他们转成汇编,一般会对应三条汇编指令:从内存中读取数据到 CPU 中;CPU 内进行操作;CPU 将结果写回内存。进程在运行的时候,随时可能被切换。

1.2 互斥量

为了解决这个问题我们就引入了互斥,保证一次只有一个执行流访问临界资源,而为了实现互斥,我们就需要保证临界区的原子性,即临界区的资源要么被执行完成,要么不执行,只存在这两态。

要做到这些,本质就是需要一把锁,所以 Linux就引入一个锁,并将其称为互斥量。

画板

1.3 互斥量的接口

1.3.1 初始化互斥量

我们可以使用pthread_mutex_init初始化互斥量,使用方法如下:

  1. 函数原型:int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
  2. 参数:
  • mutex:需要初始化的互斥量。
  • attr:初始化互斥量的属性,一般设置为 nullptr 即可。
  1. 返回值:互斥量初始化成功返回0,失败返回错误码。

这种调用函数接口初始化互斥量的方式我们称为动态分配,除此之外,我们也能使用如下的方式进行初始化,我们将其称为静态分配。

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

1.3.2 销毁互斥量

我们可以使用pthread_mutex_destory销毁互斥量,使用方法如下:

  1. 函数原型:int pthread_mutex_destroy(pthread_mutex_t *mutex);
  2. 参数:mutex:需要销毁的互斥量。
  3. 返回值:成功返回 0,失败返回错误码。

其中销毁互斥量,需要注意以下几点:

  • 使用PTHREAD_MUTEX_INITIALIZER静态初始化的互斥量不需要销毁。
  • 不能销毁一个已经加锁的互斥量。
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁。
1.3.3 加锁互斥量

加锁本质就是让被加锁区域的代码具有原子性,只能同时被一个线程访问。我们可以使用pthread_mutex_lock对互斥量进行加锁,使用方法如下:

  1. 函数原型:int pthread_mutex_lock(pthread_mutex_t *mutex);
  2. 参数:mutex:需要加锁的互斥量。
  3. 返回值:成功返回 0,失败返回错误码。

如果一个线程在执行过程中,遇见该接口,并且该锁已被其他线程申请,那么该线程此时就会陷入阻塞状态,等待其解锁。

1.3.4 解锁互斥量

在加完锁之后,我们不可能让所有代码只被一个执行流访问,所以我们需要合适的地方解锁,我们可以使用pthread_mutex_unlock对互斥量进行解锁,使用方法如下:

  1. 函数原型:int pthread_mutex_unlock(pthread_mutex_t *mutex);
  2. 参数:mutex:需要解锁的互斥量。
  3. 返回值:成功返回 0,失败返回错误码。

知道了这些概念之后我们就可以对前面的抢票逻辑进行修改:

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <cstdio>
#include <cstdlib>
using namespace std;
int tickets = 1000;
pthread_mutex_t mutex;
void *getTickets(void *args)
{string name = "thread ";name += to_string((uint64_t)args);while (true){pthread_mutex_lock(&mutex);if (tickets > 0){usleep(1000);cout << "[" << name << "]" << "get a ticket,left: " << --tickets << endl;pthread_mutex_unlock(&mutex);}else{pthread_mutex_unlock(&mutex);break;}}cout << name << " is quit!" << endl;pthread_exit((void *)0);
}
int main()
{pthread_mutex_init(&mutex, nullptr);pthread_t tids[5];for (uint64_t i = 0; i < 5; i++){pthread_create(tids + i, nullptr, getTickets, (void *)i);}for (int i = 0; i < 5; i++){pthread_join(tids[i], nullptr);}pthread_mutex_destroy(&mutex);return 0;
}

其实在大部分情况下,加锁本身都是有损于性能的事,因为它使多执行流由并行执行变为了串行执行,这几乎是不可避免的。所以我们需要在合适的位置加锁与解锁,尽可能减少锁引入锁带来的性能开销成本。

1.4 互斥量的原理

当我们使用互斥量之后,临界区的代码对于其他线程来说,只有两种状态:加锁与解锁,这就保证了临界区的原子性。而我们要知道锁本身就是能被所有执行流访问的资源,所以锁本身也是一种临界资源,当然也需要保证其原子性,所以锁本身实现就是原子的。

为了实现互斥锁操作,大多数体系结构都提供了 swapexchange 指令,该指令的作用就是把寄存器和内存单元的数据相交换,以下就是实现加锁 lock与解锁 unlock的伪代码:

我们首先可以认为 mutex 的初始值为1,al 是计算机中的一个寄存器,当线程申请锁时,需要执行以下步骤:

  1. 先将 al 寄存器中的值清0。该动作可以被多个线程同时执行,因为每个线程都有自己的一组寄存器(上下文信息),执行该动作本质上是将自己的 al 寄存器清0。
  2. 然后交换 al 寄存器和 mutex 中的值。xchgb 是体系结构提供的交换指令,该指令可以完成寄存器和内存单元之间数据的交换。

画板

  1. 最后判断 al 寄存器中的值是否大于0。若大于0则申请锁成功,此时就可以进入临界区访问对应的临界资源;否则申请锁失败需要被挂起等待,直到锁被释放后再次竞争申请锁。

我们需要注意的是CPU内的寄存器不是被所有的线程共享的,每个线程都有独自的一组寄存器,所以改变当前线程 al寄存器的值并不会影响其他线程的 al寄存器, 当然内存中的数据因为属于同一个进程,所以各个线程是共享的。

画板

而当线程释放锁时,需要执行以下步骤:

  1. 将内存中的 mutex 置回1,使得下一个申请锁的线程在执行交换指令后能够得到1。
  2. 唤醒等待 mutex 的线程,让它们继续竞争申请锁。

在线程释放锁的过程中,并没有将当前线程的 al 寄存器中的值清0,这不会造成任何影响,因为每次线程在申请锁时都会先将自己 al 寄存器中的值清0,再执行交换指令。

所以我们申请锁的本质就是执行 xchgb这一条汇编指令,因为只有一条,所以只有已执行与未执行两种状态,具有原子性。

2. 线程安全

线程安全是指在多线程环境下,多个线程并发执行同一段代码时,不会出现不可预期的错误结果或数据不一致的情况。

常见线程不安全的情况有:

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

而线程安全的情况有:

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

而可重入函数与线程安全的联系有:

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

而可重入函数与线程安全的区别有:

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

3. 死锁

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

比如说如果某一执行流连续申请了两次锁,就会陷入死锁状态。具体情况如下:当该执行流第一次申请锁时,通常会申请成功。然而,第二次申请锁时,由于此锁已经被该执行流自身持有,再次申请会失败,进而导致该执行流被挂起。而此时,这个锁在其自己手上,可它又处于被挂起的状态,根本没有机会去释放锁。这样一来,该执行流将永远无法被唤醒,从而处于死锁状态。

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <cstdio>
#include <cstdlib>
using namespace std;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void*Routine(void*args)
{pthread_mutex_lock(&mutex);pthread_mutex_lock(&mutex);return nullptr;
}
int main()
{pthread_t tid;pthread_create(&tid,nullptr,Routine,nullptr);pthread_join(tid,nullptr);return 0;
}

其中形成死锁的必要条件有以下四个:

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

而为了避免死锁我们一般也可以从这几个角度思考:

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

除此之外,还有一些避免死锁的算法,常见的比如有死锁检测算法和银行家算法。

4. 线程同步

4.1 饥饿问题

线程饥饿指的是某些线程由于各种原因,一直无法获得足够的 CPU 时间来执行任务,从而处于长期等待或执行时间极少的状态。 产生线程饥饿的原因主要有以下几种:

  1. 高优先级线程抢占:如果系统中有高优先级的线程持续占用 CPU 资源,那么低优先级的线程就可能长时间得不到执行机会,从而导致饥饿。例如,在实时系统中,高优先级的实时任务可能会一直抢占低优先级的普通任务。
  2. 线程调度不公平:如果线程调度算法不合理或者存在缺陷,可能导致某些线程被不公平地对待,长期无法获得执行机会。比如某些调度算法可能偏向于某些特定类型的线程或者特定状态的线程。
  3. 资源竞争:当多个线程竞争有限的资源时,一些线程可能因为一直无法获得所需资源而被阻塞,从而无法执行。例如,多个线程竞争一个互斥锁,而某些线程总是在竞争中失败,就可能陷入饥饿状态。

线程饥饿会导致系统性能下降,部分任务无法及时完成,甚至可能使整个系统陷入停滞或出现不可预测的行为。为了解决线程饥饿问题,我们可以让线程与线程之间形成同步关系。

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

4.2 条件变量

条件变量是利用线程间共享的全局变量进行同步的一种机制,条件变量是用来描述某种资源是否就绪的一种数据化描述。其一般包含两个步骤:

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

4.3 条件变量的接口

4.3.1 初始化条件变量

我们可以使用pthread_cond_init初始化互斥量,使用方法如下:

  1. 函数原型:int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
  2. 参数:
  • cond:需要初始化的条件变量。
  • attr:初始化条件变量的属性,一般设置为 nullptr 即可。
  1. 返回值:条件变量初始化成功返回0,失败返回错误码。

这种调用函数接口初始化条件变量的方式我们称为动态分配,除此之外,我们也能使用如下的方式进行初始化,我们将其称为静态分配。

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

4.3.2 销毁条件变量

我们可以使用pthread_cond_destory销毁互斥量,使用方法如下:

  1. 函数原型:int pthread_cond_destroy(pthread_cond_t *cond);
  2. 参数:mutex:需要销毁的条件变量。
  3. 返回值:成功返回 0,失败返回错误码。
  • 使用 PTHREAD_COND_INITIALIZER 静态初始化的条件变量不需要销毁。
4.3.3 等待条件变量

当某个线程满足某个条件时,我们就可以将其至于条件变量下等待。

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

在满足某个条件之后,我们就可以使用以下两种即可,将等待队列中的线程唤醒。

  1. 函数原型:
  • int pthread_cond_broadcast(pthread_cond_t *cond);
  • int pthread_cond_signal(pthread_cond_t *cond);
  1. 参数:cond:需要唤醒的条件变量。
  2. 返回值:成功返回 0,失败返回错误码。

其中 pthread_cond_signal()函数用于唤醒等待队列中的第一个线程。pthread_cond_broadcast()函数用于唤醒等待队列中的全部线程。

比如我们下面创建五个线程,然后将其放入等待队列,最后由主线程进行唤醒。

#include <iostream>
#include <cstdio>
#include <pthread.h>
#include <unistd.h>
using namespace std;
#include <string>
pthread_mutex_t mutex;
pthread_cond_t cond;
void *Routine(void *args)
{pthread_detach(pthread_self());string name = "thread " + to_string((uint64_t)args);while (true){pthread_mutex_lock(&mutex);pthread_cond_wait(&cond, &mutex);cout << name << " running..." << endl;pthread_mutex_unlock(&mutex);}
}
int main()
{pthread_t tids[5];pthread_mutex_init(&mutex, nullptr);pthread_cond_init(&cond, nullptr);for (uint64_t i = 0; i < 5; i++){pthread_create(tids + i, nullptr, Routine, (void *)i);}while (true){sleep(1);pthread_cond_signal(&cond);}pthread_mutex_destroy(&mutex);pthread_cond_destroy(&cond);return 0;
}

在调用<font style="color:rgb(28, 31, 35);">pthread_cond_wait</font>函数时需要传入对应的互斥锁,原因如下:

当线程由于某些条件不满足而需要在特定条件变量下进行等待时,必须释放该互斥锁。这是因为如果不释放互斥锁,其他线程将无法获取该锁以进入临界区修改共享资源,从而无法改变条件使等待线程被唤醒。

当该线程被唤醒后,会接着执行临界区内的代码,这就要求该线程必须立即获得对应的互斥锁。这样设计确保了线程在被唤醒后能够安全地访问临界区,避免了多个线程同时进入临界区而导致的数据不一致和资源竞争问题。

4.4 条件变量使用规范

使用条件变量我们一般遵守以下规范,如果是等待条件变量,函数应该放在互斥量加锁与解锁之间,因为判断条件也是一种临界资源。

pthread_mutex_lock(&mutex);
while (条件为假)
pthread_cond_wait(cond, mutex);
//修改条件
pthread_mutex_unlock(&mutex);

同样唤醒操作也需要类似的操作。

pthread_mutex_lock(&mutex);
//条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);

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

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

相关文章

MAC的几个常见的快捷方式

1.mac 查看图片好的方式 默认查看图片的方式无法直接切换上一张下一张 解决方法&#xff1a; 1.&#xff08;最好的方法&#xff09;选中图片直接按空格&#xff0c;进入快速预览图片 2.就是全部选中然后打开&#xff0c;但是说实话有点奇怪&#xff0c;而且很占内存 3.直接显示…

【JAVA开源】基于Vue和SpringBoot的网上租赁系统

本文项目编号 T 050 &#xff0c;文末自助获取源码 \color{red}{T050&#xff0c;文末自助获取源码} T050&#xff0c;文末自助获取源码 目录 一、系统介绍二、演示录屏三、启动教程四、功能截图五、文案资料5.1 选题背景5.2 国内外研究现状5.3 可行性分析5.4 用例设计5.4.1 用…

[Linux]:线程(一)

✨✨ 欢迎大家来到贝蒂大讲堂✨✨ &#x1f388;&#x1f388;养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; 所属专栏&#xff1a;Linux学习 贝蒂的主页&#xff1a;Betty’s blog 1. 初识线程 1.1 线程的概念 在操作系统中&#xff0c;进程与线程一直是我们…

Vivado - JTAG to AXI Master (GPIO、IIC、HLS_IP)

目录 1. 简介 2. JTAG to AXI Master 2.1 添加 IP Core 2.2 基本TCL命令 2.2.1 复位 JTAG-to-AXI Master 2.2.2 创建并运行写入传输事务 2.2.3 创建并运行读取传输事务 2.2.4 命令列表 2.3 帮助信息 2.4 创建TCL读写程序 2.4.1 Read proc 2.4.2 Write proc 2.4.3 …

cuda程序编译流程

cuda程序编译流程 本文以cuda example的matrixMul矩阵乘法为例说明cuda程序的编译流程。 1. 源代码 .cu 文件 在matrixMul示例中&#xff0c;源代码文件 matrixMul.cu 是典型的CUDA程序&#xff0c;包含以下部分&#xff1a; 流程图 主机代码&#xff08;Host Code&#xf…

GNSS定位中自适应调整电离层延迟参数过程噪声的方法

文章目录 前言一、非差非组合PPP模型二、电离层功率谱密度计算三、具体实现方法3.1 不平滑3.2 三阶多项式平滑 参考文献 前言 GNSS定位中不少技术手段如PPP和长基线RTK需要将电离层延迟作为参数估计&#xff0c;电离层延迟的变化通常被描述为随机游走过程&#xff0c;而功率谱密…

1.2.1 计算机网络分层结构(上)

体系结构可分层使得不同的层次承担不同的功能。 知识点&#xff1a; 1.不同类型的节点&#xff0c;实现的功能层次可能不一样。 2.分层结构的设计并不唯一&#xff0c;可以根据实际需求增加或减少层次。 3.一个功能可以放在不同的层次反复出现。 根据分层结构不同可以分为&…

CORE MVC 过滤器 (筛选器)《2》 TypeFilter、ServiceFilter

TypeFilter、ServiceFilter ServiceFilter vs TypeFilter ServiceFilter和TypeFilter都实现了IFilterFactory ServiceFilter需要对自定义的Filter进行注册&#xff0c;TypeFilter不需要 ServiceFilter的Filter生命周期源自于您如何注册&#xff08;全局、区域&#xff09;&…

推荐4款2024年热门的PDF转ppt工具

有时候&#xff0c;我们为了方便&#xff0c;需要将PDF里面的内容直接转换的PPT的格式&#xff0c;既方便自己演示和讲解&#xff0c;也让我们可以更加灵活的进行文件的编辑和修改。如果大家不知道要如何进行操作的话&#xff0c;我可以为大家推荐几个比窘方便实用的PDF转换工具…

STM32LL库之printf函数重定向

1. 加入以下代码 int fputc(int ch,FILE *f) {LL_USART_TransmitData8(USART1,ch);while(!LL_USART_IsActiveFlag_TXE(USART1));//需要等待发送完成return(ch); }记得添加 stdio.h 头文件 2. 在MDK中勾选&#xff1a;Use MicroLIB

swiper+fixed的错误,splice函数的使用,提取年月日substring

做项目时的一些问题 swiperfixedsplice函数的使用重点在 alldata.splice(0, alldata.length, ...response.data.data);splicealldata.splice(0, alldata.length, ...response.data.data) 这行代码的功能为什么不直接赋值 提取年月日 substring swiperfixed 项目中的一个错误&a…

【人人都是P8程序员】Cursor 使用的十大技巧

Cursor 使用的十大技巧 总是在一个空的文件夹中创建一个新的项目 表述需求时尽量明确但谨慎 让Cursor从项目一开始就写README文档&#xff0c;让其记录清楚产品功能、实现技术栈等等&#xff0c;并在完成关键步骤后对README文档做及时的更新&#xff0c;第二天继续完成项目时…

npj Climate and Atmospheric Science I 新疆生地所陈亚宁研究员团队孙帆博士后发表最新研究进展

题目&#xff1a;The dominant warming season shifted from winter to spring in the arid region of Northwest China 主导中国西北干旱区升温的季节已从冬季转变为春季 期刊&#xff1a;npj Climate and Atmospheric Science IF及分区&#xff1a;实时IF/JCR分区/中科院分…

【Linux】Docker下载与使用-nginx

目录 一、Docker介绍 二、Docker结构 三、下载Daocker 1. 在linux上下载docker&#xff0c;执行以下命令即可&#xff1a; 2. 开启docker 3. 执行以下操作并进行使用 四、在Docker上安装nginx 一、Docker介绍 Docker&#xff1a;是给予Go语言实现的开源项…

召回12 曝光过滤 Bloom Filter

在推荐系统中&#xff0c;如果用户看过某个物品&#xff0c;就不再把物品推荐给这个用户。小红书、抖音都这样做曝光过滤&#xff0c;原因是实验表明重复曝光同一个物品会损害用户体验。但也不是所有推荐系统都有曝光过滤&#xff0c;像 YouTube 这样的长视频就没有曝光过滤&am…

ASR-01语音模块+C8T6实现语音控制LED

不说废话&#xff0c;简单直接&#xff0c;上教程&#xff0c;包会的&#xff0c;看不会&#xff0c;后台私我 一、接线图 STM32F103C8T6 ASR-01OLED屏PA10(RX接收串口)TX&#xff08;发送串口&#xff09;PB8SCL PB9 SDAVCCVCCGNDGND 二、天问软件Block图形编程 大家不要问…

Grafana链接iframe嵌入Web前端一直跳登录页面的问题记录

概述 公司有个项目使用到Grafana作为监控界面,因为项目方的环境极其复杂,仅物理隔离的环境就有三四个,而且每个都得部署项目,今天在某个环境测试,查看界面遇到一个比较奇怪的Grafana问题,后面针对该问题进行跟踪分析并解决,故而博文记录,用于备忘。 问题 登录项目We…

fastadmin 搜索提交重置按钮文本修改

默认 修改require-backend.min.js文件 效果 当然最好还是去需修改lang文件 效果 如果修改没生效记得清楚一下缓存&#xff0c;再刷新 完结 赠人玫瑰&#xff0c;手有余香&#xff01;如果文章内容对你有所帮助&#xff0c;请不要吝啬你的点赞评论和关注&#xff0c;你…

Tair简介

概述 Tair是淘宝团队开源的高可用分布式KV存储引擎&#xff0c;采用服务端自动负载均衡方式&#xff0c;使客户端逻辑简单。Tair&#xff0c;即TaoBao Pair缩写&#xff0c;Pair表示一对、一双等意思&#xff0c;即Key-Value数据对。 Tair分为持久化和非持久化两种方式。非持…

【Linux学习】【Ubuntu入门】2-1 Linux系统下运行C语言输出hello word

1.双击打开VMware软件&#xff0c;点击开启此虚拟机后&#xff0c;等待点击头像输入密码进入 2.“CtrlAltt”调出命令行终端&#xff0c;输入命令sudo apt-get install vim安装vim&#xff0c;输入命令sudo apt-get install gcc安装gcc 3.输入命令vi hello.c进入C语言编写环境&…