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

零基础Linux_23(多线程)线程安全+线程互斥(加锁)+死锁

目录

1. 线程安全

1.1 线程不安全前期

1.2 线程不安全原因

2. 线程互斥

2.1 加锁保护(代码)

2.2 锁的本质

3. 可重入对比线程安全

4. 死锁

4.1 死锁的必要条件

4.2 避免死锁

5. 笔试面试题

答案及解析

本篇完。


1. 线程安全

基于上一篇线程控制,这里创建个linux_23文件,在里面写代码,先看一段模拟抢票的代码:

Makefile:

mythread:mythread.ccg++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:rm -f mythread

mythread.cc:(创建了三个新线程抢票)

#include <iostream>
#include <cstdio>
#include <cerrno>
#include <cstring>
#include <thread>
#include <unistd.h>
#include <pthread.h>
using namespace std;// 如果多线程访问同一个全局变量,并对它进行数据计算,多线程会互相影响吗
int tickets = 10000; // 在并发访问的时候,导致了我们数据不一致的问题void *getTickets(void *args)
{(void)args;while(true){if(tickets > 0){usleep(1000);printf("%p: %d\n", pthread_self(), tickets);tickets--;}else{break;}}return nullptr;
}int main()
{pthread_t t1,t2,t3,t4;// 多线程抢票的逻辑pthread_create(&t1, nullptr, getTickets, (void*)"user1");pthread_create(&t2, nullptr, getTickets, (void*)"user2");pthread_create(&t3, nullptr, getTickets, (void*)"user3");pthread_create(&t4, nullptr, getTickets, (void*)"user4");pthread_join(t1, nullptr);pthread_join(t2, nullptr);pthread_join(t3, nullptr);
}

编译运行:

运行以后,发现出现了负数票,这不合理,票抢完就应该停止了,包括我们的代码逻辑都是这样写的,但是此时就出现了这种情况。

  • 上面现象的原因是发生了线程不安全问题

为什么产生了线程不安全现象:

上面现象故意弄出来的,涉及到了线程调度,利用了线程调度的特性造出了一个这样的现象。要想出现上面的现象,就需要尽可能让多个线程交叉执行。多个线程交叉执行的本质:就是让调度器尽可能的频繁发生线程调度与切换。

虽然看起来是多个线程在同时运行,但这是由于CPU运行速度太快导致的,实际上,CPU是一个线程一个线程执行的。现在就是要让CPU频繁调度,不停的切换线程,一个线程还没有执行完就再执行下一个,每个线程都执行一点,这样交叉执行。

当一个线程进行延时的时候,CPU并不会等它,而是会将它放在等待队列里,然后去执行另一个线程,等延时线程醒来以后才会接着执行。

线程在时间片到来,更高优先级线程到来,线程等待的时候会发生线程切换。

线程是在从内核态转换成用户态的时候检测是否达到线程切换的条件的。

线程检测是否切换是以内核态的身份去检测的,执行的是3~4G内核空间中的代码,本质上是操作系统在检测。


1.1 线程不安全前期

假设tickets已经只剩一张了,即全局变量tickets = 1。

主线程创建好4个新线程以后,4个新线程便开始执行了,在执行到延时的时候,新线程就会被放在等待队列里。看CPU及内核:

if(tickets > 0)判断的本质逻辑: 从内存中读取数据到CPU寄存器 ->  进行判断。

在线程user1执行到if判断时,CPU从内存中将tickets变量中的数据1拿到了CPU的寄存器ebx中。

CPU进行判断后,发现符合大于0的条件。

当线程user1符合条件继续向下执行延时代码时,CPU将线程user1切走了,换上了user2。

在线程user1被切走的时候,它的上下文数据也会被切走。

所以ebx寄存器中的1也会跟着user1的PCB被切走。

user2被调度时仍然重复user1的过程,执行延时被切走,再换上user3,以此类推,直到user4被切走。四个线程都拿到了tickets=1,所以符合条件,都能向下执行。

当user4被挂起后,user1差不多就该醒来了。user1唤醒以后接着被切走的位置继续执行:

执行tickets - - 的本质:

  • 从内存中读取数据到CPU的寄存器
  • 更改数据
  • 写回数据到内存中

虽然C/C++代码只有一条语句,但是汇编后至少有3条语句。

user1执行tickets–以后,抢票成功了,并且将抢票后的tickets=0写回到了内存中。

此时user2醒来了,同样接着它被切走的位置继续执行,此时user2回来后认为tickets=1,所以就向下执行了:

当执行tickets减减时,仍然需要三步:

  • 从内存中读取tickets=0到CPU寄存器ebx中。
  • 修改值,从0变成-1。
  • 将-1写回内存中。

当user2执行完后,user3和user4醒来同样继续向下执行,重复上面的过程,仍然对tickets减一,所以导致结果不合理。


1.2 线程不安全原因

只存在两个线程,对全局变量tickets仅作减减操作:

线程A先被CPU调度,进行减减操作。

  • 从内存中将tickets=1000取到寄存器ebx中。
  • 进行减减操作,tickets变成了999。
  • 在执行第三步写回数据之前,线程A被切走了。

线程A切走的同时,它的上下文,也就是tickets=999也被切走了。

线程B此时被调度,线程A在等待队列。

  • 线程B先从内存中读取tickets = 1000到寄存器ebx中。
  • 进行减减操作。
  • 将减减后的值写回到内存中。
  • 线程B将减减操作完整的执行了很多遍,直到tickets=200时才被切下去。

线程B被切走以后,线程A又接着被调度。

线程A接着被切走的位置开始执行,也就是执行减减的第三步操作壹壹写回。

  • 线程A被调度后,先恢复上下文,将被切走时的tickets=999恢复到了ebx寄存器中。
  • 然后执行第三步,将tickets=999写回到了内存中。

线程B辛辛苦苦将tickets从1000减到了200,线程A重新被调度后,直接将tickets又从200写回到了999。上面这种现象被叫做数据不一致问题

  • 导致数据不一致问题的原因:共享资源没有被保护,多线程对该资源进行了交叉访问。

而解决数据不一致问题的办法就是对共享资源加锁。


2. 线程互斥

看看几个基本概念:

临界资源:多个执行流进行安全访问的共享资源。

上面现象中的tickets很显然就不是临界资源,因为多线程对它的访问并不安全,存在数据不一致问题。

临界区:多个执行流中,访问临界资源的代码。

假设上面例子中的是临界资源,那么每个线程都存在一部分临界区,就是对tickets进行判断,打印,减减部分的代码。多个线程中的这部分代码属于临界区。

线程互斥:让多个线程串行访问共享资源,任何时候只有一个执行流在访问共享资源。

上面例子中如果多个线程能够串行访问tickets,而不是交叉访问,也不会产生数据不一致问题。而让共享资源变成临界资源就是为了实现互斥,也就是让多个线程串行访问原本的共享资源。

原子性:对一个资源进行访问的时候,要么不做,要么就做完。

在C/C++中的减减和加加操作,看似是一句代码,但是对应着三条汇编指令,上面例子中,线程A在执行第三步之前被切走了,导致减减操作没有完成,这种行为就不具有原子性,因为对共享资源的操作没有做完。

对一个资源进行操作,如果只用一条汇编就能完成,那么就具有原子性,反之就不具有原子性。(这是当前的一种理解,这种理解只能算原子性中的一个子集,是为了方便表述。)


2.1 加锁保护(代码)

要想解决多线程的数据不一致问题,就需要做到以下几点:

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

要做到上面三点,只需要一把锁就可以,持有锁的线程才能进入临界区中执行代码,并且其他线程无法进入该临界区。

锁:就是互斥量,也叫互斥锁。

加锁可以让共享资源临界资源化,从而保护共享资源的安全,让多个线程串行访问共享资源。

锁相关的系统调用:

pthread_mutex_t lock; // 定义一把锁

和创建线程一样,锁也需要创建,POSIX提供了锁的变量类型,如上面代码所示,其中mutext是互斥量的意思。

初始化锁:man pthread_mutex_init:

int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
  • 形参1:创建的互斥锁指针
  • 形参2:锁的属性,一般情况下设为nullptr
  • 返回值:初始化成功返回0,失败返回错误码
  • 作用:将创建的锁初始化。

销毁锁:

int pthread_mutex_destroy(pthread_mutex_t *mutex);
  • 形参:创建的互斥锁指针
  • 返回值:销毁成功返回0,失败返回错误码
  • 作用:当锁使用完后,必须进行销毁

全局或者静态锁初始化:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

如果锁是全局的或者被static修饰的静态锁,只需要使用上面语句初始化锁即可。

加锁:man pthread_mutex_lock:

int pthread_mutex_lock(pthread_mutex_t *mutex);
  • 形参:创建的互斥锁指针
  • 返回值:加锁成功返回0,失败返回错误码
  • 作用:给临界区加锁,让多线程串行访问临界资源

解锁:

int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • 形参:创建的互斥锁指针
  • 返回值:解锁成功返回0,失败返回错误码
  • 作用:解锁,让多线程恢复并发执行

锁其实起一个区间划分的作用,在加锁和解锁之间的代码就是临界区,多个执行流只能串行执行临界区代码,从而保护公共资源,使之成为临界资源。

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(lock);
//临界区
//...
pthread_mutex_unlock(lock);

加锁和解锁两句代码圈定了临界区的范围。

现在将抢票代码加上锁,看看是否还会出现多线程数据不一致问题:

Makefile:

mythread:mythread.ccg++ -o $@ $^ -g -std=c++11 -lpthread
.PHONY:clean
clean:rm -f mythread

mythread.cc:

在主线程中创建一个互斥锁,并且初始化,在所有新线程等待成功后将锁释放。

但是此时的锁是存在于主线程的栈结构中,需要让所有新线程看到这把锁。(创建成全局就不用)

在线程数据类中再增加一个锁指针,此时所有线程就都能看到这把锁了。

#include <iostream>
#include <cstdio>
#include <cerrno>
#include <cstring>
#include <cassert>
#include <thread>
#include <unistd.h>
#include <pthread.h>
using namespace std;int tickets = 10000; // 在并发访问的时候,导致了我们数据不一致的问题 -> 临界资源#define THREAD_NUM 10class ThreadData
{
public:ThreadData(const std::string &n,pthread_mutex_t *pm):tname(n), pmtx(pm){}
public:std::string tname;pthread_mutex_t *pmtx;
};void *getTickets(void *args)
{ThreadData *td = (ThreadData*)args;while(true) // 抢票逻辑{int n = pthread_mutex_lock(td->pmtx); // 加锁assert(n == 0);// 临界区if(tickets > 0) // 判断的本质也是计算的一种{usleep(rand()%1500);printf("%s: %d\n", td->tname.c_str(), tickets);tickets--; // 也可能出现问题n = pthread_mutex_unlock(td->pmtx); // 解锁assert(n == 0);}else{n = pthread_mutex_unlock(td->pmtx);  // break之前解锁assert(n == 0);break;}usleep(rand()%2000); // 抢完票,其实还需要后续的动作}delete td;return nullptr;
}int main()
{time_t start = time(nullptr);pthread_mutex_t mtx;pthread_mutex_init(&mtx, nullptr);pthread_t t[THREAD_NUM];for(int i = 0; i < THREAD_NUM; i++) // 多线程抢票的逻辑{std::string name = "thread ";name += std::to_string(i+1);ThreadData *td = new ThreadData(name, &mtx);pthread_create(t + i, nullptr, getTickets, (void*)td);}for(int i = 0; i < THREAD_NUM; i++){pthread_join(t[i], nullptr);}pthread_mutex_destroy(&mtx);return 0;
}

此时抢票的结果是正常了,最终抢到1结束,符合我们的预期。

但发现抢票的速度比以前慢了好多。

因为加锁和解锁的过程是多个线程串行执行的,并且临界区的代码也是串行执行的,所以速度就变慢了。

需要注意的是

  • 当一个线程从临界区中出来并且释放锁后,执行后续任务时,其他线程才有更大几率去竞争锁。
  • 加锁时,一定要保证临界区的粒度非常小。将那些不是必须放在临界区中的代码放在临界区外。
  • 加锁是程序员行为,要加锁就所有线程都加锁,否则就起不到保护共享资源的效果。

2.2 锁的本质

如何看待锁?

在上面代码中,一个锁必须让所有线程都看到,所以锁本身就是一个共享资源。

既然是共享资源,锁也必须是安全的,那么是谁来保证锁的安全性呢?

锁是通过加锁和解锁是原子的来保证自身的安全的。

一个线程,如果申请成功锁,那么它就会继续向下执行,如果暂时申请不成功呢?

此时就被阻塞住了,线程和进程都是存在的。

  • 一个锁只能被申请一次,只有锁被释放后才能再次申请。

当一个线程申请锁暂时失败以后,就会阻塞不动。

  • 当一个线程申请锁成功,进入临界区访问临界资源,其他线程要想进入临界区只能阻塞等待,等锁释放。
  • 当一个线程申请锁成功,进入临界区访问临界资源,同样是能被切走的,而且该线程是抱着锁走的,其他线程仍然无法申请锁成功。

操作系统内部并不存在锁的概念,所以调度器在调度轻量级进程的时候并不会考虑是否有锁。

