【Linux】线程——线程池、线程池的实现、线程安全的线程池、单例模式的概念、饿汉和懒汉模式、互斥锁、条件变量、信号量、自旋锁、读写锁

文章目录

  • Linux线程
    • 7. 线程池
      • 7.1 线程池介绍
      • 7.2 线程池的实现
      • 7.3 线程安全的线程池
        • 7.3.1 单例模式的概念
        • 7.3.2 饿汉和懒汉模式
    • 8. 常见锁使用汇总
      • 8.1 互斥锁(Mutex)
      • 8.2 条件变量(Condition Variable)
      • 8.3 信号量(Semaphore)
      • 8.4 自旋锁(Spin Lock)
      • 8.5 读写锁(Read-Write Lock)

Linux线程

7. 线程池

  线程池是一种多线程编程中的技术和概念。

  它是一种线程使用模式。是一组预先创建好的线程集合,这些线程处于等待状态,随时准备接受任务并执行。

在这里插入图片描述

  

7.1 线程池介绍

为什么使用线程池

  线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。

  

线程池的应用场景

  (1)需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。

  (2)对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。

  (3)接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。 突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误。

  

使用线程池的优点

  (1)提高性能:避免了频繁创建和销毁线程的开销,因为线程的创建和销毁是比较耗时的操作。

  (2)控制资源:可以限制线程的数量,防止过多的线程竞争系统资源,导致系统性能下降甚至崩溃。

  (3)提高响应性:能够更快地响应新的任务请求,因为线程已经准备好,无需等待线程创建。

  

7.2 线程池的实现

线程池示例

  (1)创建固定数量线程池,循环从任务队列中获取任务对象。

  (2)获取到任务对象后,执行任务对象中的任务接口。

  

执行任务:

#pragma once
#include <iostream>
#include <string>std::string opers="+-*/%";enum{DivZero=1,ModZero,Unknown
};class Task
{
public:Task(){}Task(int x,int y,char op):_data1(x),_data2(y),_oper(op),_result(0),_exitcode(0){}void run(){switch (_oper){case '+':_result=_data1+_data2;break;case '-':_result=_data1-_data2;break;case '*':_result=_data1*_data2;break;case '/':{if(_data2==0) _exitcode=DivZero;else _result=_data1/_data2;}break;case '%':{if(_data2==0) _exitcode=ModZero;else _result=_data1%_data2;}break;default:_exitcode=Unknown;break;}}//Task对象重载运算符(),()直接进行run函数void operator()(){run();}std::string GetResult(){std::string r=std::to_string(_data1);r+=_oper;r+=std::to_string(_data2);r+="=";r+=std::to_string(_result);r+="[code: ";r+=std::to_string(_exitcode);r+="]";return r;}std::string GetTask(){std::string r=std::to_string(_data1);r+=_oper;r+=std::to_string(_data2);r+="=?";return r;}~Task(){}private: int _data1;int _data2;char _oper;int _result;int _exitcode;
};

  

线程池:

#pragma once#include <iostream>
#include <vector>
#include <queue>
#include <string>
#include <pthread.h>
#include <unistd.h>
#include "Task.hpp"struct ThreadData
{pthread_t tid;std::string name;
};static const int defaultnum=5; //默认线程数量//实现我们的线程池
template<class T>
class ThreadPool
{
public:void Lock(){pthread_mutex_lock(&_mutex);}void Unlock(){pthread_mutex_unlock(&_mutex);}void Wakeup(){pthread_cond_signal(&_cond);}void ThreadSleep(){pthread_cond_wait(&_cond,&_mutex);}bool IsQueueEmpty() {return _tasks.empty();}public://注意我们线程调用的函数要求参数和返回值都是void*//但是handler在类中默认有this指针->参数不匹配,可以bind或者声明static或放在类外static void *Handler(/*ThreadPool *this,*/void *args){ThreadPool<T> *tp=static_cast<ThreadPool<T>*>(args);while(true){tp->Lock();while(tp->IsQueueEmpty()) //判断任务是否为空{tp->ThreadSleep(); //条件变量}T t=tp->Pop(); //取出任务tp->Unlock();t(); //处理任务 std::cout<<" run, "<<"result: "<< t.GetResult()<<std::endl;            }return nullptr;}void Start() //启动线程池{int num=_threads.size();for(int i=0;i<num;i++){_threads[i].name="thread-"+std::to_string(i+1);pthread_create(&(_threads[i].tid),nullptr,Handler,this);}}void Push(const T &t) //向任务队列放入任务{Lock();_tasks.push(t); //放入任务Wakeup(); //唤醒线程Unlock();}T Pop() //取出任务{T t=_tasks.front();_tasks.pop();return t;}ThreadPool(int num=defaultnum):_threads(num){pthread_mutex_init(&_mutex,nullptr);pthread_cond_init(&_cond,nullptr);} ~ThreadPool(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond);}private:   std::vector<ThreadData> _threads; //线程池std::queue<T> _tasks; //任务队列pthread_mutex_t _mutex; //锁pthread_cond_t _cond; //条件变量
};

  

