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

多线程(C++11)

多线程(C++)


文章目录

  • 多线程(C++)
  • 前言
  • 一、std::thread类
    • 1.线程的创建
      • 1.1构造函数
      • 1.2代码演示
    • 2.公共成员函数
      • 2.1 get_id()
      • 2.2 join()
      • 2.3 detach()
      • 2.4 joinable()
      • 2.5 operator=
    • 3.静态函数
    • 4.类的成员函数作为子线程的任务函数
  • 二、call_once函数
    • 1.原理和介绍
    • 2.应用-懒汉模式
  • 三、线程同步之互斥锁(互斥量)
    • 1.std::mutex类
      • 1.1 成员函数
      • 1.2 线程同步
    • 2.std::lock_guard类
    • 3.std::recursive_mutex类
    • 4.std::timed_mutex类
  • 四、线程同步之条件变量
    • 1.生产者-消费者模型
    • 2.条件变量
      • 2.1 成员函数
  • 总结


前言

C++11之前,C++语言没有对并发编程提供语言级别的支持,这使得我们在编写可移植的并发程序时,存在诸多的不便。现在C++11中增加了线程以及线程相关的类,很方便地支持了并发编程,使得编写的多线程程序的可移植性得到了很大的提高。


一、std::thread类

1.线程的创建

1.1构造函数

//1. 默认构造函数,构造一个线程对象,在这个线程中不执行任何处理动作
thread() nonexcept;//2. 移动构造函数,将other的线程所有权转移给新的thread对象,之后的other不再表示执行线程
thread(thread&& other) nonexcept;//3. 创建线程对象,并在该线程中执行函数f的业务逻辑,args是要传递给函数f的参数(使用最多的,最有用的)
//此处使用右值引用的好处:可以延长临时变量的寿命,等到使用完成后再进行释放
template<class Function,class... Args>
explicit thread(Function&& f,Args&&... args);
/*备注:
任务函数f的类型可以是如下:
1.普通函数、类成员函数、匿名函数、仿函数(这些都是可调用对象类型)
2.可以是可调用对象包装器类型,也可以是使用绑定器绑定之后得到的类型(仿函数)
*///4.使用delet显示删除拷贝构造,不允许线程对象之间的拷贝
thread(const thread&)=delete;

1.2代码演示

#include <iostream>
#include <thread>void func()
{std::cout << "children worker:" <<"xiaodu ,id:"<< std::this_thread::get_id() << std::endl;
}void func1(std::string name,int id)
{std::cout << "children worker:" << name << ",id:" << std::this_thread::get_id() << std::endl;
}int main() 
{std::cout << "主线程id:" << std::this_thread::get_id() << std::endl;//1. 创建空的线程对象std::thread t1;//2. 创建一个可用的子线程std::thread t2(func);std::thread t3(func1, "xiaoliu", 20);std::thread t4([=](int id) {std::cout << "arg id :" << id << ",id:" << std::this_thread::get_id() << std::endl;}, 18);std::thread&& t5 = std::move(t4);return 0;
}

报错信息
报错原因分析:程序启动之后,执行main()函数,进程里就有一个主线程(父线程),然后接下来打印主线程id,再接下来创建线程t1、t2、t3、t4、t5,但是子线程创建后就会变成就绪态,和主线程一起抢cpu时间片,谁抢到cpu时间片谁就执行对应的任务函数,主线程大概率抢到cpu时间片,主线程执行完就退出了,随之退出的还有虚拟地址空间,因此,子线程就算抢到cpu时间片但是对应的虚拟地址空间已经没了,所以报错。
报错的解决思路:让主线程等待子线程执行完任务函数再退出。

2.公共成员函数

2.1 get_id()

应用程序启动后默认只有一个线程,这个线程一般称为主线程或父线程,通过线程类创建出来的线程一般称为子线程,每个线程创建出来都对应一个线程ID,这个ID是唯一的,可以通过这个ID来区分和识别各个已经存在的线程实例,这个函数就是get_id(),函数原型如下:

// std::thread::id--长整型数
std::thread::id get_id() const noexcept;

示例程序:

#include <iostream>
#include <thread>void func(int num,std::string str)
{for (int i = 0; i < 10; i++){std::cout << "子线程:i=" << i << " num : " << num << ",str: " << str << std::endl;}
}void func1()
{for (int i = 0; i < 10; i++){std::cout << "子线程:i=" << i << std::endl;}
}int main() 
{// 获得线程ID的两种方法:// 方法1--在子线程或者主线程函数中调用std::this_thread::get_id()函数,会得到当前线程IDstd::cout << "主线程id:" << std::this_thread::get_id() << std::endl;std::thread t(func,520,"i love you");std::thread t1(func1);// 方法2--调用thread类的成员函数get_id()std::cout << "线程t的线程ID:" << t.get_id()<<std::endl;std::cout << "线程t1的线程ID:" << t1.get_id() << std::endl;return 0;
}

2.2 join()

join()字面意思是连接一个线程,意味着主动地等待线程的终止(线程阻塞)。在某个线程A中通过子线程对象B调用join()函数,调用这个函数的线程A被阻塞,但是子线程对象B中的任务函数会继续执行,当任务执行完毕之后join()会清理当前子线程B中的相关资源然后返回,同时,调用该函数的线程A解除阻塞继续向下执行。

// 如果要阻塞主线程的执行,只需要在主线程中通过子线程对象调用这个方法即可,
// 当调用这个方法的子线程对象中的任务函数执行完毕之后,主线程的阻塞也就随之解除了
void std::thread::join();

示例程序1:

#include <iostream>
#include <thread>void func()
{std::cout << "children worker:" <<"xiaodu ,id:"<< std::this_thread::get_id() << std::endl;
}void func1(std::string name,int id)
{std::cout << "children worker:" << name << ",id:" << std::this_thread::get_id() << std::endl;
}int main() 
{std::cout << "主线程id:" << std::this_thread::get_id() << std::endl;//1. 创建空的线程对象std::thread t1;//2. 创建一个可用的子线程std::thread t2(func);std::thread t3(func1, "xiaoliu", 20);std::thread t4([=](int id) {std::cout << "arg id :" << id << ",id:" << std::this_thread::get_id() << std::endl;}, 18);std::thread&& t5 = std::move(t4);t2.join();t3.join();t5.join();		// t4线程的所有权转交给t5,t4对象失效return 0;
}

