【Linux操作系统】:Linux生产者消费者模型

news2024/9/21 0:38:44

目录

生产者消费者模型的概念

生产者消费者模型的特点

生产者消费者模型优点 

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

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

模拟实现基于阻塞队列的生产消费模型


生产者消费者模型的概念

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。

生产者和消费者彼此之间不直接通讯,而通过这个容器来通讯,所以生产者生产完数据之后不用等待消费者处理,直接将生产的数据放到这个容器当中,消费者也不用找生产者要数据,而是直接从这个容器里取数据,这个容器就相当于一个缓冲区,平衡了生产者和消费者的处理能力,这个容器实际上就是用来给生产者和消费者解耦的。

生产者消费者模型的特点

生产者消费者模型是多线程同步与互斥的一个经典场景,其特点如下:

  1. 三种关系: 生产者和生产者(互斥关系)、消费者和消费者(互斥关系)、生产者和消费者(互斥关系、同步关系)。
  2. 两种角色: 生产者和消费者。(通常由进程或线程承担)
  3. 一个交易场所: 通常指的是内存中的一段缓冲区。(可以自己通过某种方式组织起来)

我们用代码编写生产者消费者模型的时候,本质就是对这三个特点进行维护。

生产者和生产者、消费者和消费者、生产者和消费者,它们之间为什么会存在互斥关系?

  1. 介于生产者和消费者之间的容器可能会被多个执行流同时访问,因此我们需要将该临界资源用互斥锁保护起来。
  2. 所有的生产者和消费者都会竞争式的申请锁,因此生产者和生产者、消费者和消费者、生产者和消费者之间都存在互斥关系。

生产者和消费者之间为什么会存在同步关系?

  1. 如果让生产者一直生产,那么当生产者生产的数据将容器塞满后,生产者再生产数据就会生产失败。
  2. 反之,让消费者一直消费,那么当容器当中的数据被消费完后,消费者再进行消费就会消费失败。
  3. 虽然这样不会造成任何数据不一致的问题,但是这样会引起另一方的饥饿问题,是非常低效的。我们应该让生产者和消费者访问该容器时具有一定的顺序性,比如让生产者先生产,然后再让消费者进行消费。

注意: 互斥关系保证的是数据的正确性,而同步关系是为了让多线程之间协同起来。

生产者消费者模型优点 

一、提高系统效率

  1. 并行处理:生产者和消费者可以同时运行,充分利用系统资源,实现并行处理。例如,在一个视频处理系统中,生产者可以不断地从摄像头获取视频帧并放入缓冲区,而消费者则可以同时从缓冲区中取出视频帧进行编码和存储,大大提高了处理效率。
  2. 资源分配优化:通过缓冲区的调节,可以使生产者和消费者的工作节奏更加协调,避免生产者过快地生成数据而消费者无法及时处理,或者消费者等待数据的时间过长。这样可以更好地分配系统资源,提高整体性能。

二、解耦生产者和消费者

  1. 独立性:生产者和消费者之间通过缓冲区进行交互,彼此之间的依赖关系降低。生产者不需要知道消费者的具体实现和处理逻辑,只需要将数据放入缓冲区即可。同样,消费者也不需要关心数据是如何生产的,只需要从缓冲区中获取数据进行处理。这种解耦使得系统更加灵活,易于维护和扩展。
  2. 可扩展性:当需要增加生产者或消费者的数量时,只需要对相应的部分进行修改,而不会影响到整个系统的结构。例如,在一个分布式系统中,可以轻松地添加新的生产者节点来提高数据生成的速度,或者增加消费者节点来加快数据处理的能力。

三、增强系统稳定性

  1. 缓冲作用:缓冲区可以起到缓冲数据的作用,避免生产者和消费者之间的直接交互可能带来的冲突和错误。例如,在网络通信中,如果生产者发送数据的速度过快,而消费者接收数据的速度较慢,可能会导致数据丢失或网络拥塞。通过使用缓冲区,可以将数据暂时存储起来,等待消费者有能力处理时再进行读取,从而提高系统的稳定性。
  2. 错误处理:如果生产者或消费者出现故障,缓冲区可以作为一个中间层,暂时存储数据,等待故障恢复后继续处理。这样可以减少因个别组件的故障而导致整个系统崩溃的风险。