运行函数:

#include <iostream>
#include "ThreadPool.hpp"int main()
{ThreadPool<Task> *tp=new ThreadPool<Task>(5);tp->Start();srand(time(nullptr) ^ getpid());    while(true){//1. 构建任务int x=rand()%10+1;usleep(10);int y=rand()%5;char op=opers[rand()%opers.size()];Task t(x,y,op);tp->Push(t);//2. 交给线程池处理std::cout<<"main thread make task: "<<t.GetTask()<<std::endl;sleep(1);}return 0;
}

  

在这里插入图片描述

  

7.3 线程安全的线程池

7.3.1 单例模式的概念

  单例模式是一种常见的软件设计模式

  概念:单例模式确保一个类只有一个实例存在,并提供一个全局访问点来获取该实例。

  

特点包括

  唯一性:保证一个类在整个应用程序中只有一个实例。

  全局访问:提供了一种全局访问这个唯一实例的方式,方便在程序的任何地方使用。

  延迟初始化:通常实例的创建是延迟的,即在首次使用时才创建实例,以提高性能和资源利用率。

  

单例模式的优点

  节省系统资源:避免了频繁创建和销毁对象带来的资源消耗。

  统一管理:对唯一的实例进行集中管理和控制,方便维护和修改。

  保证一致性:在整个应用中,对于共享的数据或状态,通过单例模式可以保证其一致性。

  

7.3.2 饿汉和懒汉模式

  饿汉模式:

  在类加载时就创建单例对象。

  优点:线程安全, 因为对象在类加载时就已经创建好了,不存在多线程并发创建的问题。简单直接,实现较为简单。

  缺点:无论是否使用,对象都会在类加载时创建,可能会造成一定的资源浪费。

template <typename T>
class Singleton 
{static T data;public:static T* GetInstance() {return &data;}
};

  

  懒汉模式:

  在第一次使用时才创建单例对象。

  优点:延迟对象的创建,只有在真正需要时才创建,节省了资源

  缺点:线程不安全,在多线程环境下可能会创建多个实例。需要额外的处理来保证线程安全,增加了实现的复杂性。

template <typename T>
class Singleton 
{static T* inst;public:static T* GetInstance() {if (inst == NULL) {inst = new T();}return inst;}
};

  

懒汉模式实现线程安全的线程池

线程安全的线程池:

#pragma once#include <iostream>
#include <vector>
#include <queue>
#include <string>
#include <pthread.h>
#include <unistd.h>
#include "Task.hpp"struct ThreadData
{pthread_t tid;std::string name;
};static const int defaultnum=5; //默认线程数量//实现我们的线程池
template<class T>
class ThreadPool
{
public:void Lock(){pthread_mutex_lock(&_mutex);}void Unlock(){pthread_mutex_unlock(&_mutex);}void Wakeup(){pthread_cond_signal(&_cond);}void ThreadSleep(){pthread_cond_wait(&_cond,&_mutex);}bool IsQueueEmpty() {return _tasks.empty();}std::string GetThreadName(pthread_t tid){for (const auto &ti : _threads){if (ti.tid == tid)return ti.name;}return "None";}public://注意我们线程调用的函数要求参数和返回值都是void*//但是handler在类中默认有this指针->参数不匹配,可以bind或者声明static或放在类外static void *Handler(/*ThreadPool *this,*/void *args){ThreadPool<T> *tp=static_cast<ThreadPool<T>*>(args);std::string name = tp->GetThreadName(pthread_self());while(true){tp->Lock();while(tp->IsQueueEmpty()) //判断任务是否为空{tp->ThreadSleep(); //条件变量}T t=tp->Pop(); //取出任务tp->Unlock();t(); //处理任务 std::cout<<name<<" run, "<<"result: "<< t.GetResult()<<std::endl;            }return nullptr;}void Start() //启动线程池{int num=_threads.size();for(int i=0;i<num;i++){_threads[i].name="thread-"+std::to_string(i+1);pthread_create(&(_threads[i].tid),nullptr,Handler,this);}}void Push(const T &t) //向任务队列放入任务{Lock();_tasks.push(t); //放入任务Wakeup(); //唤醒线程Unlock();}T Pop() //取出任务{T t=_tasks.front();_tasks.pop();return t;}static ThreadPool<T> *GetInstance() //获取单例对象{if(nullptr==_tp) //创建单例对象后,不会再有申请和释放锁的操作{pthread_mutex_lock(&_lock); //保护临界资源if(_tp==nullptr){std::cout<<"singleton create done"<<std::endl;_tp=new ThreadPool<T>();}pthread_mutex_unlock(&_lock);}return _tp;}private:ThreadPool(const ThreadPool<T>&)=delete;const ThreadPool<T>& operator=(const ThreadPool<T>&)=delete;ThreadPool(int num=defaultnum):_threads(num){pthread_mutex_init(&_mutex,nullptr);pthread_cond_init(&_cond,nullptr);} ~ThreadPool(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond);}private:   std::vector<ThreadData> _threads; //线程池std::queue<T> _tasks; //任务队列pthread_mutex_t _mutex; //锁pthread_cond_t _cond; //条件变量static ThreadPool<T> *_tp; //获取单例指针static pthread_mutex_t _lock; //锁
};template<class T>
ThreadPool<T> *ThreadPool<T>::_tp=nullptr;template<class T>
pthread_mutex_t ThreadPool<T>::_lock=PTHREAD_MUTEX_INITIALIZER;

  

运行函数:

#include <iostream>
#include "ThreadPool.hpp"int main()
{//ThreadPool<Task> *tp=new ThreadPool<Task>(5);//tp->Start();sleep(2); //懒汉模式ThreadPool<Task>::GetInstance()->Start();srand(time(nullptr) ^ getpid());    while(true){//1. 构建任务int x=rand()%10+1;usleep(10);int y=rand()%5;char op=opers[rand()%opers.size()];Task t(x,y,op);//tp->Push(t);ThreadPool<Task>::GetInstance()->Push(t);//2. 交给线程池处理std::cout<<"main thread make task: "<<t.GetTask()<<std::endl;sleep(1);}return 0;
}

  

在这里插入图片描述
  

STL中的容器是否是线程安全的?

  不是。原因是 STL 的设计初衷是将性能挖掘到极致,而一旦涉及到加锁保证线程安全,会对性能造成巨大的影响。

  而且对于不同的容器,加锁方式的不同,性能可能也不同(例如hash表的锁表和锁桶)。
因此 STL 默认不是线程安全。如果需要在多线程环境下使用,往往需要调用者自行保证线程安全。

  

智能指针是否是线程安全的?

  对于 unique_ptr,由于只是在当前代码块范围内生效,因此不涉及线程安全问题。

  对于 shared_ptr,多个对象需要共用一个引用计数变量,所以会存在线程安全问题。但是标准库实现的时候考虑到了这个问题,基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数。

  

8. 常见锁使用汇总

8.1 互斥锁(Mutex)

  确保在同一时刻只有一个线程能够访问被保护的资源。

  例如,多个线程同时操作一个共享的全局变量时,使用互斥锁来保证数据的一致性。

  

  初始化互斥锁

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

作用:初始化一个互斥锁。

参数:
  mutex:指向要初始化的互斥锁的指针。

  attr:互斥锁的属性指针,通常为 NULL(使用默认属性)。

返回值:成功返回 0,失败返回错误码。

  

  加锁

int pthread_mutex_lock(pthread_mutex_t *mutex);

作用:获取互斥锁,如果锁已被占用则阻塞等待。

参数:mutex:要加锁的互斥锁指针。

返回值:成功返回 0,失败返回错误码。

  

  尝试加锁

int pthread_mutex_trylock(pthread_mutex_t *mutex);

作用:尝试获取互斥锁,如果锁可用则获取并返回 0,否则立即返回 EBUSY。

参数:mutex:要尝试加锁的互斥锁指针。

返回值:成功返回 0,锁不可用返回 EBUSY。

  

  解锁

int pthread_mutex_unlock(pthread_mutex_t *mutex);

作用:释放已获取的互斥锁。

参数:mutex:要解锁的互斥锁指针。

返回值:成功返回 0,失败返回错误码。

  

  销毁互斥锁

int pthread_mutex_destroy(pthread_mutex_t *mutex);

作用:销毁指定的互斥锁。

参数:mutex:要销毁的互斥锁指针。

返回值:成功返回 0,失败返回错误码。

  

8.2 条件变量(Condition Variable)

  通常与互斥锁配合使用,用于线程之间的等待和通知。

  比如一个线程需要等待某个条件满足后才能继续执行,而另一个线程在条件满足时通知它。

  

  初始化条件变量

int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);