所以站在其他线程的角度,锁只有两种状态:

  • 申请锁前
  • 申请锁后

站在其他线程的角度,看到当前持有锁的过程就是原子的。

加锁解锁的原理:

经过上面的例子,我们认识到一个事实,c/c++中加加和减减的操作并不是原子的,所以会导致多线程数据不一致的问题。

而为了能让加锁过程是原子的,在大多数体系结构了,都提供了swap或者xchange汇编指令,通过一条汇编指令来保证加锁的原子性。

加锁解锁的伪汇编代码:

lock:movb %al, $0xchange %al, mutexif(al寄存器的内容 > 0){return 0;}else{挂起等待;	}goto lock;unlock:movb mutex, $1唤醒等待mutex的线程;return 0;

加锁过程中,xchange是原子的,可以保证锁的安全。

锁只能被一个线程持有,而且由于xchange汇编只有一条指令,即使申请锁的过程被切走也不怕。

一旦一个线程通过xchage拿到了锁,即使它被切走,也是拿着锁走的,其他线程是无法拿到锁的,只有等它将锁释放。

只有持有锁的线程才能执行下去,锁相当于一张入场卷。

这样来看,释放锁的过程其实对原子性的要求并没有那么高,因为释放锁的线程必定是持有锁的线程,不持有锁的线程都不会执行到这里,都在阻塞等待。


3. 可重入对比线程安全

重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。

之前在信号部分就提到过重入,进程在执行一个函数,收到某个信号在处理信号时又调用了这个函数。今天在多线程这里,理解重入更加容易,我们以前写的多线程代码都是可重入的。

可重入和不可重入:一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

常见可重入情况:

  •  不使用全局变量或静态变量。
  •  不使用用malloc或者new开辟出的空间。
  •  不返回静态或全局数据,所有数据都有函数的调用者提供。

常见可重入情况:

  •  不使用全局变量或静态变量。
  •  不使用用malloc或者new开辟出的空间。
  •  不返回静态或全局数据,所有数据都有函数的调用者提供。

总的来说,一个函数中如果使用了全局数据,或者静态数据,以及堆区上的数据,就是不可重入的,反之就是可重入的。

线程安全:

多个线程并发同一段代码时,不会出现不同的结果(数据不一致)。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。

互斥锁就是让不安全的线程变安全,也就是前面我们所学习的内容。

常见线程安全情况:

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

多线程共同执行的代码段中,如果有全局变量或者静态变量没有被保护,那么就是线程不安全的。

常见线程不安全情况:

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

可重入与线程安全的联系:

多线程是通过调用函数来实现的,所以线程安全和重入就存在一些联系:

  • 函数是可重入的,那就是线程安全的,因为没有全局或者静态变量,不会产生数据不一致问题。
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题。出发对不可重入函数的全局变量进行保护。
  • 如果一个函数中有全局变量并且没有保护,那么这个函数既不是线程安全也不是可重入的。

可重入与线程安全的区别:

可重入和线程安全是不同的两个东西,但是又存在一定的交集。

  • 可重入说的是函数。
  • 线程安全说的是线程。

可重入函数是线程安全函数的一种,因为不存在全局或者静态变量。

线程安全不一定是可重入的,而可重入函数则一定是线程安全的。因为线程安全的情况可能是对全局变量进行了保护(加了锁)。

由于线程可以加锁,所以说线程安全的情况比可重入要多。


4. 死锁

我们前面例子中写的都是只有一把锁的情况,在实际使用中有可能会存在多把锁,此时就可能造成死锁。

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

通俗来说就是一个线程自己持有锁,并且不会释放,但是还要申请其他线程的锁,此时就容易造成死锁。

一把锁也是会有死锁的情况的,连续申请俩次就是死锁。

在上面演示一个线程暂时申请锁失败而阻塞时,就是死锁。


4.1 死锁的必要条件

死锁的四个必要条件:

① 互斥

这一点不用说,只要用到锁就会互斥。

② 请求与保持

请求就是指一个执行流申请其他锁,保持是指不释放自己已经持有的锁。

③ 不剥夺

一个执行流已经持有锁,在不主动释放前不能强行剥夺。

④ 环路等待

线程A,B,C都持有一把锁,并且不释放。

  • 线程A 申请 线程B持有的锁B
  • 线程B 申请 线程C持有的锁C
  • 线程C 申请 线程A持有的锁A

此时就构成了环路阻塞等待。

只有符合上面四个条件就会造成死锁。而要破坏死锁只要破坏其中一个条件即可。


4.2 避免死锁