四、适用于多种场景

  1. 多线程编程:在多线程环境中,生产者消费者模型是一种常见的设计模式。可以使用线程来实现生产者和消费者,通过共享的缓冲区进行数据交换。这种方式可以有效地利用多核处理器的性能,提高程序的执行效率。
  2. 分布式系统:在分布式系统中,生产者和消费者可以分布在不同的节点上,通过网络进行通信。缓冲区可以是一个分布式队列或数据库,用于存储和传递数据。这种架构可以实现大规模的数据处理和分布式计算,适用于云计算、大数据等领域。
  3. 实时系统:在实时系统中,生产者消费者模型可以用于处理实时数据。生产者可以不断地生成实时数据并放入缓冲区,消费者则可以按照一定的时间间隔从缓冲区中取出数据进行处理,确保系统能够及时响应外部事件。

如果我们在主函数中调用某一函数,那么我们必须等该函数体执行完后才继续执行主函数的后续代码,因此函数调用本质上是一种紧耦合。

对应到生产者消费者模型中,函数传参实际上就是生产者生产的过程,而执行函数体实际上就是消费者消费的过程,但生产者只负责生产数据,消费者只负责消费数据,在消费者消费期间生产者可以同时进行生产,因此生产者消费者模型本质是一种松耦合。

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

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

在多线程编程中,阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。

 

阻塞队列(Blocking Queue)其与普通的队列的区别在于:

  • 当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中放入了元素。
  • 当队列满时,往队列里存放元素的操作会被阻塞,直到有元素从队列中取出。

注意:看到以上阻塞队列的描述,我们很容易想到的就是管道,而阻塞队列最典型的应用场景实际上就是管道的实现。

模拟实现基于阻塞队列的生产消费模型

为了方便理解,我们以单生产者、单消费者为例进行实现,其中的BlockQueue就是生产者消费者模型当中的交易场所,我们可以用C++STL库当中的queue进行实现。

 BlockQueue.hpp 

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

#define NUM 5

template<class T>
class BlockQueue
{
private:
    // 判断队列是否满了
	bool IsFull()
	{
		return _q.size() == _cap;
	}
    // 判空
	bool IsEmpty()
	{
		return _q.empty();
	}
public:
    // 构造函数
	BlockQueue(int cap = NUM)
		: _cap(cap)
	{
		pthread_mutex_init(&_mutex, nullptr);
		pthread_cond_init(&_full, nullptr);
		pthread_cond_init(&_empty, nullptr);
	}
    // 析构函数
	~BlockQueue()
	{
		pthread_mutex_destroy(&_mutex);
		pthread_cond_destroy(&_full);
		pthread_cond_destroy(&_empty);
	}
	//向阻塞队列插入数据(生产者调用)
	void Push(const T& data)
	{
		pthread_mutex_lock(&_mutex);
		while (IsFull())
        {
			//不能进行生产,直到阻塞队列可以容纳新的数据
			pthread_cond_wait(&_full, &_mutex);
		}
		_q.push(data);
		pthread_mutex_unlock(&_mutex);
		pthread_cond_signal(&_empty); //唤醒在empty条件变量下等待的消费者线程
	}
	//从阻塞队列获取数据(消费者调用)
	void Pop(T& data)
	{
		pthread_mutex_lock(&_mutex);
		while (IsEmpty())
        {
			//不能进行消费,直到阻塞队列有新的数据
			pthread_cond_wait(&_empty, &_mutex);
		}
		data = _q.front();
		_q.pop();
		pthread_mutex_unlock(&_mutex);
		pthread_cond_signal(&_full); //唤醒在full条件变量下等待的生产者线程
	}
private:
	std::queue<T> _q;   //阻塞队列
	int _cap;           //阻塞队列最大容器数据个数
	pthread_mutex_t _mutex; // 互斥锁
	pthread_cond_t _full;   // 条件变量
	pthread_cond_t _empty;  // 条件变量
};

  BlockQueue.cpp