当程序运行到thread::join()时存在两种情况:

  • 如果任务函数func()还没执行完毕,主线程阻塞,知道任务执行完毕,主线程解除阻塞,继续向下运行
  • 如果任务函数func()已经执行完毕,主线程不会阻塞,继续向下执行

上述的bug通过join()函数成功解决。

示例程序2:
需求:程序中一共有三个线程,其中两个子线程负责分段下载同一个文件,下载完毕之后,由主线程对这个文件进行下一步处理

#include <iostream>
#include <thread>
#include <chrono>
using namespace std;void download1()
{// 模拟下载, 总共耗时500ms,阻塞线程500msthis_thread::sleep_for(chrono::milliseconds(500));cout << "子线程1: " << this_thread::get_id() << ", 下载1....完成" << endl;
}void download2()
{// 模拟下载, 总共耗时300ms,阻塞线程300msthis_thread::sleep_for(chrono::milliseconds(300));cout << "子线程2: " << this_thread::get_id() << ", 下载2....完成" << endl;
}void doSomething()
{cout << "完成...." << endl;
}int main()
{thread t1(download1);thread t2(download2);// 阻塞主线程,等待所有子线程任务执行完毕再继续向下执行t1.join();t2.join();doSomething();
}

最核心的处理是在主线程调用doSomething();之前在第35、36行通过子线程对象调用了join()方法,这样就能够保证两个子线程的任务都执行完毕了,也就是文件内容已经全部下载完成,主线程再对文件进行后续处理,如果子线程的文件没有下载完毕,主线程就去处理文件,很显然从逻辑上讲是有问题的。

2.3 detach()

detach()函数的作用是进行线程分离,分离主线程和创建出的子线程。在线程分离之后,主线程退出也会一并销毁创建出的所有子线程,在主线程退出之前,它可以脱离主线程继续独立的运行,任务执行完毕之后,这个子线程会自动释放自己占用的系统资源。(唯一好处)(比如孩子(子线程)翅膀硬了,和家里(父线程)断绝关系(detach),自己外出闯荡(脱离主线程执行任务函数),死了家里也不会给买棺材(执行完毕后,自己释放系统资源),如果家里被诛九族(主线程退出)还是会受到牵连(子线程还是会退出))。
示例程序:

#include <iostream>
#include <thread>void func()
{std::cout << "children worker:" << "xiaodu ,id:" << std::this_thread::get_id() << std::endl;
}void func1(std::string name, int id)
{std::cout << "children worker:" << name << ",id:" << std::this_thread::get_id() << std::endl;
}int main()
{std::cout << "主线程id:" << std::this_thread::get_id() << std::endl;//1. 创建空的线程对象std::thread t1;//2. 创建一个可用的子线程std::thread t2(func);std::thread t3(func1, "xiaoliu", 20);std::thread t4([=](int id){std::cout << "arg id :" << id << ",id:" << std::this_thread::get_id() << std::endl;}, 18);std::thread&& t5 = std::move(t4);t2.join();t3.join();// 子线程t5在主线程中调用detach()函数,子线程和主线程分离,//从此再也不能在主线程中子线程调用成员函数进行任何有效操作,否则会报错t5.detach();// 不能得到真正的线程IDt5.get_id();//  失效,报错//t5.join();		return 0;
}

注意事项:线程分离函数detach()不会阻塞线程,子线程和主线程分离之后,在主线程中就不能再对这个子线程做任何控制了,比如:通过join()阻塞主线程等待子线程中的任务执行完毕,或者调用get_id()获取子线程的线程ID。有利就有弊,鱼和熊掌不可兼得,建议使用join(),一般不用detach()。

2.4 joinable()

joinable()函数用于判断主线程和子线程是否处理关联(连接)状态,一般情况下,二者之间的关系处于关联状态,该函数返回一个布尔类型:

  • 返回值为true:主线程和子线程之间有关联(连接)关系
  • 返回值为false:主线程和子线程之间没有关联(连接)关系
bool joinable() const noexcept;

示例程序:

#include <iostream>
#include <thread>
#include <chrono>
using namespace std;void foo()
{std::this_thread::sleep_for(chrono::seconds(1));
}int main()
{// 创建一个空的线程对象,如果子线程未关联任务函数,则二者也是未关联thread t;cout << "before starting,joinable:" << t.joinable() << endl;	//false// 匿名对象作为临时变量,使用右值引用拷贝构造函数t = thread(foo);cout << "after starting,joinable:" << t.joinable() << endl;		//true// 线程t调用jion函数,阻塞主线程,执行任务函数完成后,退出子线程t.join();cout << "after starting,joinable:" << t.joinable() << endl;		//false// 创建线程t1 thread t1(foo);cout << "after starting,joinable:" << t1.joinable() << endl;	//true// 线程分离t1.detach();cout << "after starting,joinable:" << t1.joinable() << endl;		//falsereturn 0;
}

2.5 operator=

线程中的资源是不能被复制的,因此通过=操作符进行赋值操作最终并不会得到两个完全的对象。

// move(1)
thread& operator=(thread&& other) noexcept;
// copy[deleted](2)
thread& operator=(thread& other) noexcept;

通过以上=操作符的重载声明可以得知:

  • 如果other是一个右值,会将资源所有权进行转移
  • 如果other不是一个右值,禁止拷贝,该函数显示被删除(delete),不可用

3.静态函数

thread线程类还提供了一个静态方法,用于获取当前计算机的CPU核心数,根据这个结果在程序中创建出数量相等的线程,每个线程独自占有一个CPU核心,这些线程就不用分时复用CPU时间片,此时程序的并发效率是最高的(并行)。

static unsigned hardware_concurrency() noexcept;

示例代码:

#include <iostream>
#include <thread>
using namespace std;int main()
{unsigned int num = thread::hardware_concurrency();cout << "hardware_concurrency = " << num << endl;return 0;
}

4.类的成员函数作为子线程的任务函数

待学TODO

二、call_once函数

1.原理和介绍

在某些特定情况下,某些函数只能在多线程环境下调用一次,比如:要初始化某个对象,而这个对象只能被初始化一次,就可以使用std::call_once()来保证函数在多线程环境下只能被调用一次。使用call_once()的时候,需要一个once_flag作为call_once()的传入参数,该函数的原型如下:

// 定义位于<mutex>
#include <mutex>
void call_once(std::once_flag& flag,Callable&& f,Args&&... args)
  • flag:once_flag类型的对象,要保证这个对象能够被多个线程同时访问到
  • f:回调函数,可以传递一个有名函数地址,也可以指定一个匿名函数
  • args:作为实参传递给回调函数
    多线程操作过程中,std::call_once()内部的回调函数只会被执行一次,示例代码如下:
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;// 全局变量
once_flag flag;void do_once()
{cout << "do once function" << endl;
}void do_something()
{static int num = 1;// call_once注意事项:flag要能被多个线程同时访问;传入实参参数的形式类似thread函数;无返回值,void// 使用场景:在多线程中,只需要初始化一次对象(new 一个对象)call_once(flag, do_once);cout << "do something function: " << num++ << endl;
}int main()
{thread t1(do_something);thread t2(do_something);thread t3(do_something);t1.join();t2.join();t3.join();return 0;
}

call_once

2.应用-懒汉模式

单例模式分类:

  • 懒汉式
    系统运行中,实例并不存在,只有当需要使用该实例时,才会去创建并使用实例。这种方式要考虑线程安全
  • 饿汉式
    系统一运行,就初始化创建实例,当需要时,直接调用即可。这种方式本身就线程安全,没有多线程的线程安全问题。

编写一个单例模式(分为懒汉式和饿汉式)的类–懒汉模式。
示例程序:

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;// 单例模式--懒汉模式
// 全局变量,保证被多个线程访问
once_flag g_flag;// 单例类
class Base
{
public:// 实现单例模式首先将拷贝构造和赋值操作显示删除Base(const Base& obj) = delete;Base& operator=(const Base& obj) = delete;// 静态成员函数static Base* getInstance() {// 调用call_once函数保证多个线程访问时只有一个对象创建call_once(g_flag, [&]() {ptr = new Base;cout << "单例创建" << endl;});return ptr;}void release(){delete ptr;ptr = nullptr;}string getName(){return name;}void setName(string Name){this->name = Name;}private:// 构造函数设为private,不能被外界直接访问Base() { }~Base() { }static Base* ptr;string name;
};
// 懒汉模式是运行时才会创建对象,所以ptr先设置为nullptr
Base* Base::ptr = nullptr;void func(string name)
{Base::getInstance()->setName(name);std::cout<<"name:"<< Base::getInstance()->getName()<<endl;
}int main()
{thread t1(func,"路飞");thread t2(func,"埃斯");thread t3(func,"萨博");t1.join();t2.join();t3.join();Base::getInstance()->release();return 0;
}

懒汉模式
此处,很大概率会出现打印信息不对,因为三个线程同时对一块内存进行了读写操作,所以为了让多线程线性访问,需要加锁。
加锁后的代码程序:

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;// 单例模式--懒汉模式
// 全局变量,保证被多个线程访问
once_flag g_flag;// 单例类
class Base
{
public:// 实现单例模式首先将拷贝构造和赋值操作显示删除Base(const Base& obj) = delete;Base& operator=(const Base& obj) = delete;// 静态成员函数static Base* getInstance() {// 调用call_once函数保证多个线程访问时只有一个对象创建call_once(g_flag, [&]() {ptr = new Base;cout << "单例创建" << endl;});return ptr;}void release(){delete ptr;ptr = nullptr;}string getName(){return name;}void setName(string Name){this->name = Name;}private:// 构造函数设为private,不能被外界直接访问Base() { }~Base() { }static Base* ptr;string name;
};
// 懒汉模式是运行时才会创建对象,所以ptr先设置为nullptr
Base* Base::ptr = nullptr;mutex mx;
void func(string name)
{// 多个线程的共享资源是Base::getInstance()mx.lock();Base::getInstance()->setName(name);std::cout<<"name:"<< Base::getInstance()->getName()<<endl;mx.unlock();
}int main()
{thread t1(func,"路飞");thread t2(func,"埃斯");thread t3(func,"萨博");t1.join();t2.join();t3.join();Base::getInstance()->release();return 0;
}

三、线程同步之互斥锁(互斥量)

进行多线程编程,如果多个线程需要对同一块内存进行操作,比如:同时读、同时写、同时读写对于后两种情况来说,如果不做任何的人为干涉就会出现各种各样的错误数据。这是因为线程在运行的时候需要先得到CPU时间片,时间片用完之后需要放弃已获得的CPU资源,就这样线程频繁地在就绪态和运行态之间切换,更复杂一点还可以在就绪态、运行态、挂起态之间切换,这样就会导致线程的执行顺序并不是有序的,而是随机的混乱的。
多线程访问
注意概念:线程同步是指让多线程进行线性有序的进行,而不是同时对一块内存操作。
解决多线程数据混乱的方案就是进行线程同步,最常用的就是互斥锁,在C++11中一共提供了四种互斥锁:

  • std::mutex:独占的互斥锁,不能递归使用 (最常用)
  • std::timed_mutex:带超时的独占互斥锁,不能递归使用
  • std::recursive_mutex:递归互斥锁,不带超时功能
  • std::recursive_timed_mutex:带超时的递归互斥锁
    注意事项:
  • 内存可能涉及到多个线程的同时访问(写、读写),此时需要通过互斥锁对内存进行保护
  • 使用互斥锁锁住的是和共享数据相关的一个代码块(临界区)
  • 在程序中一个共享数据对应多个代码块,在锁住这些代码块的时候要用同一把锁,并且程序运行期间不能销毁这把互斥锁

1.std::mutex类

1.1 成员函数

lock()函数用于给临界区加锁,并且只能有一个线程获得锁的所有权,它有阻塞其他线程的作用,函数原型如下:

// 
void lock();

独占互斥锁对象有两种状态:锁定和未锁定。如果互斥锁是打开的,调用lock()函数的线程会得到互斥锁的所有权,并将其上锁,其它线程再调用该函数的时候由于得不到互斥锁的所有权,就会被lock()函数阻塞。当拥有互斥锁所有权的线程将互斥锁解锁,此时被lock()阻塞的线程解除阻塞,抢到互斥锁所有权的线程加锁并继续运行,没抢到互斥锁所有权的线程继续阻塞。

除了使用lock()还可以使用try_lock()获取互斥锁的所有权并对互斥锁加锁,函数原型如下:

bool try_lock();