① 上面四个必要条件中的第一个无法破坏,因为我们使用的就是锁,锁就具有互斥的性质。只能破坏其他三个条件。

② 加锁顺序一致

这是为了避免形参环路等待,只要不构成环路即可。

③ 避免锁位释放的场景

④ 避免锁位释放的场景

临界资源尽量一次性分配好,不要分布在太多的地方加锁,这样的话导致死锁的概率就会增加。

解决死锁的基本方法如下:

预防死锁、避免死锁、检测死锁、解除死锁。

解决死锁的常用策略如下:

鸵鸟策略 对可能出现的问题采取无视态度,前提是出现概率很低

预防策略 破坏死锁产生的必要条件

避免策略 银行家算法,分配资源前进行风险判断,避免风险的发生

检测与解除死锁 分配资源时不采取措施,但是必须提供死锁的检测与解除手段

可以避免(预防)死锁的算法(了解):

  • 死锁检测算法
  • 银行家算法(避免策略)

银行家算法的思想在于将系统运行分为两种状态:安全/非安全,有可能出现风险的都属于非安全。

银行家算法的思想是为了避免出现“环路等待”条件


5. 笔试面试题

1. 以下描述正确的有:

A.可以使用ps -l命令查看轻量级进程信息

B.可以使用ps -L命令查看轻量级进程信息

C.可以使用pthread_self接口获取轻量级进程ID

D.可以使用getpid接口获取轻量级进程ID

2. 以下描述正确的有:[多选]

A.pthread_create函数是一个库函数, 代码当中如果使用该函数创建线程, 则需要在编译的时候链接“libpthread.so”线程库

B.那个线程调用pthread_exit函数, 那个线程就退出。俗称“谁调用谁退出”

C.在有多个线程的情况下,主线程调用pthread_cancel(pthread_self()), 则主线程状态为Z, 其他线程正常运行

D.在有多个线程的情况下,主线程从main函数的return返回或者调用pthread_exit函数,则整个进程退出

3. 下列不属于POSIX互斥锁相关函数的是:()

A.int pthread_mutex_destroy(pthread_mutex_t* mutex)

B.int pthread_mutex_lock(pthread_mutex_t* mutex)

C.int pthread_mutex_trylock(pthread_mutex_t* mutex)

D.int pthread_mutex_create(pthread_mutex_t* mutex)

4. 进程A、B共享变量x,需要互斥执行;

    进程B、C共享变量y,B、C也需要互斥执行,

    因此进程A、C必须互斥执行

A.错

B.对

5. 设两个进程共用一个临界资源的互斥信号量mutex,当mutex=1时表示()。

A.一个进程进入了临界区,另一个进程等待

B.没有一个进程进入临界区

C.两个进程都进入临界区

D.两个进程都在等待

6. 在一段时间内,只允许一个进程访问的资源被称为()

A.共享资源

B.临界区

C.临界资源

D.共享区

7. 简述轻量级进程ID与进程ID之间的区别

8. 简述LWP与pthread_create创建的线程之间的关系

9. 简述什么是LWP

10. 简述什么是线程互斥,为什么需要互斥


答案及解析

1. B

A错误,

B正确 ps命令用于查看进程信息,其中-L选项用于查看轻量级进程信息

C错误 pthread_self() 用于获取用户态线程的tid,而并非轻量级进程ID

D错误 getpid() 用于获取当前进程的id,而并非某个特定轻量级进程

2. ABC

C:主线程调用pthread_cancel(pthread_self())函数来退出自己, 则主线程对应的轻量级进程状态变更成为Z, 其他线程不受影响,这是正确的(正常情况下我们也不会这么做....)

D:主线程调用pthread_exit只是退出主线程,并不会导致进程的退出

3. D

A pthread_mutex_destroy 用于销毁互斥锁

B pthread_mutex_lock 用于加锁保护临界区

C pthread_mutex_trylock 用户非阻塞加锁

D 没有这个函数 pthread_create是线程创建函数,而互斥锁并没有对应的创建函数,而是直接定义pthread_mutex_t类型的互斥锁变量

4. A

进程A操作的x,C并不进行操作;进程C操作的y,进程A并不操作;因此A和C并不需要互斥执行

5. B

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

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

1表示未加锁,资源可访问。

因此选择B选项,表示没有执行流完成加锁对资源进行访问,资源处于可访问状态。

6. C

A 共享资源表示能够被多个执行流同时访问的资源

B 对临界资源进行操作的代码段被称作临界区

C 临界资源表示同一时间只能有一个执行流访问的共享资源