#include "BlockQueue.hpp"


void* Producer(void* arg)
{
	BlockQueue<int>* bq = (BlockQueue<int>*)arg;
	//生产者不断进行生产
	while (true){
		sleep(1);
		int data = rand() % 100 + 1;
		bq->Push(data); //生产数据
		std::cout << "Producer: " << data << std::endl;
	}
}
void* Consumer(void* arg)
{
	BlockQueue<int>* bq = (BlockQueue<int>*)arg;
	//消费者不断进行消费
	while (true){
		sleep(1);
		int data = 0;
		bq->Pop(data); //消费数据
		std::cout << "Consumer: " << data << std::endl;
	}
}
int main()
{
	srand((unsigned int)time(nullptr));
	pthread_t producer, consumer;
	BlockQueue<int>* bq = new BlockQueue<int>;
	//创建生产者线程和消费者线程
	pthread_create(&producer, nullptr, Producer, bq);
	pthread_create(&consumer, nullptr, Consumer, bq);

	//join生产者线程和消费者线程
	pthread_join(producer, nullptr);
	pthread_join(consumer, nullptr);
	delete bq;
	return 0;
}
  1. 我们实现的是单生产者、单消费者的生产者消费者模型,因此我们不需要维护生产者和生产者之间的关系,也不需要维护消费者和消费者之间的关系,我们只需要维护生产者和消费者之间的同步与互斥关系即可。
  2. 将BlockingQueue当中存储的数据模板化,方便以后需要时进行复用。
  3. 我们设置BlockingQueue存储数据的上限为5,当阻塞队列中存储了五组数据时生产者就不能进行生产了,此时生产者就应该被阻塞。
  4. 阻塞队列是会被生产者和消费者同时访问的临界资源,因此我们需要用一把互斥锁将其保护起来。
  • 生产者线程要向阻塞队列当中Push数据,前提是阻塞队列里面有空间,若阻塞队列已经满了,那么此时该生产者线程就需要进行等待,直到阻塞队列中有空间时再将其唤醒。
//向阻塞队列插入数据(生产者调用)
	void Push(const T& data)
	{
		pthread_mutex_lock(&_mutex);
		while (IsFull())
        {
			//不能进行生产,直到阻塞队列可以容纳新的数据
			pthread_cond_wait(&_full, &_mutex);
		}
		_q.push(data);
		pthread_mutex_unlock(&_mutex);
		pthread_cond_signal(&_empty); //唤醒在empty条件变量下等待的消费者线程
	}
  • 消费者线程要从阻塞队列当中Pop数据,前提是阻塞队列里面有数据,若阻塞队列为空,那么此时该消费者线程就需要进行等待,直到阻塞队列中有新的数据时再将其唤醒。