作用:初始化一个条件变量。

参数:
  cond:指向要初始化的条件变量的指针。

  attr:条件变量的属性指针,通常为 NULL(使用默认属性)。

返回值:成功返回 0,失败返回错误码。

  

  等待条件变量

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

作用:阻塞当前线程,直到指定的条件变量被唤醒。

参数:
  cond:要等待的条件变量指针。

  mutex:与条件变量关联的互斥锁指针。

返回值:成功返回 0,失败返回错误码。

  

  定时等待条件变量

int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);

作用:阻塞当前线程,直到指定的条件变量被唤醒或到达指定的超时时间。

参数:
  cond:要等待的条件变量指针。

  mutex:与条件变量关联的互斥锁指针。

  abstime:指定的超时时间。

返回值:成功返回 0,超时返回 ETIMEDOUT,失败返回其他错误码。

  

  唤醒一个等待条件变量的线程

int pthread_cond_signal(pthread_cond_t *cond);

作用:唤醒至少一个等待指定条件变量的线程。

参数:cond:要唤醒的条件变量指针。

返回值:成功返回 0,失败返回错误码。

  

  唤醒所有等待条件变量的线程

int pthread_cond_broadcast(pthread_cond_t *cond);

作用:唤醒所有等待指定条件变量的线程。

