Linux学习之路 -- 线程 -- 条件变量与生产消费模型

前面我们已经提过线程互斥的相关概念,但是我们在前文的抢票逻辑中,我们其实很容易发现一个问题。那就是票可能被一直被一个人抢,这里我们就需要引入条件变量的概念。

目录

1、条件变量

<1>线程同步

<2>相关概念

<3>相关的接口

2、生产消费模型

<1>概念的引入

<2>基于条件变量的生产消费模型的示例代码  

<3>多生产多消费模型

<4>小结

3、基于环形队列的生产消费模型

<1>概念引入

<2>具体介绍

<3>具体实现方法 - 信号量

<4>基于信号量的环形队列生产消费模型的代码示例


1、条件变量

<1>线程同步

我们先用一个例子引入条件变量。假设我们现在在学校里有一台冰柜,冰柜上有一把锁,每次冰柜只能被一个人使用。现在有一名同学放了一大杯饮料在冰柜中,其他同学看到冰柜被使用,就只能等第一名同学用完后,把锁交出来。但是这里就可能会出现这种问题,每当第一名用锁把冰柜打开后,把里面的冷饮喝了一口又放回去锁上了,这就导致其他同学压根拿不到锁,无法使用冰柜。学校的管理者,看到这种现象肯定是不允许的,所以就规定了每次使用完冰柜后,需要将钥匙交出,并且到队列尾部重新排队等待锁。这样就能使得每个人都有机会获得冰柜的使用权。

上述例子中的同学我们其实就可以看成是线程,而冰柜就是一个临界资源。上述例子中,修改后冰柜的使用方法,基本做到让不同线程访问同一个临界资源安全的情况下,让不同的线程访问临界资源具有顺序性,我们称这为线程同步

<2>相关概念

如何实现线程的同步,这里我们就需要是用条件变量了。这里再用一个例子来理解一下条件变量是什么东西。

假设先在D向一个箱子内放置苹果,箱子有锁,A、B、C都会在箱子内取东西。现在箱子内没有苹果,需要D去放置苹果。但是D的抢锁能力很差,根本就拿不到锁,而A、B、C抢锁能力很强。这就会导致C一直抢锁,检查箱子内有没有苹果(箱子内没有苹果),而D无法持有锁去放苹果。为了避免这种情况,我们就可以让A、B、C先拿锁,如果打开箱子后没有苹果(有就直接拿),就到一旁的队列排队等待,D放完苹果后,D(用铃铛)通知了再去拿锁开箱,取出苹果。(如下图)

这里的"铃铛+队列"其实就是条件变量。

<3>相关的接口

这里三个接口的使用和互斥锁的使用一样,只不过需要定义的类型变成了pthread_cond_t而已。

pthread_cond_wait其实就和上图中队列对应,表示在该条件变量下等待。pthread_cond_timedwait和pthread_cond_wait的区别就是pthread_cond_timedwait在第三个参数处设置了过期时间,超过该时间后,线程会自动被唤醒,这个接口我们一般不使用。

这两个接口就类似于上面的铃铛,用于唤醒队列中的线程。pthread_cond_signal 表示唤醒一个线程,pthread_cond_broadcast是唤醒cond下等待的所有线程。

上述的所有接口成功都返回零,失败返回错误码

下面简单地示范一下如何使用条件变量