//从阻塞队列获取数据(消费者调用)
	void Pop(T& data)
	{
		pthread_mutex_lock(&_mutex);
		while (IsEmpty())
        {
			//不能进行消费,直到阻塞队列有新的数据
			pthread_cond_wait(&_empty, &_mutex);
		}
		data = _q.front();
		_q.pop();
		pthread_mutex_unlock(&_mutex);
		pthread_cond_signal(&_full); //唤醒在full条件变量下等待的生产者线程
	}
  1. 因此在这里我们需要用到两个条件变量一个条件变量用来描述队列为空,另一个条件变量用来描述队列已满。当阻塞队列满了的时候,要进行生产的生产者线程就应该在 full条件变量下进行等待;当阻塞队列为空的时候,要进行消费的消费者线程就应该在 empty条件变量下进行等待。
  2. 不论是生产者线程还是消费者线程,它们都是先申请到锁进入临界区后再判断是否满足生产或消费条件的,如果对应条件不满足,那么对应线程就会被挂起。但此时该线程是拿着锁的,为了避免死锁问题,在调用pthread_cond_wait函数时就需要传入当前线程手中的互斥锁,此时当该线程被挂起时就会自动释放手中的互斥锁,而当该线程被唤醒时又会自动获取到该互斥锁。
  3. 当生产者生产完一个数据后,意味着阻塞队列当中至少有一个数据,而此时可能有消费者线程正在empty条件变量下进行等待,因此当生产者生产完数据后需要唤醒在 empty条件变量下等待的消费者线程。
  4. 当消费者消费完一个数据后,意味着阻塞队列当中至少有一个空间,而此时可能有生产者线程正在full条件变量下进行等待,因此当消费者消费完数据后需要唤醒在 full条件变量下等待的生产者线程。
  • 判断是否满足生产消费条件时不能用if,而应该用while:
    1. pthread_cond_wait函数 是让当前执行流进行等待的函数,是函数就意味着有可能调用失败,调用失败后该执行流就会继续往后执行。
    2. 其次,在多消费者的情况下,当生产者生产了一个数据后如果使用pthread_cond_broadcast函数唤醒消费者,就会一次性唤醒多个消费者,但待消费的数据只有一个,此时其他消费者就被伪唤醒了。

为了避免出现上述情况,我们就要让线程被唤醒后再次进行判断,确认是否真的满足生产消费条件,因此这里必须要用while进行判断。

生产者消费者步调一致

  1. 代码中生产者是每隔一秒生产一个数据,而消费者是每隔一秒消费一个数据
  2. 因此运行代码后我们可以看到生产者和消费者的执行步调是一致的。 

生产者生产的慢,消费者消费的快 

我们可以让生产者每隔一秒进行生产,而消费者不停的进行消费

void* Producer(void* arg)
{
	BlockQueue<int>* bq = (BlockQueue<int>*)arg;
	while (true)
    {
		sleep(1);
		int data = rand() % 100 + 1;
		bq->Push(data); //生产数据
		std::cout << "Producer: " << data << std::endl;
	}
}
void* Consumer(void* arg)
{
	BlockQueue<int>* bq = (BlockQueue<int>*)arg;
	while (true)
    {
		int data = 0;
		bq->Pop(data); //消费数据
		std::cout << "Consumer: " << data << std::endl;
	}
}
  1. 虽然消费者消费的很快,但一开始阻塞队列中是没有数据的
  2. 因此消费者只能在empty条件变量下进行等待,直到生产者生产完一个数据后
  3. 消费者才会被唤醒进而进行消费,消费者消费完这一个数据后又会进行等待,因此生产者和消费者的步调就是一致的。 

生产者生产的快,消费者消费的慢 

我们可以让生产者不停的进行生产,而消费者每隔一秒进行消费

void* Producer(void* arg)
{
	BlockQueue<int>* bq = (BlockQueue<int>*)arg;
	while (true)
    {
		int data = rand() % 100 + 1;
		bq->Push(data); //生产数据
		std::cout << "Producer: " << data << std::endl;
	}
}
void* Consumer(void* arg)
{
	BlockQueue<int>* bq = (BlockQueue<int>*)arg;
	while (true)
    {
		sleep(1);
		int data = 0;
		bq->Pop(data); //消费数据
		std::cout << "Consumer: " << data << std::endl;
	}
}
  1. 此时由于生产者生产的很快,运行代码后一瞬间生产者就将阻塞队列打满了
  2. 此时生产者想要再进行生产就只能在 full条件变量下进行等待,直到消费者消费完一个数据后
  3. 生产者才会被唤醒进而继续进行生产,生产者生产完一个数据后又会进行等待

满足某一条件时再唤醒对应的生产者或消费者 

  1. 我们也可以当阻塞队列当中存储的数据大于队列容量的一半时,再唤醒消费者线程进行消费
  2. 当阻塞队列当中存储的数据小于队列容器的一半时,再唤醒生产者线程进行生产。 