参数:cond:要唤醒的条件变量指针。

返回值:成功返回 0,失败返回错误码。

  

  销毁条件变量

int pthread_cond_destroy(pthread_cond_t *cond);

作用:销毁指定的条件变量。

参数:cond:要销毁的条件变量指针。

返回值:成功返回 0,失败返回错误码。

  

8.3 信号量(Semaphore)

  用于控制同时访问某一资源的线程数量。

  例如限制同时访问数据库连接的线程数量。

  

  初始化信号量

int sem_init(sem_t *sem, int pshared, unsigned int value);

作用:初始化一个信号量。

参数:
  sem:指向要初始化的信号量的指针。

  pshared:表示信号量的共享属性,0 表示线程间共享,非 0 表示进程间共享。

  value:信号量的初始值。

返回值:成功返回 0,失败返回 -1。

  

  等待信号量

int sem_wait(sem_t *sem);

作用:等待信号量的值大于 0,然后将其减 1。

参数:sem:要操作的信号量指针。

返回值:成功返回 0,失败返回 -1。

  

  尝试等待信号量

int sem_trywait(sem_t *sem);

作用:尝试等待信号量,如果信号量的值大于 0,则将其减 1 并立即返回;否则返回错误。

参数:sem:要操作的信号量指针。

返回值:成功返回 0,信号量不可用返回 -1 并设置 errno 为 EAGAIN。

  

  释放信号量

int sem_post(sem_t *sem);

作用:将信号量的值增加 1。

参数:sem:要操作的信号量指针。

返回值:成功返回 0,失败返回 -1。

  

  获取信号量的值

int sem_getvalue(sem_t *sem, int *sval);

作用:获取信号量的当前值,并将其存储在 sval 指向的变量中。

参数:
  sem:要操作的信号量指针。

  sval:用于存储信号量值的整数指针。