D 没有这个专业说法,非要简单理解就是可以共同执行的代码片段

题目为选择同一时间只有一个进程能访问的资源,则就是临界资源,因此选择C选项

7. 简述轻量级进程ID与进程ID之间的区别:

因为Linux下的轻量级进程是一个pcb,每个轻量级进程都有一个自己的轻量级进程ID(pcb中的pid),而同一个程序中的轻量级进程组成线程组,拥有一个共同的线程组ID

8. 简述LWP与pthread_create创建的线程之间的关系:

pthread_create是一个库函数,功能是在用户态创建一个用户线程,而这个线程的运行调度是基于一个轻量级进程实现的

9. 简述什么是LWP

LWP是轻量级进程,在Linux下进程是资源分配的基本单位,线程是cpu调度的基本单位,而线程使用进程pcb描述实现,并且同一个进程中的所有pcb共用同一个虚拟地址空间,因此相较于传统进程更加的轻量化

10. 简述什么是线程互斥,为什么需要互斥:

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

本篇完。

下一篇:零基础Linux_24(多线程)线程同步+条件变量+生产者消费模型。

相关文章:

零基础Linux_23(多线程)线程安全+线程互斥(加锁)+死锁

目录 1. 线程安全 1.1 线程不安全前期 1.2 线程不安全原因 2. 线程互斥 2.1 加锁保护&#xff08;代码&#xff09; 2.2 锁的本质 3. 可重入对比线程安全 4. 死锁 4.1 死锁的必要条件 4.2 避免死锁 5. 笔试面试题 答案及解析 本篇完。 1. 线程安全 基于上一篇线程…...

【算法|贪心算法系列No.5】leetcode409. 最长回文串

个人主页&#xff1a;兜里有颗棉花糖 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 兜里有颗棉花糖 原创 收录于专栏【手撕算法系列专栏】【LeetCode】 &#x1f354;本专栏旨在提高自己算法能力的同时&#xff0c;记录一下自己的学习过程&#xff0c;希望…...

【Linux】安装与配置虚拟机及虚拟机服务器坏境配置与连接---超详细教学

一&#xff0c;操作系统介绍 1.1.什么是操作系统 操作系统&#xff08;Operating System&#xff0c;简称OS&#xff09;是一种系统软件&#xff0c;它是计算机硬件和应用软件之间的桥梁。它管理计算机的硬件和软件资源&#xff0c;为应用程序提供接口和服务&#xff0c;并协…...

机器学习实验一:KNN算法,手写数字数据集(使用汉明距离)(2)

KNN-手写数字数据集&#xff1a; 使用sklearn中的KNN算法工具包&#xff08; KNeighborsClassifier)替换实现分类器的构建&#xff0c;注意使用的是汉明距离&#xff1b; 运行结果&#xff1a;&#xff08;大概要运行4分钟左右&#xff09; 代码&#xff1a; import pandas as…...

docker应用部署---nginx部署的配置

1. 搜索nginx镜像 docker search nginx2. 拉取nginx镜像 docker pull nginx3. 创建容器&#xff0c;设置端口映射、目录映射 # 在/root目录下创建nginx目录用于存储nginx数据信息 mkdir ~/nginx cd ~/nginx mkdir conf cd conf# 在~/nginx/conf/下创建nginx.conf文件,粘贴下…...

Sql Server中的表组织和索引组织(聚集索引结构,非聚集索引结构,堆结构)

正文 SqlServer用三种方法来组织其分区中的数据或索引页&#xff1a; 1、聚集索引结构 聚集索引是按B树结构进行组织的&#xff0c;B树中的每一页称为一个索引节点。每个索引行包含一个键值和一个指针。指针指向B树上的某一中间级页&#xff08;比如根节点指向中间级节点中的…...

C++类对象反制机制实现_精简修改版