//向阻塞队列插入数据(生产者调用)
void Push(const T& data)
{
	pthread_mutex_lock(&_mutex);
	while (IsFull())
    {
		//不能进行生产,直到阻塞队列可以容纳新的数据
		pthread_cond_wait(&_full, &_mutex);
	}
	_q.push(data);
	if (_q.size() >= _cap / 2)
    {
		pthread_cond_signal(&_empty); //唤醒在empty条件变量下等待的消费者线程
	}
	pthread_mutex_unlock(&_mutex);
}
//从阻塞队列获取数据(消费者调用)
void Pop(T& data)
{
	pthread_mutex_lock(&_mutex);
	while (IsEmpty())
    {
		//不能进行消费,直到阻塞队列有新的数据
		pthread_cond_wait(&_empty, &_mutex);
	}
	data = _q.front();
	_q.pop();
	if (_q.size() <= _cap / 2)
    {
		pthread_cond_signal(&_full); //唤醒在full条件变量下等待的生产者线程
	}
	pthread_mutex_unlock(&_mutex);
}
  1.  我们仍然让生产者生产的快,消费者消费的慢
  2. 运行代码后生产者还是一瞬间将阻塞队列打满后进行等待,但此时不是消费者消费一个数据就唤醒生产者线程
  3. 而是当阻塞队列当中的数据小于队列容器的一半时,才会唤醒生产者线程进行生产

基于计算任务的生产者消费者模型 

  1. 当然,实际使用生产者消费者模型时可不是简单的让生产者生产一个数字让消费者进行打印而已,我们这样做只是为了测试代码的正确性。
  2. 由于我们将BlockingQueue当中存储的数据进行了模板化,此时就可以让BlockingQueue当中存储其他类型的数据。

例如,我们想要实现一个基于计算任务的生产者消费者模型,此时我们只需要定义一个Task类,这个类当中需要包含一个Run成员函数,该函数代表着我们想让消费者如何处理拿到的数据。

#pragma once
#include <iostream>

class Task
{
public:
	Task(int x = 0, int y = 0, int op = 0)
		: _x(x), _y(y), _op(op)
	{}
	~Task()
	{}
	void Run()
	{
		int result = 0;
		switch (_op)
		{
		case '+':
			result = _x + _y;
			break;
		case '-':
			result = _x - _y;
			break;
		case '*':
			result = _x * _y;
			break;
		case '/':
			if (_y == 0){
				std::cout << "Warning: div zero!" << std::endl;
				result = -1;
			}
			else{
				result = _x / _y;
			}
			break;
		case '%':
			if (_y == 0){
				std::cout << "Warning: mod zero!" << std::endl;
				result = -1;
			}
			else{
				result = _x % _y;
			}
			break;
		default:
			std::cout << "error operation!" << std::endl;
			break;
		}
		std::cout << _x << _op << _y << "=" << result << std::endl;
	}
private:
	int _x;
	int _y;
	char _op;
};

此时生产者放入阻塞队列的数据就是一个Task对象,而消费者从阻塞队列拿到Task对象后,就可以用该对象调用Run成员函数进行数据处理。

void* Producer(void* arg)
{
	BlockQueue<Task>* bq = (BlockQueue<Task>*)arg;
	const char* arr = "+-*/%";
	//生产者不断进行生产
	while (true){
		int x = rand() % 100;
		int y = rand() % 100;
		char op = arr[rand() % 5];
		Task t(x, y, op);
		bq->Push(t); //生产数据
		std::cout << "producer task done" << std::endl;
	}
}
void* Consumer(void* arg)
{
	BlockQueue<Task>* bq = (BlockQueue<Task>*)arg;
	//消费者不断进行消费
	while (true){
		sleep(1);
		Task t;
		bq->Pop(t); //消费数据
		t.Run(); //处理数据
	}
}

此后我们想让生产者消费者模型处理某一种任务时,就只需要提供对应的Task类,然后让该Task类提供一个对应的Run成员函数告诉我们应该如何处理这个任务即可。

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

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

相关文章

MySQL Email验证流程详解:从注册到激活!