返回值:成功返回 0,失败返回 -1。

  

  销毁信号量

int sem_destroy(sem_t *sem);

作用:销毁指定的信号量。

参数:sem:要销毁的信号量指针。

返回值:成功返回 0,失败返回 -1。

  

8.4 自旋锁(Spin Lock)

  线程在获取锁失败时,会一直循环尝试获取,而不是阻塞等待。

  适用于锁被持有的时间很短的情况,能避免线程切换的开销,但如果锁被长时间持有,会浪费 CPU 资源。

  

  初始化自旋锁

int spinlock_init(spinlock_t *lock, const spinlockattr_t *attr);

作用:初始化指定的自旋锁。

参数:
  lock:指向要初始化的自旋锁的指针。
  attr:自旋锁属性指针,可为 NULL(使用默认属性)。

返回值:成功返回 0,失败返回错误码。

  

  销毁自旋锁

int spinlock_destroy(spinlock_t *lock);

作用:销毁指定的自旋锁。

参数:lock:要销毁的自旋锁指针。

返回值:成功返回 0,失败返回错误码。

  

  尝试获取自旋锁(读)

int spinlock_rdlock(spinlock_t *lock);

作用:尝试获取自旋锁的读锁。

参数:lock:指向要获取读锁的自旋锁的指针。

返回值:成功返回 0,失败返回错误码。

  

  尝试获取自旋锁(写)

int spinlock_wrlock(spinlock_t *lock);

作用:尝试获取自旋锁的写锁。

参数:lock:指向要获取写锁的自旋锁的指针。

返回值:成功返回 0,失败返回错误码。

  

  释放自旋锁

int spinlock_unlock(spinlock_t *lock);

作用:释放指定的自旋锁。

参数:lock:要释放的自旋锁指针。

返回值:成功返回 0,失败返回错误码。

  

8.5 读写锁(Read-Write Lock)

  区分读操作和写操作。允许多个线程同时进行读操作,但在写操作时,不允许其他线程进行读或写操作。

  适用于读操作频繁而写操作较少的场景,比如共享数据的读取次数远多于修改次数的情况。

在这里插入图片描述  

  初始化读写锁

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);

作用:初始化指定的读写锁。

参数:
  rwlock:指向要初始化的读写锁的指针。
  attr:读写锁属性指针,可为 NULL(使用默认属性)。

返回值:成功返回 0,失败返回错误码。

  

  销毁读写锁

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

作用:销毁指定的读写锁。

参数:rwlock:要销毁的读写锁指针。

返回值:成功返回 0,失败返回错误码。

  

  获取读写锁的读锁

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

作用:尝试获取指定读写锁的读锁。

参数:rwlock:指向要获取读锁的读写锁的指针。

返回值:成功返回 0,失败返回错误码。

  

  获取读写锁的写锁

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

作用:尝试获取指定读写锁的写锁。

参数:rwlock:指向要获取写锁的读写锁的指针。

返回值:成功返回 0,失败返回错误码。

  

  释放读写锁

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

作用:释放指定的读写锁。

参数:rwlock:要释放的读写锁指针。

返回值:成功返回 0,失败返回错误码。

  

  设置读写锁的优先级

int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);

作用:设置读写锁的优先级。

参数:
  attr:读写锁属性指针。
  pref:优先级选择,有以下 3 种:

  PTHREAD_RWLOCK_PREFER_READER_NP (默认设置)读者优先,可能会导致写者饥饿情况。

  PTHREAD_RWLOCK_PREFER_WRITER_NP 写者优先,目前有 BUG,导致表现行为和 PTHREAD_RWLOCK_PREFER_READER_NP 一致。

  PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP 写者优先,但写者不能递归加锁。

返回值:成功返回 0,失败返回错误码。

  

其他概念:

  悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。

  乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。

  公平锁:公平锁按照线程请求锁的先后顺序来分配锁。先请求的线程先获取,保证了顺序公平。保证顺序,适合要求严格公平的场景。但性能开销大,高并发时吞吐量可能受影响。

  非公平锁:非公平锁不按请求顺序分配锁,锁释放时竞争的线程都可能获取,不一定是先请求的。性能好,高并发时吞吐量可能高。但可能导致线程饥饿,行为不太确定。

  CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。

            

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

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