二者的区别在于try_lock()不会阻塞线程,lock()会阻塞线程:

  • 如果互斥锁是未锁定状态,得到了互斥锁所有权并加锁成功,函数返回true
  • 如果互斥锁是锁定状态,无法得到互斥锁所有权加锁失败,函数返回false

**当互斥锁被锁定之后可以通过unlock()进行解锁,但是需要注意的是只有拥有互斥锁所有权的线程也就是对互斥锁上锁的线程才能将其解锁,其它线程是没有权限做这件事情的。**该函数的函数原型如下:

void unlock();

通过介绍以上三个函数,使用互斥锁进行线程同步的大致思路差不多就能搞清楚了,主要分为以下几步:

  • 找到多个线程操作的共享资源(全局变量、堆内存、类成员变量等),也可以称之为临界资源
  • 找到和共享资源有关的上下文代码,也就是临界区(下图中的黄色代码部分)
  • 在临界区的上边调用互斥锁类的lock()方法
  • 在临界区的下边调用互斥锁的unlock()方法
    线程同步
    线程同步的目的是让多线程按照顺序依次执行临界区代码,这样做线程对共享资源的访问就从并行访问变为了线性访问,访问效率降低了,但是保证了数据的正确性。

注意:
当线程对互斥锁对象加锁,并且执行完临界区代码之后,一定要使用这个线程对互斥锁解锁,否则最终会造成线程的死锁。死锁之后当前应用程序中的所有线程都会被阻塞,并且阻塞无法解除,应用程序也无法继续运行。

1.2 线程同步

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;// 互斥锁
class Base
{
public:// count表示打印的个数void increment(int count) {for (int i = 0; i < count; i++){mx.lock();++num;cout << "++current number: " << num << endl;mx.unlock();this_thread::sleep_for(chrono::milliseconds(100));}}// count表示打印的个数void decrement(int count){for (int i = 0; i < count; i++){mx.lock();--num;cout << "--current number: " << num << endl;mx.unlock();this_thread::yield();}}private:// 此处为共享资源int num=999;mutex mx;
};int main()
{Base b;thread t1(&Base::increment,&b,10);thread t2(&Base::decrement, &b, 10);t1.join();t2.join();return 0;
}

结果

2.std::lock_guard类

lock_guard是C++11新增的一个模板类,使用这个类,可以简化互斥锁lock()和unlock()的写法,同时也更安全。这个模板类的定义和常用的构造函数原型如下:

// 类的定义,定义于头文件 <mutex>
template< class Mutex >
class lock_guard;// 常用构造函数
explicit lock_guard( mutex_type& m );

lock_guard在使用上面提供的这个构造函数构造对象时,会自动锁定互斥量,而在退出作用域后进行析构时就会自动解锁,从而保证了互斥量的正确操作,避免忘记unlock()操作而导致线程死锁。lock_guard使用了RAII技术,就是在类构造函数中分配资源,在析构函数中释放资源,保证资源出了作用域就释放。
使用lock_guard对上面的例子进行修改,代码如下:

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;// 互斥锁
class Base
{
public:// count表示打印的个数void increment(int count) {for (int i = 0; i < count; i++){{lock_guard<mutex> lock(mx);++num;cout << "++current number: " << num << endl;}this_thread::sleep_for(chrono::milliseconds(100));}}// count表示打印的个数void decrement(int count){for (int i = 0; i < count; i++){{lock_guard<mutex> lock(mx);--num;cout << "--current number: " << num << endl;}this_thread::yield();}}private:// 此处为共享资源int num=999;mutex mx;
};int main()
{Base b;thread t1(&Base::increment,&b,10);thread t2(&Base::decrement, &b, 10);t1.join();t2.join();return 0;
}

lock_guard通过修改发现代码被精简了,而且不用担心因为忘记解锁而造成程序的死锁,需要根据实际情况选择最优的解决方案。

3.std::recursive_mutex类

递归互斥锁std::recursive_mutex允许同一线程多次获得互斥锁,可以用来解决同一线程需要多次获取互斥量时死锁的问题,在下面的例子中使用独占非递归互斥量会发生死锁:

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;// 互斥锁
class Base
{
public:// count表示打印的个数void increment(int count){for (int i = 0; i < count; i++){mx.lock();++num;cout << "++current number: " << num << endl;mx.unlock();this_thread::sleep_for(chrono::milliseconds(100));}}// count表示打印的个数void decrement(int count){for (int i = 0; i < count; i++){{lock_guard<mutex> lock(mx);// 发生死锁increment(2);--num;cout << "--current number: " << num << endl;}this_thread::yield();}}private:// 此处为共享资源int num = 999;mutex mx;	//递归互斥锁
};int main()
{Base b;thread t1(&Base::increment, &b, 10);thread t2(&Base::decrement, &b, 10);t1.join();t2.join();return 0;
}

错误信息
上面的程序中执行了decrement( )中对incremen( )调用之后,程序就会发生死锁,在decrement( )中已经对互斥锁加锁了,继续调用incremen( )函数,已经得到互斥锁所有权的线程再次获取这个互斥锁的所有权就会造成死锁(在C++中程序会异常退出,使用C库函数会导致这个互斥锁永远无法被解锁,最终阻塞所有的线程)。要解决这个死锁的问题,一个简单的办法就是使用递归互斥锁std::recursive_mutex,它允许一个线程多次获得互斥锁的所有权。修改之后的代码如下:

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;// 互斥锁
class Base
{
public:// count表示打印的个数void increment(int count){for (int i = 0; i < count; i++){mx.lock();++num;cout << "++current number: " << num << endl;mx.unlock();this_thread::sleep_for(chrono::milliseconds(100));}}// count表示打印的个数void decrement(int count){for (int i = 0; i < count; i++){{lock_guard<recursive_mutex> lock(mx);increment(2);--num;cout << "--current number: " << num << endl;}this_thread::yield();}}private:// 此处为共享资源int num = 999;recursive_mutex mx;	//递归互斥锁
};int main()
{Base b;thread t1(&Base::increment, &b, 10);thread t2(&Base::decrement, &b, 10);t1.join();t2.join();return 0;
}