前几天写的类对象反射机制太烦锁了,今天写个修改版的,精简为两个类 一个是类的数据结构,另一个是类的父类对象,把所有操作类的方法都写到父类中 1.类的信息结构体 struct Field_Node {TCHAR m_name[20]; //字段名称TCHAR m_typeName[20]; // 字段类型名称size_t m_typeHashC…...

C#开发的IEnumerable接口

C#开发的IEnumerable接口 在前面分析中,我们会遇到下面这行代码: var refineries = self.World.ActorsWithTrait<IAcceptResources>() .Where(r => r.Actor != ignore && r.Actor.Owner == self.Owner && IsAcceptableProcType(r.Actor)) .Select…...

Redis详细安装教程

目录 一、Redis 的安装及启动停止1-1 下载 redis的压缩包1-2 开始解压 redis1-3 执行 make 命令编译1-4 启动 redis修改配置文件1-5 设置远程连接1-6 设置后台启动1-7 设置密码1-8 配置服务启动&#xff08;使用 systemctl 的方法&#xff09;启动 redis配置开机启动操作redis使…...

36基于matlab的对分解层数和惩罚因子进行优化

基于matlab的对分解层数和惩罚因子进行优化。蚁狮优化算法优化VMD,算术优化算法优化VMD&#xff0c;遗传优化算法优化VMD&#xff0c;灰狼优化算法优化VMD&#xff0c;海洋捕食者优化算法优化VMD&#xff0c;粒子群优化VMD&#xff0c;麻雀优化算法优化VMD&#xff0c;鲸鱼优化…...

【Flutter】自定义分段选择器Slider

【Flutter】ZFJ自定义分段选择器Slider 前言 在开发一个APP的时候&#xff0c;需要用到一个分段选择器&#xff0c;系统的不满足就自己自定义了一个&#xff1b; 可以自定义节点的数量、自定义节点的大小、自定义滑竿的粗细&#xff0c;自定义气泡的有无等等… 基本上满足你…...

【软考系统架构设计师】2023年系统架构师冲刺模拟习题之《软件工程》

在软考中软件工程模块主要包含以下考点&#xff1a; 文章目录 软件过程模型&#x1f31f;&#x1f31f;&#x1f31f;&#x1f31f;逆向工程&#x1f31f;基于构件的软件工程&#x1f31f;&#x1f31f;软件开发与软件设计与维护净室软件工程软件模型软件需求 软件过程模型&am…...

非遗主题网站的设计与实现基于PHP实现

包括源码参考论文 下载地址: https://juzhendongli.store/commodity/details/18...

YOLO目标检测——红外人员数据集【含对应voc、coco和yolo三种格式标签+划分脚本】

实际项目应用&#xff1a;红外热像仪进行安全监控数据集说明&#xff1a;红外人员检测数据集&#xff0c;真实场景的高质量图片数据标签说明&#xff1a;使用lableimg标注软件标注&#xff0c;标注框质量高&#xff0c;含voc(xml)、coco(json)和yolo(txt)三种格式标签&#xff…...

C++项目——云备份-⑧-客户端各模块实现

文章目录 专栏导读1.客户端数据管理模块实现2.客户端文件检测模块实现3.客户端文件备份模块设计4.客户端文件备份模块实现 专栏导读 &#x1f338;作者简介&#xff1a;花想云 &#xff0c;在读本科生一枚&#xff0c;C/C领域新星创作者&#xff0c;新星计划导师&#xff0c;阿…...

分享一款基于 AI 的 Chrome 插件

最近使用大模型比较多&#xff0c;公司虽然提供了免费的 ChatGPT 但是需要跳转特定页面才能访问&#xff0c;比较麻烦&#xff0c;于是就想到是否可以开发一款类似于有道词典一样的 Chrome 插件&#xff0c;可以在任意页面使用&#xff0c;虽然市面上也有类似的插件&#xff0c…...

Spring Authorization Server 1.1 扩展实现 OAuth2 密码模式与 Spring Cloud 的整合实战

目录 前言无图无真相创建数据库授权服务器maven 依赖application.yml授权服务器配置AuthorizationServierConfigDefaultSecutiryConfig 密码模式扩展PasswordAuthenticationTokenPasswordAuthenticationConverterPasswordAuthenticationProvider JWT 自定义字段自定义认证响应认…...

第二证券:AIGC概念活跃,焦点科技、三维通信涨停,万兴科技大涨

AIGC概念24日盘中走势生动&#xff0c;到发稿&#xff0c;万兴科技、三态股份涨超10%&#xff0c;焦点科技、三维通讯、我国科传等涨停&#xff0c;中文在线涨超9%&#xff0c;果麦文明、新国都涨约7%。 消息面上&#xff0c;各大电商途径于10月18-24日先后发动“双11”大促或…...

7-4、S加减速转动实现【51单片机控制步进电机-TB6600系列】

摘要&#xff1a;本节介绍实现步进电机S曲线运动的代码 一、目标功能 实现步进电机转动总角度720&#xff0c;其中加减速各90 加速段&#xff1a;加速类型&#xff1a;S曲线   加速角度&#xff1a;角度为90   起步速度&#xff1a;30RPM&#xff0c;   终止速度&#x…...

RK3568-pcie接口

pcie接口与sata接口 pcie总线pcie总线pcie控制器sata控制器nvme设备sata设备nvme协议ahci协议m-key接口b-key接口RC模式和EP模式 RC和EP分别对应主模式和从模式,普通的PCI RC主模式可以用于连接PCI-E以太网芯片或PCI-E的硬盘等外设。 RC模式使用外设一般都有LINUX驱动程序,安…...

spring监听请求执行结束,移除当前ThreadLocal数据两种方法

在开发过程中&#xff0c;很多时候我们会使用ThreadLocal来临时缓存数据&#xff0c;当一次数据请求执行完成后需要主动执行释放当前ThreadLocal缓存数据资源&#xff0c;防止未能及时释放导致下一次访问时候ThreadLocal依然保持上一次缓存的数据。 spring提供两种方式去监听一…...

知识图谱--Jena基础操作和检索推理应用

在上一篇读书笔记中讲到知识图谱存储主要有基于开源的Jena方式和基于图数据库(Neo4j)方式,本次主要对Jena的基础操作和如何应用进行了实践总结,同时结合了D2R,将结构化数据转换成Jena可以加载的格式(即RDF,后缀是.nt),Apache Jena作为一种开源的Java语义网框架,主要功…...

GEE python——将GEE ASSETS中存储的影像或者矢量转化为数据格式XEE()

数据转换器是内置于 getPixels、computePixels、listFeatures 和 computeFeatures 中的客户端转换能力。通过指定兼容的文件格式,这些方法可以返回 Python 原生格式的数据,如用于栅格的结构化 NumPy 数组和用于矢量的 Pandas DataFrames 或 GeoPandas GeoDataFrames。对于矢量…...

Java集合框架:List、Set、Map类型及泛型详解

文章目录 &#x1f4d5;我是廖志伟&#xff0c;一名Java开发工程师、Java领域优质创作者、CSDN博客专家、51CTO专家博主、阿里云专家博主、清华大学出版社签约作者、产品软文创造者、技术文章评审老师、问卷调查设计师、个人社区创始人、开源项目贡献者。&#x1f30e;跑过十五…...

Ubuntu 安装 docker

一.添加Docker官方GPG密钥 curl -fsSL http://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | sudo apt-key add - 如果出现&#xff1a; The program curl is currently not installed. You can install it by typing: sudo apt install curl 先安装 curl : sudo apt inst…...

BUUCTF zip伪加密 1

BUUCTF:https://buuoj.cn/challenges 题目描述&#xff1a; 下载附件&#xff0c;得到一个zip压缩包。 密文&#xff1a; 解题思路&#xff1a; 1、刚开始尝试解压&#xff0c;看到了flag.txt文件&#xff0c;但需要解压密码。结合题目&#xff0c;确认这是zip伪加密&#…...

p5.js 到底怎么设置背景图?

本文简介 点赞 关注 收藏 学会了 在 《p5.js 光速入门》 里我们学过加载图片元素&#xff0c;学过过背景色的用法&#xff0c;但当时没提到背景图要怎么使用。 本文就把背景图这部分内容补充完整&#xff0c;并且会提到在 p5.js 里使用背景图的一些注意点。 背景图的用法…...

python+unittest+requests+HTMLRunner编写接口自动化测试集

问题描述&#xff1a; 搭建接口测试框架&#xff0c;执行用例请求多个不同请求方式的接口 实现步骤&#xff1a; ① 创建配置文件config.ini&#xff0c;写入部分公用参数&#xff0c;如接口的基本url、测试报告文件路径、测试数据文件路径等配置项 1 [DATABASE] 2 data_addre…...

Vue---监听div元素宽高改变时echart图表重新resize

一、需求描述 当点击上图的红色框时&#xff0c;echart的div元素宽会改变但是无法触发echarts图表的resize重新渲染&#xff0c;对于浏览器而言&#xff0c;浏览器具有window.resize方法监听浏览器窗口大小的改变&#xff0c;而div元素没有监听宽高改变的方法。 二、解决方案 …...

Kubernetes Etcd不可用日志:NOSPACE 的问题修复

两套k8s集群同一天同时出现etcd集群空间超过配额&#xff0c;kubectl get cs时发现所有的etcd均返回503报错&#xff0c;查看etcd的告警发现有NO SPACE的信息且 etcdctl --endpoints${ETCD_ENDPOINT} --cert${ETCD_CERTFILE} --key${ETCD_KEYFILE} --cacert${ETCD_CAFILE} endp…...