相关文章

华为云GaussDB部署指南:主备架构的常见问题与解决方案

文章目录 华为云GaussDB部署指南&#xff1a;主备架构的常见问题与解决方案背景介绍部署步骤1.修改主机名2.软件安装检查3.禁用交换内存4.创建数据目录并挂载5.配置NTP时钟同步6.添加资源限制参数7.修改网卡的MTU8.上传安装工具包9.编辑集群配置文件10.修改集群安装模板11.安装…

ROS、pix4、gazebo、qgc仿真ubuntu20.04

一、ubuntu、ros安装教程比较多&#xff0c;此文章不做详细讲解。该文章基于ubuntu20.04系统。 pix4参考地址&#xff1a;https://docs.px4.io/main/zh/index.html 二、安装pix4 1. git clone https://github.com/PX4/PX4-Autopilot.git --recursive 2. bash ./PX4-Autopilot…

可视化剪辑,账号矩阵,视频分发,聚合私信一体化营销工具 源----代码开发部署方案

可视化剪辑&#xff1a; 为了实现可视化剪辑功能&#xff0c;可以使用流行的视频编辑软件或者开发自己的视频编辑工具。其中&#xff0c;通过设计用户友好的界面&#xff0c;用户可以简单地拖拽和放大缩小视频片段&#xff0c;剪辑出满足需求的视频。在开发过程中&#xff0c;可…

SpringBoot框架学习笔记(四):yaml 介绍及其使用

1 yaml 介绍 百度百科&#xff1a;YAML 是 “YAML Aint a Markup Language”&#xff08;YAML 不是一种标记语言&#xff09;的递归缩写。在开发这种语言时&#xff0c;YAML 的意思其实是&#xff1a;“Yet Another Markup Language”&#xff08;仍是一种标记语言&#xff09…

电流测量分流电阻

电流测量分流电阻 测量电流的设备称为安培计。大多数现代安培计测量已知电阻的精密电阻上的电压降。电流的计算使用欧姆定律&#xff1a;我五R 大多数电流表都内置电阻器来测量电流。但是&#xff0c;当电流对于电流表来说太高时&#xff0c;需要不同的设置。解决方案是将电流…

使用 Flask 3 搭建问答平台(三):注册页面模板渲染

前言 前端文件下载 链接https://pan.baidu.com/s/1Ju5hhhhy5pcUMM7VS3S5YA?pwd6666%C2%A0 知识点 1. 在路由中渲染前端页面 2. 使用 JinJa 2 模板实现前端代码复用 一、auth.py from flask import render_templatebp.route(/register, methods[GET]) def register():re…

= null 和 is null;SQL中关于NULL处理的4个陷阱;三值逻辑

一、概述 1、NULL参与的所有的比较和算术运算符(>,,<,<>,<,>,,-,*,/) 结果为unknown&#xff1b; 2、unknown的逻辑运算(AND、OR、NOT&#xff09;遵循三值运算的真值表&#xff1b; 3、如果运算结果直接返回用户&#xff0c;使用NULL来标识unknown 4、如…

IAR嵌入式开发解决方案已全面支持芯科集成CX3288系列车规RISC-V MCU,共同推动汽车高品质应用的安全开发

中国上海&#xff0c;2024年7月16日 — 全球领先的嵌入式系统开发软件解决方案供应商IAR与芯科集成电路&#xff08;以下简称“芯科集成”&#xff09;联合宣布&#xff0c;最新版本IAR Embedded Workbench for RISC-V 3.30.2功能安全版已全面支持芯科集成CX3288系列车规RISC-V…

目标检测IOU和NMS详解

1. 目标检测中两个重要的概念:IOU和NMS 1, 具体来说&#xff0c;它是两边界框相交部分面积与相并部分面积之比&#xff0c;如下所示&#xff1a; 2.原理 这里详细解释一下计算的原理&#xff1a; 一般来说我们给定框的坐标有两种形式&#xff1a; [x,y,w,h] (或者说是[x,y,h,…

windows 11 PC查询连接过的wlan密码

1:管理员打开cmd 2:输入netsh wlan show profiles 3:netsh wlan show profiles Shw2024-5G keyclear 密码关键内容&#xff1a;12345678