MySQL Email通知系统搭建教程&#xff01;如何从MySQL发送邮件&#xff1f; MySQL Email验证是一个至关重要的环节&#xff0c;它确保了用户注册过程的安全性和有效性。AokSend将详细介绍从用户注册到MySQL Email激活的完整流程&#xff0c;帮助开发者更好地理解和实现这一功能…

东风汽车将出席第五届中国新能源汽车热管理创新国际峰会

2024第五届中国新能源汽车热管理创新国际峰会将于11月14-15日在上海召开。峰会将汇聚来自全球的行业专家、学者、企业领袖及技术精英&#xff0c;共同探讨新能源汽车热管理领域的最新技术成果和发展趋势。 本次峰会将涵盖整车热管理系统构建、新能源商用车热管理、智能热管理系…

Python OpenCV 影像处理:傅立叶转换

►前言 上篇介绍基于计算影像的梯度&#xff0c;通过在影像中找到梯度值的变化来识别边缘。 本篇将介绍傅立叶变换的基本原理&#xff0c;了解傅立叶变换是如何将影像从空间域转换到频率域的&#xff0c;以及为什么这种转换在影像处理过程中是有用的。以及傅立叶变换的实际应…

9.3 k8s介绍

⼀、编排分类 单机容器编排: docker-compose 容器集群编排: docker swarm、mesosmarathon、kubernetes 应⽤编排: ansible(模块&#xff0c;剧本&#xff0c;⻆⾊) ⼆、系统管理进化史 1. 传统部署时代 早期&#xff0c;各个组织是在物理服务器上运⾏应⽤程序。 由于⽆法限…

getLocation:fail, the permission value is offline verifying

getLocation:fail, the permission value is offline verifying 后端会根据appid和secret生成 签名&#xff0c;前端wx配置时一定用appid来验证签名的正确 本次错误为配置初始化失败&#xff1a;前端与后端的appId不一致&#xff0c;我的失误也

TikTok直播为什么要用独立IP

TikTok直播作为一种受欢迎的社交媒体形式&#xff0c;吸引了越来越多的用户和内容创作者。在进行TikTok直播时&#xff0c;选择使用独立IP地址是一种被广泛推荐的做法。本文将探讨为什么在TikTok直播中更推荐使用独立IP&#xff0c;并解释其优势和应用。 独立IP是指一个唯一的互…

探索Linux项目自动化构建:make/Makefile的使用方法

&#x1f331;博客主页&#xff1a;青竹雾色间 &#x1f331;系列专栏&#xff1a;Linux &#x1f618;博客制作不易欢迎各位&#x1f44d;点赞 ⭐收藏 ➕关注 标题&#xff1a; 使用 Makefile 实现项目自动化构建 - 从零开始学习 Makefile 摘要&#xff1a; Makefile 是一个用…

如何在 OpenCloudOS 上安装 OpenTenBase 数据库

OpenTenBase 是由开放原子开源基金会孵化及运营的开源项目&#xff0c;是一款企业级的分布式 HTAP 数据库&#xff0c;具备高扩展性、商业数据库语法兼容、分布式 HTAP 引擎、多级容灾和多维度资源隔离等能力&#xff0c;目前已经成功应用于金融、医疗、航天等诸多行业的核心业…

Github Coplit和Poe不再订阅,改用Token和LobeChat

优化AI使用方式 1.取消Poe和Github Coplit的年度订阅 今天把200$ 的Poe和100$的Github Coplit的年度订阅取消了&#xff0c;确保到期不会续定&#xff0c;包年用AI的时代&#xff0c;在这里结束了。 2.改用Token购买模式 使用的AI质量必须不变&#xff0c;改用Token的方式&…

【Prometheus】Prometheus安装部署流程详解,配置参数webUI使用方法解析说明

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

二十、Java8新特性

文章目录 引入一、Lambda表达式1.1 快速入门1.2 Lambda语法 二、函数式(Functional)接口2.1 函数式(Functional)接口介绍2.2 Java内置函数式接口 三、方法引用与构造器引用3.1 方法引用3.2 构造器引用 四、强大的Stream API4.1 创建 Stream 的4种方式4.2 Stream 的中间操作4.2.…

PHP图书馆在指尖图书借阅小程序助力全民阅读系统小程序源码

​图书馆在指尖 —— 图书借阅小程序助力全民阅读 &#x1f4da;【开篇&#xff1a;指尖上的知识海洋】&#x1f4da; 在这个快节奏的时代&#xff0c;你是否曾渴望随时随地都能沉浸在书海中&#xff1f;现在&#xff0c;有了图书借阅小程序&#xff0c;图书馆就真正来到了你…

【MySQL08】【死锁】

文章目录 一、前言二、查看事务加锁情况1. 使用 information_schema 数据库中表获取锁信息1.1 INNODB_TRX1.2 INNODB_LOCKS1.3 INNODB_LOCK_WAITS 2. 使用 SHOW ENGIN INNODB STATUS 获取锁信息 三、死锁四、参考内容 一、前言 最近在读《MySQL 是怎样运行的》、《MySQL技术内…

GPU版pytorch安装(win/linux)

参考&#xff1a; Pytorch环境配置——cuda、、cudnn、torch、torchvision对应版本&#xff08;最全&#xff09;及安装方法-CSDN博客 Previous PyTorch Versions | PyTorch 法1&#xff1a;命令安装 如&#xff1a; conda install pytorch2.1.0 torchvision0.16.0 torchau…

Leetcode面试经典150题-63.不同路径II

解法都在代码里&#xff0c;不懂就留言或者私信 class Solution {/**本题是典型的动态规划&#xff0c;但是需要注意的是这个网格中是有障碍的&#xff0c;障碍不能走所以其实还是一样的&#xff0c;计算所有点到(m-1,n-1)有多少种方式&#xff0c;返回(0,0)位置的解就行了 */…

Python | Leetcode Python题解之第393题UTF-8编码验证

题目&#xff1a; 题解&#xff1a; class Solution:def validUtf8(self, data: List[int]) -> bool:MASK1, MASK2 1 << 7, (1 << 7) | (1 << 6)def getBytes(num: int) -> int:if (num & MASK1) 0:return 1n, mask 0, MASK1while num & m…

Python文件自动分类

假如这样的步骤全部手动做下来耗时是6秒&#xff0c;在文件数量不多的情况下&#xff0c;比如10个文件&#xff0c;总共耗时一分钟其实是能够接受的。 但当文件数量特别多时&#xff0c;或者这个操作特别频繁每天都要做十几二十次时&#xff0c;手动操作就会变得耗时又繁琐…

哪款宠物空气净化器能更好的清除浮毛?希喂、范罗士测评

七年前开始养了第一只小猫咪&#xff0c;没想到从此家里就开始一直养&#xff0c;到现在都已经养了5只。之前第一只的时候&#xff0c;就觉得很可爱&#xff0c;而且当时刚毕业&#xff0c;算是一时上头才养了它&#xff0c;后面发现我们经常是要出门上班、出差、游玩&#xff…

冲击大厂算法面试=>链表专题【链表反转之局部反转升级版】

目录标题 多重局部反转之K 个一组翻转链表上代码题解呀实在不会的时候记住 多重局部反转之K 个一组翻转链表 上代码 整个函数通过不断地检查剩余节点数量和进行局部反转&#xff0c;实现了链表的分组反转&#xff0c;最后返回反转后的链表。这种方法有效地利用了额外的 pre 和…

VBA学习(71):Excel VBA 访问带密码保护的Access数据库/用户窗体设置/EXCEL用户+密码登录界面(Access版)

前两天我们分享了一个用户密码登录EXCEL的案例【Excel VBA 用户窗体设置/一步一步代你设计EXCEL用户密码登录界面】&#xff0c;文中提及数据存储在Access中的情况&#xff0c;今天我就来把数据表&#xff08;tb用户&#xff09;移到Access中&#xff0c;修改一下代码&#xff…