结果
虽然递归互斥锁可以解决同一个互斥锁频繁获取互斥锁资源的问题,但是还是建议少用,主要原因如下:

  • 使用递归互斥锁的场景往往都是可以简化的,使用递归互斥锁很容易放纵复杂逻辑的产生,从而导致bug的产生
  • 递归互斥锁比非递归互斥锁效率要低一些。
  • 递归互斥锁虽然允许同一个线程多次获得同一个互斥锁的所有权,但最大次数并未具体说明,一旦超过一定的次数,就会抛出std::system错误。

4.std::timed_mutex类

std::timed_mutex是超时独占互斥锁,主要是在获取互斥锁资源时增加了超时等待功能,因为不知道获取锁资源需要等待多长时间,为了保证不一直等待下去,设置了一个超时时长,超时后线程就可以解除阻塞去做其他事情了。
std::timed_mutex比std::_mutex多了两个成员函数:try_lock_for()和try_lock_until():

void lock();
bool try_lock();
void unlock();// std::timed_mutex比std::_mutex多出的两个成员函数
template <class Rep, class Period>bool try_lock_for (const chrono::duration<Rep,Period>& rel_time);template <class Clock, class Duration>bool try_lock_until (const chrono::time_point<Clock,Duration>& abs_time);
  • try_lock_for函数是当线程获取不到互斥锁资源的时候,让线程阻塞一定的时间长度
  • try_lock_until函数是当线程获取不到互斥锁资源的时候,让线程阻塞到某一个指定的时间点
  • 关于两个函数的返回值:当得到互斥锁的所有权之后,函数会马上解除阻塞,返回true,如果阻塞的时长用完或者到达指定的时间点之后,函数也会解除阻塞,返回false
    下面的示例程序中为大家演示了std::timed_mutex的使用:
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;// 互斥锁
class Base
{
public:void work(){while (true) {// 锁未锁定--返回值为trueif (mx.try_lock_for(chrono::milliseconds(100))){cout << "count"<<++count<<"  ok 线程ID=" << this_thread::get_id() << endl;// 模拟任务使用的时长this_thread::sleep_for(chrono::milliseconds(500));cout << "count" << count << "  ok 线程ID=" << this_thread::get_id()<<endl;// 互斥锁解锁mx.unlock();break;}else // 阻塞时间超时--返回false{// 模拟任务使用的时长this_thread::sleep_for(chrono::milliseconds(50));cout << "not ok 线程ID=" << this_thread::get_id()<<endl;}}}private:int count = 0;	// 共享资源timed_mutex mx;	// 超时独占互斥锁
};int main()
{Base b;thread t1(&Base::work, &b);thread t2(&Base::work, &b);t1.join();t2.join();return 0;
}

result
**注意:解除阻塞和解锁是两个不同的概念。**从上述的结果,我们可以看出线程38332获得互斥锁的所有权,try_lock_for函数超过阻塞时长,返回false,阻塞的另一个线程1864执行else中的任务,由于这个是while(true)该线程1864重新被阻塞,超过阻塞时长,又重新执行else,直到线程38332解除互斥锁的所有权,线程1864获得互斥锁的所有权,执行接下来的过程。

四、线程同步之条件变量

1.生产者-消费者模型

条件变量通常用于生产者和消费者模型。生产者和消费者模型主要应用的场景是生产者和消费者的速度不匹配。 条件变量能够帮助我们解决某一类线程(生产者或者消费者),互斥锁能够帮助我们解决某一类线程因为多个线程访问共享资源导致的数据混乱问题。
生产者和消费者模型的组成:

  1. 生产者线程->若干个
    生产商品或者任务放入到任务队列中;
    任务队列满了生产者线程就阻塞,不满就工作;
    通过一个生产者的条件变量控制生产者线程阻塞和非阻塞。
  2. 消费者线程->若干个
    读任务队列,将任务或者数据取出;
    任务队列中有数据就有消费者,没有数据就阻塞;
    通过一个消费者的条件变量控制消费者线程阻塞和非阻塞。
  3. 任务队列->存储数据或者任务,对应一块内存,为了读写访问可以通过一个数据结构维护这块内存
    数据结构的类型可以分为数组、链表,也可以是STL容器(queue\stack\list\vector)
    注意:推荐使用STL中的容器,方便增加删除以及动态扩展等。
    生产者消费者模型
    生产者消费者模型任务队列类的代码演示:
#include <queue>
#include <iostream>
#include <thread>
using namespace std;class WorkQueue
{
public:// 添加数据void put(const int& task){workQueue.push(task);			// 尾部添加数据cout << "添加任务:" << task << ",线程ID:" << this_thread::get_id() << endl;}// 删除数据void take(){int node = workQueue.front();	// 取出头部数据workQueue.pop();cout << "删除任务:" << node << ",线程ID:" << this_thread::get_id() << endl;}// 判断任务队列是否满bool isFull(){return workQueue.size() == maxSize;}// 判断任务队列是否空bool isEmpty(){return workQueue.size() == 0;}// 当前任务队列的尺寸int workSize(){return workQueue.size();}private:int maxSize;	//任务队列的容量std::queue<int> workQueue;	// 任务队列的数据结构
};

优点:线程阻塞相比较线程空循环或者线程的重新创建来说效率更高,资源消耗更少。

2.条件变量

condition_variable:需要配合std::unique_lockstd::mutex进行wait操作,也就是阻塞线程的操作。
condition_variable_any:可以和任意带有lock()、unlock()语义的mutex搭配使用,也就是说有四种:

  • std::mutex:独占的非递归互斥锁
  • std::timed_mutex:带超时的独占非递归互斥锁
  • std::recursive_mutex:不带超时功能的递归互斥锁
  • std::recursive_timed_mutex:带超时的递归互斥锁

条件变量通常用于生产者和消费者模型,大致使用过程如下:

  • 拥有条件变量的线程获取互斥量
  • 循环检查某个条件,如果条件不满足阻塞当前线程,否则线程继续向下执行
    产品的数量达到上限,生产者阻塞,否则生产者一直生产。。。
    产品的数量为零,消费者阻塞,否则消费者一直消费。。。
  • 条件满足之后,可以调用notify_one()或者notify_all()唤醒一个或者所有被阻塞的线程
    由消费者唤醒被阻塞的生产者,生产者解除阻塞继续生产。。。
    由生产者唤醒被阻塞的消费者,消费者解除阻塞继续消费。。。

2.1 成员函数

condition_variable的成员函数主要分为两部分:线程等待(阻塞)函数 和线程通知(唤醒)函数,这些函数被定义于头文件 <condition_variable>

  • 等待函数
    调用wait()函数的线程会被阻塞
// ①	调用该函数的线程直接被阻塞
void wait (unique_lock<mutex>& lck);
// ②	该函数的第二个参数是一个判断条件,是一个返回值为布尔类型的函数
// 		该参数可以传递一个有名函数的地址,也可以直接指定一个匿名函数
//		表达式返回false当前线程被阻塞,表达式返回true当前线程不会被阻塞,继续向下执行
template <class Predicate>
void wait (unique_lock<mutex>& lck, Predicate pred);

独占的互斥锁对象不能直接传递给wait()函数,需要通过模板类unique_lock进行二次处理,通过得到的对象仍然可以对独占的互斥锁对象做(lock、try_lock、try_lock_for、try_lock_until、unlock)操作,使用起来更灵活。
如果线程被该函数阻塞,这个线程会释放占有的互斥锁的所有权,当阻塞解除之后这个线程会重新得到互斥锁的所有权,继续向下执行。(wait函数内部实现,其目的是为了避免线程的死锁)
wait_for()函数和wait()的功能是一样的,只不过多了一个阻塞时长,假设阻塞的线程没有被其他线程唤醒,当阻塞时长用完之后,线程就会自动解除阻塞,继续向下执行。

template <class Rep, class Period>
cv_status wait_for (unique_lock<mutex>& lck,const chrono::duration<Rep,Period>& rel_time);template <class Rep, class Period, class Predicate>
bool wait_for(unique_lock<mutex>& lck,const chrono::duration<Rep,Period>& rel_time, Predicate pred);

wait_until()函数和wait_for()的功能是一样的,它是指定让线程阻塞到某一个时间点,假设阻塞的线程没有被其他线程唤醒,当到达指定的时间点之后,线程就会自动解除阻塞,继续向下执行。

template <class Clock, class Duration>
cv_status wait_until (unique_lock<mutex>& lck,const chrono::time_point<Clock,Duration>& abs_time);template <class Clock, class Duration, class Predicate>
bool wait_until (unique_lock<mutex>& lck,const chrono::time_point<Clock,Duration>& abs_time, Predicate pred);
  • 通知函数
void notify_one() noexcept;
void notify_all() noexcept;

notify_one():唤醒一个被当前条件变量阻塞的线程
notify_all():唤醒全部被当前条件变量阻塞的线程

生产者线程:当任务队列满的时候,阻塞生产者线程;消费者线程:当任务队列空的时候,阻塞消费者线程。因此,需要两个条件变量控制生产者线程和消费者线程阻塞。

#include <queue>
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
using namespace std;class WorkQueue
{
public:// 添加数据void put(const int& task){unique_lock<mutex> locker(mx);// 如果任务队列满了,就需要阻塞生产者线程while(workQueue.size()==maxSize){// unique_lock对象类似lock_guard,管理mutex的加锁和解锁,当线程访问到这个对象时,未加锁则加锁,反之阻塞// 如果locker析构了,自动对mx管理的线程所有权进行解锁// 假设现在有线程A,B,C,线程A获得了互斥锁资源,则线程B,C进行阻塞notFull.wait(locker);	// 线程A在运行到wait的位置会先释放拥有的互斥锁资源(解锁),此时,线程B,C从阻塞态变成就绪态,线程A再阻塞,所以不会出现死锁// 等到线程A从wait位置解除阻塞,会跟其他线程再去抢互斥锁资源}workQueue.push(task);			// 尾部添加数据cout << "添加任务:" << task << ",线程ID:" << this_thread::get_id() << endl;// 唤醒消费者notEmpty.notify_one();}// 删除数据void take(){unique_lock<mutex> locker(mx);// 如果任务队列空了,就需要阻塞消费者线程while(workQueue.size()==0){notEmpty.wait(locker);}int node = workQueue.front();	// 取出头部数据workQueue.pop();cout << "删除任务:" << node << ",线程ID:" << this_thread::get_id() << endl;// 唤醒生产者notFull.notify_one();}// 判断任务队列是否满bool isFull(){// 任务队列的访问所以加锁std::lock_guard<std::mutex> locker(mx);return workQueue.size() == maxSize;}// 判断任务队列是否空bool isEmpty(){std::lock_guard<std::mutex> locker(mx);return workQueue.size() == 0;}// 当前任务队列的尺寸int workSize(){std::lock_guard<std::mutex> locker(mx);return workQueue.size();}private:int maxSize=100;	//任务队列的容量std::queue<int> workQueue;	// 任务队列的数据结构mutex mx;						// 独占的互斥锁condition_variable notFull;		// 控制生产者线程condition_variable notEmpty;	// 控制消费者线程
};int main()
{thread t1[5];thread t2[5];WorkQueue workQ;for (int i = 0; i < 5; i++){t1[i] = thread(&WorkQueue::put, &workQ, 100 + i);t2[i] = thread(&WorkQueue::take, &workQ);}for (int i = 0; i < 5; i++){t1[i].join();t2[i].join();}return 0;
}

在这里插入图片描述
条件变量condition_variable类的wait()还有一个重载的方法,可以接受一个条件,这个条件也可以是一个返回值为布尔类型的函数,条件变量会先检查判断这个条件是否满足,如果满足条件(布尔值为true),则当前线程重新获得互斥锁的所有权,结束阻塞,继续向下执行;如果不满足条件(布尔值为false),当前线程会释放互斥锁(解锁)同时被阻塞,等待被唤醒。
优化后的代码:

#include <queue>
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
using namespace std;class WorkQueue
{
public:// 添加数据void put(const int& task){unique_lock<mutex> locker(mx);notFull.wait(locker, [&]() {return !(workQueue.size() == maxSize);});workQueue.push(task);			// 尾部添加数据cout << "添加任务:" << task << ",线程ID:" << this_thread::get_id() << endl;// 唤醒消费者notEmpty.notify_one();}// 删除数据void take(){unique_lock<mutex> locker(mx);notEmpty.wait(locker, [&]() {return !workQueue.size() == 0;});int node = workQueue.front();	// 取出头部数据workQueue.pop();cout << "删除任务:" << node << ",线程ID:" << this_thread::get_id() << endl;// 唤醒生产者notFull.notify_one();}// 判断任务队列是否满bool isFull(){// 任务队列的访问所以加锁std::lock_guard<std::mutex> locker(mx);return workQueue.size() == maxSize;}// 判断任务队列是否空bool isEmpty(){std::lock_guard<std::mutex> locker(mx);return workQueue.size() == 0;}// 当前任务队列的尺寸int workSize(){std::lock_guard<std::mutex> locker(mx);return workQueue.size();}private:int maxSize=100;	//任务队列的容量std::queue<int> workQueue;	// 任务队列的数据结构mutex mx;						// 独占的互斥锁condition_variable notFull;		// 控制生产者线程condition_variable notEmpty;	// 控制消费者线程
};int main()
{thread t1[50];thread t2[50];WorkQueue workQ;for (int i = 0; i < 50; i++){t2[i] = thread(&WorkQueue::take, &workQ);t1[i] = thread(&WorkQueue::put, &workQ, 100 + i);}for (int i = 0; i < 50; i++){t1[i].join();t2[i].join();}return 0;
}

总结

本文详细介绍了C++11多线程中thread线程类、mutex等互斥锁、以及线程同步和生产者消费者模型,并提供了对应的示例,本文主要是参考爱编程的大丙,欢迎大家阅读大丙老师的文章,本文作个人学习之用。

相关文章:

多线程(C++11)

多线程&#xff08;C&#xff09; 文章目录 多线程&#xff08;C&#xff09;前言一、std::thread类1.线程的创建1.1构造函数1.2代码演示 2.公共成员函数2.1 get_id()2.2 join()2.3 detach()2.4 joinable()2.5 operator 3.静态函数4.类的成员函数作为子线程的任务函数 二、call…...

HLS入门

目录 一、 内容介绍二、 理解HLS2.1 HLS是什么&#xff1f;与VHDL/Verilog编程技术有什么关系?2.2 HLS有哪些关键技术问题&#xff1f;目前存在什么技术局限性&#xff1f; 三、 HLS在Quartus上的实现3.1 配置环境3.2 测试 四、 参考链接 一、 内容介绍 理解HLSHLS在Quartus上…...

电信光猫的USB存储对外网开放访问

前提条件当然是要有公网IP地址了&#xff0c;没有的话去找电信索要&#xff0c;然后可以使用动态域名正常访问。 我的电信光猫发现共享访问速度还可以&#xff0c;会有31M/s左右的写入速度 但是有一个不方便的是&#xff0c;无法从外网提供访问&#xff0c;SMB协议所用的445端…...

世界上首位AI程序员诞生,AI将成为人类的对手吗?

3月13日&#xff0c;世界上第一位AI程序员Devin诞生&#xff0c;不仅能自主学习新技术&#xff0c;自己改Bug&#xff0c;甚至还能训练和微调自己的AI模型&#xff0c;表现已然远超GPT-4等“顶流选手”。 AI的学习速度如此之快&#xff0c;人类的教育能否跟上“机器学习”的速…...

什么是创造力?如何判断自己的创造力?

创造力&#xff0c;主要表现为创新思想、发现和创造新事物的能力&#xff0c;是知识&#xff0c;智力和能力的综合能力&#xff0c;尤其是在职业发展方面&#xff0c;创造力具有重要的意义&#xff0c;企业的核心竞争力就来源于创造力&#xff0c;这就需要具有创造力的员工来推…...

Elasticsearch集群搭建学习

Elasticsearch集群聚合、集群搭建 RestClient查询所有高亮算分控制 数据聚合DSL实现Bucket聚合DSL实现Metrics聚合RestAPI实现聚合 拼音分词器如何使用拼音分词器&#xff1f;如何自定义分词器&#xff1f;拼音分词器注意事项&#xff1f; 自动补全数据同步集群搭建ES集群结构创…...

数据库(vb.net+OleDB+Access)简易学生信息管理系统

在我们日常生活当中&#xff0c;数据库一词往往离不开我们的编程界&#xff0c;在学校、仓库等方面起着存储数据及数据关系作用的文件。相较于Excel&#xff0c;Access可以存储无限多的记录&#xff0c;内容也十分丰富&#xff0c;例如文本、数字、日期、T&F等。而且不需要…...

Android 自定义图片进度条

用系统的Progressbar&#xff0c;设置图片drawable作为进度条会出现图片长度不好控制&#xff0c;容易被截断&#xff0c;或者变形的问题。而我有个需求&#xff0c;使用图片背景&#xff0c;和图片进度&#xff0c;而且在进度条头部有个闪光点效果。 如下图&#xff1a; 找了…...

对话:用言语构建深刻的思想碰撞

对话&#xff1a;用言语构建深刻的思想碰撞 在写书中&#xff0c;对话是一种有力的工具&#xff0c;能与读者进行有效的沟通和交流&#xff0c;引发深思和反思。它不仅是信息传递的方式&#xff0c;更是加深情感、探讨主题和吸引读者参与的桥梁。你应从读者的角度思考&#xf…...

Linux完整版命令大全(九)

4. linux压缩备份命令 ar 功能说明&#xff1a;建立或修改备存文件&#xff0c;或是从备存文件中抽取文件。语  法&#xff1a;ar[-dmpqrtx][cfosSuvV][a<成员文件>][b<成员文件>][i<成员文件>][备存文件][成员文件]补充说明&#xff1a;ar可让您集合许多…...

solidworks画螺栓学习笔记

螺栓 单位mm 六边形 直径16mm 水平约束 拉伸 选择厚度6mm 拉伸切除 画相切圆 切除厚度6mm&#xff0c;反向切除 &#xff0c;拔模角度45 螺栓 直径9mm&#xff0c;长度30mm 倒角 直径1mm&#xff0c;角度45 异形孔向导 螺纹线 偏移打勾&#xff0c;距离为2mm&#…...

【Spark】加大hive表在HDFS存的每个文件的大小

配置参数&#xff1a; spark.hadoop.hive.exec.orc.default.stripe.size78643200 spark.hadoop.orc.stripe.size78643200 spark.hadoopRDD.targetBytesInPartition78643200 spark.hadoop.hive.exec.dynamic.partition.modenonstrict spark.sql.sources.partitionOverwriteMode…...

2024 年 5 个 GO REST API 框架

什么是API&#xff1f; API是一个软件解决方案&#xff0c;作为中介&#xff0c;使两个应用程序能够相互交互。以下一些特征让API变得更加有用和有价值&#xff1a; 遵守REST和HTTP等易于访问、广泛理解和开发人员友好的标准。API不仅仅是几行代码&#xff1b;这些是为移动开…...

socket地址理解

socket介绍 套接字的基本概念 1. 套接字的定义&#xff1a; 套接字&#xff08;socket&#xff09;是计算机网络中用于通信的端点&#xff0c;它抽象了不同主机上应用进程之间双向通信的机制。 2. 套接字的作用&#xff1a; 套接字连接应用进程与网络协议栈&#xff0c;使…...

Gopeed的高级用法

Gopeed是一个开源全平台下载器&#xff0c;具体简介请参考&#xff1a; “狗屁下载器”&#xff1f;Gopeed - 开源全平台下载器 (免费轻量 / 比 Aria2 好用 / 远程下载) - 异次元软件世界 (iplaysoft.com) 这里主要介绍下自己摸索出来的 Gopeed 的高级做法。 有的网站添加的…...

OpenHarmony系统使用gdb调试init

前言 OpenAtom OpenHarmony&#xff08;简称“OpenHarmony”&#xff09;适配新的开发板时&#xff0c;启动流程init大概率会出现问题&#xff0c;其为内核直接拉起的第一个用户态进程&#xff0c;问题定位手段只能依赖代码走读和增加调试打印&#xff0c;初始化过程中系统崩溃…...

【SpringCloud】Spring Cloud基本介绍

目录 回顾架构分类单体架构分布式架构微服务架构什么是微服务优点缺点微服务的架构特征&#xff1a;微服务架构面临的挑战技术挑战微服架构的设计原则微服务概念提供者(Provider)消费者(Consumer)RPC和Restful集群分布式 总结 服务拆分和远程调用服务拆分原则服务拆分示例 思考…...

全域运营是本地生活服务的新模式吗?

最近&#xff0c;本地生活赛道又出现了一个新的说法&#xff0c;即全域运营是本地生活的下半场。事实上&#xff0c;这一论断并非空穴来风&#xff0c;而是有真凭实据。 作为多家互联网大厂重点布局的业务板块&#xff0c;本地生活的火爆程度早已有目共睹。根据多家互联网大厂…...

机器视觉-硬件

机器视觉-硬件 镜头焦距凸透镜焦点不止一个相机镜头由多个镜片组成对焦和变焦 镜头光圈光圈的位置光圈系数F 镜头的景深景深在光路中的几何意义 远心镜头远心镜头的种类远心镜头特性应用场景 镜头的分辨率镜头反差镜头的MTF曲线镜头的靶面尺寸镜头的几何相差相机镜头接口螺纹接…...

机器学习实验 --- 逻辑回归

第1关&#xff1a;逻辑回归核心思想 任务描述 本关任务&#xff1a;根据本节课所学知识完成本关所设置的编程题 #encodingutf8 import numpy as npdef sigmoid(t):完成sigmoid函数计算:param t: 负无穷到正无穷的实数:return: 转换后的概率值:可以考虑使用np.exp()函数#*****…...

浅谈C++函数

目录 一、函数的概念二、调用函数的两个前提三、函数传参的三种形式四、函数返回类型 一、函数的概念 函数是C程序的基本模块&#xff0c;通常一个C程序由一个或多个函数组成。函数可以完成用户指定的任务&#xff0c;一般分为库函数和用户自定义的函数。函数由函数头和函数体…...

6.小程序页面布局 - 账单明细

文章目录 1. 6.小程序页面布局 - 账单明细1.1. 竞品1.2. 布局分析1.3. 布局demo1.4. 页面实现-头部1.5. 账单明细1.5.1. 账单明细-竞品分析1.5.2. 账单明细-实现1.5.2.1. 账单明细-实现-mock数据1.5.2.2. 每日收支数据的聚合整理1.5.2.3. 页面scroll-view 1.6. TODO 1. 6.小程序…...

记录ES7.X更新数据的低级错误

背景&#xff1a;新项目复用之前同事遗留下的方法 问题&#xff1a;ES跨索引更新数据错误 排查&#xff1a;复用同事的方法有问题&#xff0c;他直接使用ES别名更新数据导致&#xff0c;只有一个索引时无问题&#xff0c;当多个索引使用同一别名时会出现异常 解决&#xff1…...

【简单介绍下链表基础知识】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…...

leetcode 2915.和为目标值的最长子序列的长度

思路&#xff1a;01背包 这个背包问题很经典了&#xff0c;但是这里涉及到一个问题&#xff0c;就是我们转化问题的时候发现&#xff0c;这个背包需要正好装满才行。这里我们把长度作为价值&#xff0c;也就是说每一个数的价值都是1。 我们需要把dp初始化为全部为负数&#x…...

欧拉函数、快速幂、扩展欧几里得算法、中国剩余定理和高斯消元

欧拉函数 给定 n 个正整数 ai&#xff0c;请你求出每个数的欧拉函数。 欧拉函数的定义1∼N 中与 N 互质的数的个数被称为欧拉函数&#xff0c;记为 ϕ(N)。 若在算数基本定理中&#xff0c;Np1a11p2a2…pmm&#xff0c;则&#xff1a;ϕ(N) Np1−1/p1p2−1/p2…pm−1/pm 输…...

自定义原生小程序顶部及获取胶囊信息

需求&#xff1a;我需要将某个文字或者按钮放置在小程序顶部位置 思路&#xff1a;根据获取到的顶部信息来定义我需要放的这个元素样式 * 这里我是定义某个指定页面 json&#xff1a;给指定页面的json中添加自定义设置 "navigationStyle": "custom" JS&am…...

yolov8推理由avi改为mp4

修改\ultralytics-main\ultralytics\engine\predictor.py&#xff0c;即可 # Ultralytics YOLO &#x1f680;, AGPL-3.0 license """ Run prediction on images, videos, directories, globs, YouTube, webcam, streams, etc.Usage - sources:$ yolo modepred…...

Vue3设置缓存:storage.ts

在vue文件使用&#xff1a; import { Local,Session } from //utils/storage; // Local if (!Local.get(字段名)) Local.set(字段名, 字段的值);// Session Session.getToken()storage.ts文件&#xff1a; import Cookies from js-cookie;/*** window.localStorage 浏览器永…...

超市信息管理系统(java+swing+jdbc+msyql)

...