全时守护,无死角监测:重点海域渔港视频AI智能监管方案

一、方案背景 随着海洋经济的快速发展和海洋资源的日益紧缺&#xff0c;对重点海域渔港进行有效监控和管理显得尤为重要。视频监控作为一种高效、实时的管理手段&#xff0c;已成为渔港管理中不可或缺的一部分。当前&#xff0c;我国海域面积广阔&#xff0c;渔港众多&#xf…

Token Labeling(NeurIPS 2021, ByteDance)论文解读

paper&#xff1a;All Tokens Matter: Token Labeling for Training Better Vision Transformers official implementation&#xff1a;https://github.com/zihangJiang/TokenLabeling 出发点 ViTs的局限性&#xff1a;尽管ViTs在捕捉长距离依赖方面表现出色&#xff0c; 但…

Postman导出excel文件

0 写在前面 在我们后端写接口的时候&#xff0c;前端页面还没有出来&#xff0c;我们就得先接口测试&#xff0c;在此记录下如何使用postman测试导出excel接口。 如果不会使用接口传参可以看我这篇博客如何使用Postman 1 方法一 2 方法二 3 写在末尾 虽然在代码中写入文件名…

Linux 推出 Redis 分支 Valkey

Valkey——一个开源高性能键值存储 Redis 公司宣布更改开源许可之后&#xff0c;社区里出现了多个 Redis 分支&#xff0c;如 Redict、Valkey 等 2024 年 3 月 21 日&#xff0c;Redis 背后企业 Redis 的 CEO Rowan Trollope 宣布&#xff0c;该项目的许可证类型将从原本的 BS…

VLAN 划分案例详解

vlan 的应用在网络项目中是非常广泛的&#xff0c;基本上大部分的项目都需要划分 vlan&#xff0c;这里从基础的 vlan 的知识开始&#xff0c;了解 vlan 的划分原理。 为什么需要 vlan&#xff1a; 1、什么是 VLAN&#xff1f; VLAN&#xff08;Virtual LAN&#xff09;&…

SpringCloud------Sentinel(微服务保护)

目录 雪崩问题 处理方式!!!技术选型 Sentinel 启动命令使用步骤引入依赖配置控制台地址 访问微服务触发监控 限流规则------故障预防流控模式流控效果 FeignClient整合Sentinel线程隔离-------故障处理线程池隔离和信号量隔离​编辑 两种方式优缺点设置方式 熔断降级-----…

【JavaEE-多线程背景-线程等待-线程的六种状态-线程安全问题-详解】

&#x1f308;个人主页&#xff1a;SKY-30 ⛅个人推荐&#xff1a;基于java提供的ArrayList实现的扑克牌游戏 |C贪吃蛇详解 ⚡学好数据结构&#xff0c;刷题刻不容缓&#xff1a;点击一起刷题 &#x1f319;心灵鸡汤&#xff1a;总有人要赢&#xff0c;为什么不能是我呢 &…

高性能、安全、低碳绿色的趋势下,锐捷网络发布三擎云办公解决方案 3.0

桌面虚拟化作为云时代的主流和热门技术&#xff0c;已经取得了广泛应用。随着生成式 AI 爆炸式发展&#xff0c;CSDN 看到&#xff0c;人工智能正在引发计算、开发、交互三大范式的全面升级&#xff0c;技术开发或将迎来一次全新的科技变革周期&#xff0c;因此 VDI 云桌面随之…

20分钟迁移完阿里云ECS跨区域迁移,用老操作系统作为新服务操作系统

由于特殊原因或者数据备份需要迁移ecs服务器 跨区域复制 镜像复制 由于特殊原因或者数据备份需要迁移ecs服务器 1.老服务快照 选择ecs实例&#xff0c;点开实例 进入云盘 https://ecs.console.aliyun.com/disk 在云盘上点击建立快照 https://oss.console.aliyun.com/bu…

C# 位移运算符 <<、>>, 以及 operator 关键字 重载运算符

关键字 operator operator关键字用于在类或结构声明中声明运算符。运算符声明可以采用下列四种形式之一&#xff1a; public static result-type operator unary-operator ( op-type operand ) public static result-type operator binary-operator ( op-type operand, op-ty…