#include <iostream>
#include <vector>
#include <pthread.h>
#include <unistd.h>
#include <string>#define Num 4pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t g_cond = PTHREAD_COND_INITIALIZER;
bool stop_threads = false;void *Slaverwork(void *arg)
{while (true){pthread_mutex_lock(&g_mutex);while (!stop_threads){pthread_cond_wait(&g_cond, &g_mutex);}pthread_mutex_unlock(&g_mutex);break; // Exit the thread}std::string threadname = static_cast<const char *>(arg);std::cout << "线程被唤醒, 唤醒名称为: " << threadname << std::endl;delete[] (char *)arg;return nullptr;
}void *Create_Slaver(void *arg)
{std::vector<pthread_t> *tids = static_cast<std::vector<pthread_t> *>(arg);for (int i = 0; i < Num; i++){pthread_t tid;char *ptr = new char[64];snprintf(ptr, 64, "pthread - %d", i + 1);pthread_create(&tid, nullptr, Slaverwork, ptr);tids->push_back(tid);}return nullptr;
}void MasterStart(std::vector<pthread_t> &tids)
{pthread_t tid;int n = pthread_create(&tid, nullptr, Create_Slaver, &tids);if (n == 0){std::cout << "Master create success" << std::endl;}tids.push_back(tid);
}void Master_awake(std::vector<pthread_t> &tids)
{int count = 6;std::cout << "Master begin work" << std::endl;while (count--){sleep(1);pthread_mutex_lock(&g_mutex);pthread_cond_signal(&g_cond);pthread_mutex_unlock(&g_mutex);}pthread_mutex_lock(&g_mutex);stop_threads = true;pthread_cond_broadcast(&g_cond); pthread_mutex_unlock(&g_mutex);std::cout << "Master work done " << std::endl;
}void MasterJoin(std::vector<pthread_t> &tids)
{for (auto &tid : tids){pthread_join(tid, nullptr);std::cout << "join success" << std::endl;}
}int main()
{std::vector<pthread_t> tid;MasterStart(tid);Master_awake(tid);MasterJoin(tid);return 0;
}

上述代码简单地演示了条件变量地使用方式,需要注意的是,条件变量一定要在加锁后进行等待。

2、生产消费模型

为了更好地理解条件变量地具体作用,这里介绍一下生产消费模型。

<1>概念的引入

其实我们这个生产消费模型在日常生活中其实挺常见,超市就是典型的生产消费模型。

上图的厂商和消费者我们均可以看成是线程,而超市就是临界资源。这里的商品就是数据,超市实际就是用来存数据品的一段内存空间(数据结构)。这里多个消费者和厂商之间关系,就涉及了多线程的同步和互斥问题。在这个模型中存在着三种关系:生产者和生产者、消费者和消费者(厂商就是生产者)、消费者和生产者、。

<1>生产者和生产者 - 互斥(+同步)

生产者之间的关系必然是互斥的关系。举个生活中的例子,当超市在进货时,不可能会让不同的厂商在同一时间,在同一货架上摆货,这样可能会造成货架上的货物混乱。所以生产者之间一定得是互斥关系。为了避免一个商家一直摆货,我们也可以加上同步关系。

<2>消费者和消费者 - 互斥 (+同步)

 消费者之间的关系必然是互斥的。举个生活中的例子,假设现在超市里只有一瓶水,此时有两个想喝水的人同时进入了超市,在这种竞争条件下,两个人都无法获得水。为了避免一个人买完水后,又去拿水结账,一直重复该动作,导致其他人压根无法结账购买货物。我们可以加上同步的关系。

<3>消费者和生产者 - 互斥 + 同步

 消费者和生产者之间必须同时拥有这两种关系。这其实很好理解,厂商在上架时,消费者不能去取货物,否则可能会导致货物录入超市系统时出现数目对不上的情况。而厂商不能一直摆货,消费者也不能一直消费货物,这两者的行为必须是同步的。

总结一下,这个超市本质上就是商品的一个缓冲地带。生产消费模型能够为我们提供更好的并发度,将生产和消费的过程进行解耦合。

<2>基于条件变量的生产消费模型的示例代码  

在正式编写代码前,我们需要知道的是,了解一下阻塞队列的概念。阻塞队列是一种特殊的队列,当队列为空时,获取队列元素的操作将会被阻塞。当队列满了时,向队列中存放数据的操作也会被阻塞。(这里的阻塞队列就对应着超市)为了实现这种操作,我们就需要应用到条件变量。

 <1>单生产单消费模型

ThreadMode.hpp

#ifndef __THREAD_HPP__
#define __THREAD_HPP__#include <iostream>
#include <string>
#include <unistd.h>
#include <functional>
#include <pthread.h>namespace ThreadModule
{template<typename T>using func_t = std::function<void(T)>;// typedef std::function<void(const T&)> func_t;template<typename T>class Thread{public:void Excute(){_func(_data);}public:Thread(func_t<T> func, T data, const std::string name="none-name")//右值: _func(func), _data(data), _threadname(name), _stop(true){}static void *threadroutine(void *args) // 类成员函数,形参是有this指针的!!{Thread<T> *self = static_cast<Thread<T> *>(args);self->Excute();return nullptr;}bool Start(){int n = pthread_create(&_tid, nullptr, threadroutine, this);if(!n){_stop = false;return true;}else{return false;}}void Detach(){if(!_stop){pthread_detach(_tid);}}void Join(){if(!_stop){pthread_join(_tid, nullptr);}}std::string name(){return _threadname;}void Stop(){_stop = true;}T& Data(){return _data;}~Thread() {}private:pthread_t _tid;std::string _threadname;T _data;  // 为了让所有的线程访问同一个全局变量func_t<T> _func;bool _stop;};
}#endif

BlockQueue.hpp

#include <iostream>
#include <vector>
#include <queue>template <typename T>
class blockqueue
{typedef int data;
private:bool isfull(){return self.size() == capacity;}public:blockqueue(int cap = 20) : capacity(cap){pthread_mutex_init(&_glock, nullptr);pthread_cond_init(&_product_cond, nullptr);pthread_cond_init(&_consume_cond, nullptr);}void Enqueue(const T& in){// 1.加锁pthread_mutex_lock(&_glock);while(isfull()){_product_wait_num++;pthread_cond_wait(&_product_cond, &_glock);//存在伪唤醒情况(消费一个数据,却唤醒了多个生产线程),可以将外部判断语句变为while_product_wait_num--;}self.push(std::move(in));//1.1唤醒消费线程if(_consume_wait_num > 0)//有消费者在等,才需要唤醒pthread_cond_signal(&_consume_cond);// 2.解锁pthread_mutex_unlock(&_glock);}void Popqueue(T* out){// 1.加锁pthread_mutex_lock(&_glock);while(self.empty())//避免多个线程在被唤醒时,只有一个线程持有锁,其他线程在锁下等待失败,继续向下执行时,出现问题。{_consume_wait_num++;pthread_cond_wait(&_consume_cond, &_glock);//1.让线程进入休眠,当被唤醒后,需要重新持有锁后方能从该位置继续执行。_consume_wait_num--;}*out =  self.front();self.pop();//1.1唤醒生产的线程if(_product_wait_num > 0)//有生产者在等,才需要唤醒pthread_cond_signal(&_product_cond);// 2.解锁pthread_mutex_unlock(&_glock);}~blockqueue(){pthread_mutex_destroy(&_glock);pthread_cond_destroy(&_product_cond);pthread_cond_destroy(&_consume_cond);}private:int capacity; // 容量std::queue<T> self;pthread_mutex_t _glock; // 互斥锁pthread_cond_t _product_cond; // 生产者条件变量pthread_cond_t _consume_cond; // 消费者条件变量int _product_wait_num;//等待线程数int _consume_wait_num;};

Main.cc

#include <iostream>
#include <string>
#include <vector>
#include "ThreadMode.hpp"
#include "BlockQueue.hpp"
#include "unistd.h"
using namespace ThreadModule;typedef int data;
using Blockqueue_t = blockqueue<data>*;
pthread_mutex_t _data_lock = PTHREAD_MUTEX_INITIALIZER;data cnt = 100;
void Product(Blockqueue_t ptr)
{while (1){sleep(1);pthread_mutex_lock(&_data_lock);ptr->Enqueue(cnt);std::cout << "product data is :" << cnt << std::endl;cnt--;pthread_mutex_unlock(&_data_lock);}
}
void Consume(Blockqueue_t ptr)
{while (1){sleep(2);data sum;ptr->Popqueue(&sum);std::cout << "comsume data is :        " << sum << std::endl;}
}
void StartComm(std::vector<Thread<Blockqueue_t>> &thread, int num, Blockqueue_t ptr, func_t<Blockqueue_t> fun)
{for (int i = 0; i < num; i++){std::string name = "thread - " + std::to_string(i + 1);thread.emplace_back(fun, ptr, name);thread.back().Start();}
}
void StartProducter(std::vector<Thread<Blockqueue_t>> &thread, int num, Blockqueue_t ptr)
{StartComm(thread, num, ptr, Product);
}
void StartConsumer(std::vector<Thread<Blockqueue_t>> &thread, int num, Blockqueue_t ptr)
{StartComm(thread, num, ptr, Consume);
}
void Wait(std::vector<Thread<Blockqueue_t>> &threads)
{for (auto &e : threads){e.Join();}
}
int main()
{blockqueue<data> *ptr = new blockqueue<data>;std::vector<Thread<Blockqueue_t>> threads;StartProducter(threads, 1, ptr);//第二个参数确定生产者数量StartConsumer(threads, 1, ptr);//第二个参数确定消费者数量Wait(threads);return 0;
}

 运行结果

<3>多生产多消费模型

在实际场景下,单生产单消费的模型其实并不多见,绝大部分是多生产多消费模型。多生产多消费模型的代码其实和上面单消费单生产模型代码一致,我们只需要修改main函数中的参数即可。这里我在阻塞队列中存放的均是int类型的数据,正常情况下,这里面存放的是一个一个的任务。由消费者执行任务,生产者获取任务,这里就不演示具体做法了。

<4>小结

这个模型的优点在于能够提供较好的并发度,虽然临界资源只能由一个线程进行访问,但是拿到任务以后,不同的线程可以并发的处理任务。

3、基于环形队列的生产消费模型

<1>概念引入

上面我们介绍了普通的生产消费模型,在上一个模型中。我们其实可以发现,对于队列的使用(临界资源)其实是不怎么高效的,因为我把队列看成一个整体对数据进行保存。前面我们提到过信号量的概念,该篇文章中,我们提到过临界资源可以被划分称多块使用,也可以被整体使用。既然这里使用的是将临界资源看成整体的方法,那么也应该可以将该资源看成多块进行使用。下面介绍一下基于环形队列的生产消费模型。

<2>具体介绍

环形队列本质上是一个数组(也可以是其他数据结构),这里不对它的具体实现作描述,简单叙述一下原理即可。环形队列将整体资源分成多块,不用像上面一样进行整体访问,可以做到生产和消费并发执行。

在环形队列中,我们需要使用两个指针,一个指向消费者下标,一个指向生产者下标。在这两个下标指向同一个位置时,可能会存在两种情况:

1、环形队列为满

在这种情况下,我们就要确保消费者先走,不能让生产者先动,否则会造成数据覆盖的现象。

2、环形队列为空

在这种情况下,我们需要确保生产者先走,只有生产者走了,消费者才有东西可以消费。

除开两个下标在同一个位置的情况下,其余情况都是两个指正指向不同的位置。在这种情况下,我们就可以让生产者和消费者并发执行。不过需要注意的是,生产者一定不能超过消费者一圈,并且消费者不能超过生产者,否则会出现问题。

<3>具体实现方法 - 信号量

为了实现上述功能,我们需要引入信号量。前面我们提到信号量就是用来描述临界资源多少的计数器,当信号量大于零时,我们申请临界资源就肯定可以成功,如果等于零,就肯定无法申请成功。这个特性可以让我们省去很多对临界资源的判断。

下面我使用伪代码来简单描述一下环形队列的具体实现过程。

对于生产者来说,空间是比较重要的,而对于消费者来说,数据是比较重要的,而空间和数据都属于资源。所以这里我们需要申请两个信号量,一个用于描述空间资源,一个用于描述数据资源。对于生产者来说,我们首先就要申请空间信号量,由于我们将空间信号量设置成了大于零的初始值,而数据信号量设置成了等于零的初始值。所以在这种情况下,生产者肯定比消费者先走,而消费者只能阻塞在P操作中。在生产者执行完相关代码后,会对数据信号量做V操作,此时消费者申请数据信号量成功,开始向下执行。当执行完所有代码后,会对空间信号量做V操作(这个过程其实就是生产者生产完后,提醒消费者,消费者消费完后提醒生产者,这个过程形成完美闭环)。当两个信号量都不等于0或10时,消费者和生产者就可以同步运行。

<4>基于信号量的环形队列生产消费模型的代码示例

下面对引用上一个模型的代码,简单写一个基于信号量的环形队列生产消费模型。在此之前,我们需要介绍几个信号量的相关接口,方便大家理解

1、sem_init

首先我们需要定义一个sem_t变量,将该变量地址用sem_init进行初始。pshared为零表示,信号量只在线程之间进行共享,如果大于零表示能在进程之间共享。第三个参数表示信号量需要设定的值。
2、sem_destroy

 当我们不需要使用信号量时,就可以使用sem_destroy进行销毁。成功返回0,错误返回-1,错误码被设置。

3、sem_wait

sem_wait用于减少信号量,当信号量减少到小于等于零,线程会阻塞。上图第二个接口在出现信号量小于等于零时,不会阻塞,会直接出错返回,第三个接口则是在特定时间内返回。这里我们一般就使用第一个接口,模拟P操作。成功返回0,失败返回-1.

4、sem_post

sem_post用于增加信号量,用于模拟V操作。成功返回0,失败返回-1.

需要注意的是,以上的所有接口,均需链接pthread原生线程库

示例代码:

ThreadMode.hpp(该文件较上文有些许修改)

#ifndef __THREAD_HPP__
#define __THREAD_HPP__#include <iostream>
#include <string>
#include <unistd.h>
#include <functional>
#include <pthread.h>namespace ThreadModule
{template<typename T>using func_t = std::function<void(T,std::string name)>;// typedef std::function<void(const T&)> func_t;template<typename T>class Thread{public:void Excute(){_func(_data,_threadname);}public:Thread(func_t<T> func, T data, const std::string name="none-name")//右值: _func(func), _data(data), _threadname(name), _stop(true){}static void *threadroutine(void *args) // 类成员函数,形参是有this指针的!!{Thread<T> *self = static_cast<Thread<T> *>(args);self->Excute();return nullptr;}bool Start(){int n = pthread_create(&_tid, nullptr, threadroutine, this);if(!n){_stop = false;return true;}else{return false;}}void Detach(){if(!_stop){pthread_detach(_tid);}}void Join(){if(!_stop){pthread_join(_tid, nullptr);}}std::string name(){return _threadname;}void Stop(){_stop = true;}T& Data(){return _data;}~Thread() {}private:pthread_t _tid;std::string _threadname;T _data;  // 为了让所有的线程访问同一个全局变量func_t<T> _func;bool _stop;};
}#endif

RingQueue.hpp

#include <vector>
#include <iostream>
#include <string>
#include <semaphore.h>template <class T>
class RingQueue
{
private:void P(sem_t &sem){sem_wait(&sem);}void V(sem_t &sem){sem_post(&sem);}void Lock(pthread_mutex_t &lock){pthread_mutex_lock(&lock);}void Unlock(pthread_mutex_t &lock){pthread_mutex_unlock(&lock);}public:RingQueue(int cap = 10) : _cap(cap)//初始值默认设置成10{pthread_mutex_init(&_productor_mutex, nullptr);pthread_mutex_init(&_consumer_mutex, nullptr);sem_init(&room, 0, 10);sem_init(&data, 0, 0);_product_index = 0;_consume_index = 0;queue.resize(cap);}void Enqueue(const T &date){//申请信号量P(room);//由于PV操作本身就是原子的,所以这里是不用加锁//加锁,保护临界资源Lock(_productor_mutex);queue[_product_index++] = date;_product_index %= _cap;Unlock(_productor_mutex);V(data);}void Popqueue(T *date){P(data);Lock(_consumer_mutex);*date = queue[_consume_index];queue[_consume_index++] = T();(_consume_index) %= _cap;Unlock(_consumer_mutex);V(room);}~RingQueue(){sem_destroy(&room);sem_destroy(&data);pthread_mutex_destroy(&_productor_mutex);pthread_mutex_destroy(&_consumer_mutex);}private:int _cap;std::vector<T> queue;// 空间信号量sem_t room;sem_t data;// 空间信号量下标int _product_index;int _consume_index;// 多线程需要加锁pthread_mutex_t _productor_mutex;pthread_mutex_t _consumer_mutex;
};

Main.cc(较上个示例代码有一定改动)

#include <iostream>
#include <string>
#include <vector>
#include "ThreadMode.hpp"
#include "unistd.h"
#include "RingQueue.hpp"
using namespace ThreadModule;typedef int data;
using Ringqueue_t = RingQueue<data>*;
pthread_mutex_t _data_lock = PTHREAD_MUTEX_INITIALIZER;data cnt = 100;
void Product(Ringqueue_t ptr,std::string name)
{while (1){sleep(1);pthread_mutex_lock(&_data_lock);ptr->Enqueue(cnt);std::cout << "product data is :" << cnt << "---[" << name << "]" <<  std::endl;cnt--;pthread_mutex_unlock(&_data_lock);}
}
void Consume(Ringqueue_t ptr,std::string name)
{while (1){sleep(2);data sum;ptr->Popqueue(&sum);std::cout << "comsume data is : " << sum << "---[" << name << "]" << std::endl;}
}
void StartComm(std::vector<Thread<Ringqueue_t>> &thread, int num, Ringqueue_t ptr, func_t<Ringqueue_t> fun,const std::string cname)
{for (int i = 0; i < num; i++){std::string name = "thread-" + std::to_string(i + 1) + " + " + cname;thread.emplace_back(fun, ptr, name);}
}
void StartProducter(std::vector<Thread<Ringqueue_t>> &thread, int num, Ringqueue_t ptr)
{StartComm(thread, num, ptr, Product, "Productor");
}
void StartConsumer(std::vector<Thread<Ringqueue_t>> &thread, int num, Ringqueue_t ptr)
{StartComm(thread, num, ptr, Consume,"Consumer");
}
void Wait(std::vector<Thread<Ringqueue_t>> &threads)
{for (auto &e : threads){e.Join();}
}
void StartAll(std::vector<Thread<Ringqueue_t>>& thread)
{for(auto& e : thread){e.Start();}
}
int main()
{RingQueue<data> *ptr = new RingQueue<data>;std::vector<Thread<Ringqueue_t>> threads;StartProducter(threads, 4, ptr);StartConsumer(threads, 5, ptr);StartAll(threads);Wait(threads);return 0;
}

运行结果:

以上就是所有内容,文中如有不对之处,还望各位大佬指正,谢谢!!!

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

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

相关文章

YOLO格式数据集转为COCO数据集(简单粗暴)

最近需要用的coco格式的数据集&#xff0c;但是在网上找的很多 毕竟麻烦&#xff0c;简单记录一下&#xff01; 1、调整目录结构&#xff08;以GC10-DET数据集为例&#xff09; YOLO格式数据集目录结构如下&#xff1a; 简单来说就是images文件夹里面有train、val、test三个文…

rk3588s android12 自启动 C程序

首先是 拷贝 TEST程序 到android跟文件系统看看能否成功。 首先将需要 在android 系统中 启动的 TEST 程序,拷贝到虚拟机。 这个程序 然后需要拷贝到 android 的文件系统中。 我是这样做的。 改的是这个文件。 然后, 编译+烧写测试。 发现, 在android 系…

国内车市销量激增,理想成功超越BBA

文/王俣祺 导语&#xff1a;随着“金九银十”的到来&#xff0c;国内汽车市场迎来了一个充满活力的开局。乘用车市场的销量已经迎来新的突破&#xff0c;彰显出中国汽车市场的韧性和潜力。尤为引人注目的是&#xff0c;新能源汽车销量同样激增&#xff0c;成为推动市场增长的重…

字节豆包C++一面-面经总结

talk is cheap show me the code lc206&#xff1a;链表反转&#xff1a;给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。 class Solution { public:ListNode* reverseList(ListNode* head) {if(headnullptr||!head->next)return head…

RHCSA认证-Linux(RHel9)-Linux入门

文章目录 概要一、创建、查看和编辑⽂本1.1 输出重定向1.2 vim编辑器1.3 shell 变量1.5 获取帮助 二、管理本地用户和组2.1 描述用户2.2 切换用户和赋权2.3 用户管理2.4 用户组管理2.5 密码策略 三、控制文件访问3.1 列出文件和文件权限3.2 更改文件权限和拥有者3.3 控制默认权…

Node.js官网无法正常访问时安装NodeJS的方法

目录 一、使用 nvm 进行安装二、通过阿里云开源镜像站进行安装 一、使用 nvm 进行安装 此时如果直接使用 nvm install 命令进行安装会报错&#xff1a; nvm install 16.14.0Could not retrieve https://nodejs.org/dist/latest/SHASUMS256.txt. Get “https://nodejs.org/dis…

STM32精确控制步进电机

目的&#xff1a;学习使用STM32电机驱动器步进电机&#xff0c;进行电机运动精确控制。 测试环境&#xff1a; MCU主控芯片STM32F103RCT6 &#xff1b;A4988步进电机驱动器模块&#xff1b; 微型2相4线步…

AMP网站如何适配提升SEO效果?

AMP网站如何适配提升SEO效果&#xff1f; 很多网站本来就有了 PC 和 M 站&#xff0c;现在多来了个 AMP&#xff0c;如何适配提升网站SEO效果呢&#xff1f;有两种适配形式&#xff1a; 1.PC AMP 这种形式比较简单&#xff0c;只需要配置好 amphtml 和 canonical 即可。关系…

论文阅读《Co-clustering for Federated Recommender System》

论文概况 本文是2024 WWW的一篇联邦推荐论文&#xff0c;提出了一个基于特定类别物品相似度来进行聚类的联邦推荐框架。 Introduction 分析了经典聚类技术KMeans在联邦推荐设置中的不足&#xff0c;提出了一种新的共聚类联邦推荐机制CoFedRec&#xff0c;该机制在每个通信回合…

堆排序算法详解:原理与Python实现

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐&#xff1a;「storm…

后端回写前端日期格式化

问题 不进行格式化处理&#xff0c;就会导致传递的字符串很奇怪 解决方案 注解&#xff08;字段&#xff09; <dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>2.9.2</…

【unity进阶知识1】最详细的单例模式的设计和应用,继承和不继承MonoBehaviour的单例模式,及泛型单例基类的编写

文章目录 前言一、不使用单例二、普通单例模式1、单例模式介绍实现步骤&#xff1a;单例模式分为饿汉式和懒汉式两种。 2、不继承MonoBehaviour的单例模式2.1、基本实现2.2、防止外部实例化对象2.3、最终代码 3、继承MonoBehaviour的单例模式3.1、基本实现3.2、自动创建和挂载单…

QT窗口无法激活弹出问题排查记录

问题背景 问题环境 操作系统: 银河麒麟V10SP1qt版本 : 5.12.12 碰见了一个问题应用最小化,然后激活程序窗口无法弹出 这里描述一下代码的逻辑,使用QLocalServer实现一个单例进程,具体的功能就是在已存在一个程序A进程时,再启动这个程序A,新的程序A进程会被杀死,然后激活已存…

linux信号| 学习信号三步走 | 学习信号需要打通哪些知识脉络?

前言: 本节内容主要讲解linux下信号的预备知识以及信号的概念&#xff0c; 信号部分我们将会分为几个阶段进行讲解&#xff1a;信号的概念&#xff0c; 信号的产生&#xff0c; 信号的保存。本节主要讲解信号 ps:本节内容适合学习了进程相关概念的友友们进行观看哦 目录 什么是…

电脑上数据丢了怎么找回来 Win系统误删文件如何恢复

无论是在工作中&#xff0c;还是生活中&#xff0c;电脑都是不可缺少的重要工具&#xff0c;尤其是在工作中&#xff0c;电脑不仅可以高效的完成工作&#xff0c;还可以存储工作中的重要资料。不过在使用电脑的时候&#xff0c;也会遇到数据丢失的情况。针对这一问题&#xff0…

水面巡检船垃圾漂浮物检测系统源码分享

水面巡检船垃圾漂浮物检测检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of …

在GPU计算型实例中安装Tesla驱动超详细过程

摘要&#xff1a;在深度学习、AI等通用计算业务场景或者OpenGL、Direct3D、云游戏等图形加速场景下&#xff0c;安装了Tesla驱动的GPU才可以发挥高性能计算能力&#xff0c;或提供更流畅的图形显示效果。如果您在创建GPU计算型实例&#xff08;Linux&#xff09;时未同时安装Te…

Linux服务器安装Anaconda环境

Linux浪潮云服务器安装Anaconda环境 读研之后在导师的帮助下&#xff0c;获得了浪潮的一台公共云服务器。以后做实验跑代码就可以使用云服务器上的虚拟环境了。减少了自己笔记本的压力。在创建并保存完成镜像环境之后。最重要的就是安装好深度学习需要的Anaconda环境&#xff0…

“类型名称”在Go语言规范中的演变

Go语言规范&#xff08;The Go Programming Language Specification&#xff09;[1]是Go语言的核心文档&#xff0c;定义了该语言的语法、类型系统和运行时行为。Go语言规范的存在使得开发者在实现Go编译器时可以依赖一致的标准&#xff0c;它确保了语言的稳定性和一致性&#…

python -tkinter

在Button的command绑定中&#xff0c;如果给的一个函数&#xff0c;则表示执行一次。 import tkinter as Tkfrom tkinter import messageboxi 0def myLabel():global root,is Tk.Label(root,text"the import thing is :" )s.pack()root Tk.Tk()def fun1():if mess…