【C++】C++11 线程库

news2024/11/18 20:00:30

文章目录

  • 一、thread 线程库
  • 二、mutex 锁
  • 三、atomic 原子性操作
  • 四、RAII 管理锁资源
  • 五、condition_variable 条件变量

一、thread 线程库

在 C++11 之前,由于 C++ 没有对各平台的线程接口进行封装,所以当涉及到多线程编程时,编写出来的代码都是和平台相关的,因为不同平台提供的线程相关接口是不同的;这就导致代码的可移植性比较差。C++11 一个很重要的改动就是对各平台的线程操作进行了封装,从而有了自己的线程库,同时还在原子操作中还引入了原子类的概念。

C++11 线程库定义在 <thread> 头文件下,我们可以查询相关文档进行学习:C++11线程库类image-20230527130805407

thread 类中主要提供了如下接口:

  • 构造函数:支持无参构造,即构造一个空线程对象,由于线程对象不会和任何外部线程关联,也没有关联的线程函数,因此不能直接开始执行线程,无参构造通常需要配合移动赋值来使用。

    支持构造一个线程对象,并关联线程函数,构造函数中的可变参数是传递给线程函数的参数,这种线程对象一旦创建就会开始执行。同时支持移动构造,即使用一个将亡对象来构造一个新的线程对象。image-20230527131908957

  • 赋值重载:线程不允许两个非将亡对象之间的赋值,只运行将一个将亡对象赋值给另一个非将亡对象,即移动赋值,移动赋值的常见用法是构造一个匿名线程对象,然后将其赋值给一个空线程对象。image-20230527132104223

  • get_id:获取当前线程的 id,即线程的唯一标识 – bool joinable() const noexcept

  • joinable:用于检查当前线程对象是否与某个底层线程相关联,从而判断是否需要对线程对象进行 join() 或 detach() 操作 – bool joinable() const noexcept

  • join:由于线程是进程中的一个执行单元,同时线程的所有资源也是由进程分配的,所以主线程在结束前需要对其他从线程进行 join;即判断从线程是否全部指向完毕,如果指向完毕就回收从线程资源并继续向后执行;如果存在未指向完毕的从线程,主线程就会阻塞在 join 语句处等待从线程,直到所有从线程都执行完毕 – void join()

  • detach:将当前线程与主线程分离,分离后,主线程不能再 join 当前线程,当前线程的资源会被自动回收 – void detach()

使用 thread 类时有如下注意事项:

  1. 线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的状态。当创建一个线程对象后,如果没有提供线程函数,则该对象实际没有对应任何线程。image-20230527134503383

  2. 当创建一个线程对象并且给定与线程关联的线程函数后,该线程就被启动,与主线程一起运行。线程函数一般情况下可按照以下三种方式提供:函数指针、lambda 表达式、函数对象。其中,lambda 表达式的本质其实是匿名的函数对象;除此之外,我们还可以使用包装器对象,其底层也是匿名的函数对象。

    void ThreadFunc(int a) { cout << "Thread1->" << a << endl; }
    
    class TF
    {
    public:
    	void operator()() { cout << "Thread3" << endl; }
    };
    
    int main()
    {
    	// 线程函数为函数指针
    	thread t1(ThreadFunc, 10);
    	// 线程函数为lambda表达式
    	thread t2([] {cout << "Thread2" << endl; });
    	// 线程函数为函数对象
    	TF tf;
    	thread t3(tf);
    
    	t1.join();
    	t2.join();
    	t3.join();
    	cout << "Main thread!" << endl;
    	return 0;
    }
    

    image-20230527135244892

    可以看到,上面程序的输出结果是混乱的,这是因为我们在创建多个线程时,这些线程的执行顺序完全是由操作系统来进行调度的,所以 thread 1/2/3 的输出顺序也是不确定的,只有 main thread 语句是确定最后打印的,因为这条语句在打印前需要等待其他从线程执行完毕。

  3. 我们可以通过 jionable() 函数来判断线程是否有效;如果是以下任意情况,则线程无效:采用无参构造函数构造的线程对象、线程对象的状态已经转移给其他线程对象、线程已经调用 jion 或者 detach 结束。

  4. 线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。

    void ThreadFunc1(int& x) { x += 10; }
    
    void ThreadFunc2(int* x) { *x += 10; }
    
    int main()
    {
    	int a = 10;
    	//在线程函数中对a修改,不会影响外部实参
    	//并且这里会发生int与int&类型不匹配的报错
    	//thread t1(ThreadFunc1, a);
    	//t1.join();
    	//cout << a << endl;
    
    	//如果想要通过形参改变外部实参时,必须借助std::ref()函数
    	thread t2(ThreadFunc1, std::ref(a));
    	t2.join();
    	cout << a << endl;
    
    	//其实现原理类似于地址的拷贝
    	thread t3(ThreadFunc2, &a);
    	t3.join();
    	cout << a << endl;
    
    	return 0;
    }
    

    image-20230527141430192

  5. 进程具有独立性,所以一个进程的退出并不会影响其他进程的正常执行;但是线程并不是独立的,一个进程下的多个线程共享进程的地址空间,所以一个线程的崩溃会导致整个进程崩溃。

this_thread 命名空间

C++11 thread 头文件中,除了有 thread 类,还存在一个 this_thread 命名空间,它其中保存了线程的一些相关属性信息,如下:image-20230527140357851


二、mutex 锁

多线程的线程安全问题

由于同一进程下的多个线程共享进程的地址空间,因此进程内的大部分资源都是共享的,比如内核区、堆区、共享区、数据区以及代码区。这虽然极大程度上缩小了进程间通信的成本,但同时也引发了共享资源的线程安全问题

线程安全问题是指多个线程并发/并行的访问/修改共享资源的数据,从而造成的数据混乱的问题。线程安全问题一般发生在全局变量上,因为全局变量保存在全局数据区,被所有线程共享;当然,局部变量也可能存在线程安全问题,只要能够以某种方式让其他线程访问到该变量即可,比如通过 lambda 表达式的引用捕捉。

int g_val = 0;

void Func1(int n)
{
	for (size_t i = 0; i < n; i++)
	{
		++g_val;
	}
}

void Func2(int n)
{
	for (size_t i = 0; i < n; i++)
	{
		++g_val;
	}
}

image-20230527144438385

可以看到,全局变量 g_val 的值和我们的预期值不一样,并且大家如果多次运行程序可以发现每次的运行结果也是不同的。造成这种结果的原因是C++语言层面的++操作一般都对应着三条汇编指令:

  1. 从内存中获取变量并存放到寄存器中。
  2. 对寄存器中的变量进行++操作。
  3. 将++之后的结果写回到内存中。

也就是说,++g_val 这个操作不是一步就能完成的,换句话说,++ 操作不是原子的。所以,当多个线程并行/并发的访问 g_val 时,就可能会出现线程 A 已经完成了前两步正准备将 ++ 后的结果 temp 写回内存时,线程 A 的时间片到了的情况;此时其他线程继续对 g_val 进行操作;而最后线程 A 被唤醒时会继续完成之前的操作,即将 g_val 的值改为 temp;这样做的结果就是线程 A 被阻塞的这段时间中其他线程对 g_val 的 ++ 操作全部无效了。

C++11 mutex 类

为了解决上面的线程安全问题,C++11 提供了 mutex 类;mutex 是一个可锁定的对象,用于在代码的关键部分需要独占访问时发出信号,防止具有相同保护的其他线程同时执行并访问相同的内存位置。

具体来说,当我们对程序中的某一部分代码加锁之后,线程如果想要执行这部分代码 (即访问这部分数据),必须先申请锁;当访问完毕后再释放锁。同时,一把锁在同一时间只能被一个线程持有,当其他线程再来申请锁时,会直接申请失败,从而阻塞或不断重新申请,直到持有锁的线程将锁释放。通过以上策略,我们就可以保证多个线程只能串行的访问临界区中的代码/数据,从而保证了其安全性

C++11 对操作系统的互斥锁接口进行了封装,产生出了 <mutex> 类;其中 <mutex> 中又包含了四个种类的锁,下面我们先来学习最重要的 mutex:image-20230527193112156

image-20230527175241423

mutex 的主要接口如下:

  • 构造:互斥锁仅支持无参构造,不支持拷贝构造。image-20230527175415259
  • lock:加锁函数。如果当前锁没有被任何线程持有,则当前线程持有锁并加锁;如果当前锁已经被其他线程持有,则当前线程阻塞直到持有锁的线程释放锁;如果当前互斥量被当前调用线程锁住,则会产生死锁。
  • try_lock:尝试加锁函数。如果当前锁没有被任何线程持有,则当前线程持有锁并加锁;如果当前锁已经被其他线程持有,则加锁失败返回 false,但当前线程并不会阻塞,而是跳过临界区代码继续向后执行;如果当前互斥量被当前调用线程锁住,则会产生死锁。
  • unlock:解锁函数。当前线程执行完临界区中的代码后释放锁,如果存在其他线程正在申请当前锁,则它们其中的一个将会持有锁并继续向后执行;当然,当前锁也可能重新被当前线程竞争得到。image-20230527180330249

recursive_mutex

recursive_mutex 和 mutex 大体相同,只是 recursive_mutex 允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权,释放互斥量时需要调用与该锁层次深度相同次数的 unlock()。

timed_mutex

相较于 mutex,timed_mutex 增加了两个两个成员函数 try_lock_for() 和 try_lock_until():

  • try_lock_for():接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与std::mutex 的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。
  • try_lock_until():接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。

需要注意的是,在实际开发中 try_lock_for() 和 try_lock_until() 并不常用,其中对于时间的控制也比较复杂,所以这里我们只需要了解即可,如果将来用到了,再去查阅 相关文档 也完全没问题。

现在,我们可以使用互斥锁 mutex 对之前的代码进行改进:

int g_val = 0;
mutex mtx;

void Func1(int n)
{
	mtx.lock();
	for (size_t i = 0; i < n; i++)
	{
		++g_val;
	}
	mtx.unlock();
}

void Func2(int n)
{
	mtx.lock();
	for (size_t i = 0; i < n; i++)
	{
		++g_val;
	}
	mtx.unlock();
}

image-20230527180947466

注意:有的同学可能会疑惑既然对 g_val 的 ++ 操作不是线程安全的,那为什么不只保护 ++g_val。其实只保护 ++ 操作是可以达到相同目的的,但是在当前场景下保护整个 for 循环程序的效率会更高 – 因为CPU的速度很快,如果我们对 ++g_val语句进行加锁,那么CPU就需要频繁的在 t1 和 t2 两个线程之间切换,并且 t1 和 t2 也需要频繁的加锁解锁,而这些操作都是要消耗资源的。


三、atomic 原子性操作

C++11 atomic 类

我们上文已经提到,多线程最主要的问题是共享数据带来的问题 (即线程安全)。如果共享数据都是只读的,那么没问题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦,比如数据混乱。

虽然我们可以通过加锁来对共享资源进行保护,但加锁存在一定缺陷,比如多个线程只能串行访问被锁包含的资源,会导致程序运行效率降低;同时,加锁如果控制不当还可能会造成死锁等问题。因此 C++11 引入了原子操作,原子操作即不可被中断的一个或一系列操作;C++11通过引入原子操作类型,使得线程间数据的同步变得更加高效。

C++11 原子操作包含在 <atomic> 头文件中:image-20230527183527111

由于原子类型通常属于 “资源型” 数据,多个线程只能访问单个原子类型的拷贝,因此在C++11中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及赋值重载等。image-20230527191524333

image-20230527191600538

atomic 类主要支持原子性的 ++、–、+、-、按位或、按位与 以及 按位异或操作:image-20230527183756143

atomic 类能够支持这些原子性操作本质是因为其底层对 CAS 操作进行了封装,可以简单理解为,atomic = CAS + while

CAS 操作

CAS (compare and swap) 是 CPU 硬件同步原语,它是支持并发的第一个处理器提供原子的测试并设置操作。CAS 操作包含三个操作数 – 内存位置(V)、预期原值(A)和新值 (B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则处理器不做任何操作。

我们还是以 ++g_val 操作为例,和一般的 ++ 操作不同,CAS 在会额外使用一个寄存器来保存讲寄存器中 g_val 修改之前的值 (预期原值),并且在将修改之后的值 (新值) 写回到内存时会重新取出内存中 g_val 的值与预期原值进行比较,如果二者相等,则将新值写入内存;如果二者不等,则放弃写入。

这样当线程 A 将新值写入到内存之前,如果有其他线程对 g_val 的值进行了修改,则内存中 g_val 的值就会与预期原值不等,此时操作系统就会放弃写入来保证整个 ++ 操作的原子性。

但单纯的放弃写入会导致可能当前 ++ 操作执行了但是 g_val 的值并不变;所以 C++ 对 CAS 操作进行了封装,即在 CAS 外面套了一层 while 循环,当新值成功写入时跳出循环,当新值写入失败时重新执行之前的取数据、修改数据、写回数据的操作,直到新值写入成功。这样做的优点是即实现了语言层面上 ++ 操作的原子性,解决了其线程安全问题;缺点是有一些 ++ 操作可能要重复执行多次才能成功,一定程度上影响程序效率,但还是比加锁解锁的效率要高。

注:上面只是对 atomic 底层原理的简单理解,atomic 底层逻辑控制肯定不是单纯的 CAS + while 这么简单的,但作为一般程序员这样理解也就够了;如果对 CAS 特别感兴趣的同学,这里我推荐一篇陈皓大佬关于 CAS 的文章 (RIP):无锁队列的实现 – coolshell.com

下面是基于 atomic 原子性操作改进的代码:

#include <atomic>

atomic<int> aval = 0;

void Func1(int n)
{
	for (size_t i = 0; i < n; i++)
	{
		++aval;
	}
}

void Func2(int n)
{
	for (size_t i = 0; i < n; i++)
	{
		++aval;
	}
}

image-20230527191303140


四、RAII 管理锁资源

在多线程环境下,如果想要保证某个变量的安全性,只要将其设置成对应的原子类型即可,即高效又不容易出现死锁问题。但是有些情况下,我们可能需要保证一段代码的安全性,那么就只能通过锁的方式来进行控制。

但是锁控制不好时,可能会造成死锁,最常见的比如在锁中间代码返回,或者在锁的范围内抛异常等,我们前面 特殊类设计 中懒汉模式 new 单例对象时就提到过这个问题。为了更好的管理锁,C++11采用 RAII 的方式对锁进行了封装,并提供了 lock_guard 和 unique_lock 两个类image-20230527194046881

lock_guard

lock_guard 是 C++11 中定义的模板类,如下:

template<class _Mutex>
class lock_guard
{
public:
	// 在构造lock_gard时,_Mtx还没有被上锁
	explicit lock_guard(_Mutex& _Mtx)
		: _MyMutex(_Mtx)
	{
		_MyMutex.lock();
	}
	// 在构造lock_gard时,_Mtx已经被上锁,此处不需要再上锁
	lock_guard(_Mutex& _Mtx, adopt_lock_t)
		: _MyMutex(_Mtx)
	{}
	~lock_guard() _NOEXCEPT
	{
		_MyMutex.unlock();
	}
	lock_guard(const lock_guard&) = delete;
	lock_guard& operator=(const lock_guard&) = delete;
private:
	_Mutex& _MyMutex;
};

可以看到,lock_guard 类模板通过 RAII 的方式对其管理的互斥量进行了封装;因此在需要加锁的地方,只需要实例化一个 lock_guard 对象,然后将锁交给该对象管理即可。lock_guard 对象调用构造函数成功上锁,出作用域前,lock_guard 对象要被销毁,调用析构函数自动解锁,从而有效避免死锁问题。

lock_guard 的缺陷是太单一,用户没有办法对该锁进行控制,因此C++11又提供了 unique_lock。

unique_lock

与 lock_guard 类似,unique_lock 类模板也是采用 RAII 的方式对锁进行了封装,并且也是以独占所有权的方式来管理 mutex 对象的上锁和解锁操作,即其对象之间不能发生拷贝。

与 lock_guard 不同的是,unique_lock 更加的灵活,提供了更多的成员函数:

  • 上锁/解锁操作:lock、try_lock、try_lock_for、try_lock_until 和 unlock。
  • 修改操作:移动赋值、交换 (swap:与另一个unique_lock对象互换所管理的互斥量所有权)、释放 (release:返回它所管理的互斥量对象的指针,并释放所有权)。
  • 获取属性:owns_lock (返回当前对象是否上了锁)、operator bool() (与 owns_lock() 的功能相同)、mutex (返回当前 unique_lock 所管理的互斥量的指针)。

unique_lock 和 lock_guard 最大的区别在于 lock_guard 无法手动释放和重新获取互斥锁,只能在创建时 lock,析构时 unlock,这在某些复杂的多线程编程场景中可能会受到一些限制。而 unique_lock 则提供了更加灵活和精细的互斥锁控制,unique_lock 可以在任何时刻手动地释放和重新获取互斥锁,并且支持不同的互斥锁处理策略,例如延时加锁、尝试加锁等

下面是基于互斥锁和 lock_guard 的代码:

#include <mutex>

int g_val = 0;
mutex mtx;

void Func1(int n)
{
    //构造时自动lock,出局部作用域析构时自动unlock
	lock_guard<mutex> lg(mtx);
	for (size_t i = 0; i < n; i++)
	{
		++g_val;
	}
}

void Func2(int n)
{
	lock_guard<mutex> lg(mtx);
	for (size_t i = 0; i < n; i++)
	{
		++g_val;
	}
}

五、condition_variable 条件变量

我们以一道面试题来引入条件变量:写一个程序,支持两个线程从 0 到 100 交替打印,一个打印奇数,一个打印偶数。

代码实现如下:

int main()
{
	int val = 0;
	//假设t1线程打印偶数,t2线程打印奇数
	thread t1([&]()
		{
			while (val <= 100)
			{
				if (val % 2 == 0)
				{
					cout << "t1:" << this_thread::get_id() << "->" << val << endl;
					++val;
				}
			}
		});

	thread t2([&]()
		{
			while (val <= 100)
			{
				if (val % 2 == 1)
				{
					cout << "t2:" << this_thread::get_id() << "->" << val << endl;
					++val;
				}
			}
		});

	t1.join();
	t2.join();

	return 0;
}

image-20230529220711549

可以看到,上面的程序是存在问题的,可能出现下面这种情况 – val 的值刚好为100,并且 t1 线程在 ++val 之前 t2 线程被调度,由于 val 为 100 所以 while 条件满足,而当 t2 执行 if 条件判断时 t1 已经完成了 val 的 ++ 操作,此时 val 为 101,if 条件成立,打印。

虽然我们可以通过将 t2 的 while 条件改为 while(val < 100)来避免上面的情况,但这仍然存在资源浪费的问题 – 例如下面这种场景:当 t1 满足条件正在运行并且在 ++val 之前其时间片到了被中断,此时 t2 被调度,但由于 t2 条件不满足,所以 t2 就会一直进行 while if 判断,直到时间片到被中断;这就导致 CPU 资源被浪费,那么能不能让线程在不满足条件时主动让出 CPU 资源,在条件满足时再被唤醒呢?这就需要依靠条件变量来实现了。

条件变量

条件变量 condition_variable 是 C++11 引入的同步机制之一,用于实现线程间的协作。它能够在多个线程之间传递信号,实现线程的等待和唤醒。通过使用 condition_variable,一个线程可以通知其它线程某个特定的事件已经发生,然后其它线程也可以相应地执行各自的操作。这种机制可以避免线程一直忙等待某个事件的发生,从而提高了应用程序的效率。image-20230529221454194

具体来说,condition_variable 主要由以下两个成员函数组成:

  • void wait(std::unique_lock<std::mutex>& lock): 该函数会使当前线程阻塞,直到另一个线程调用 notify_one()notify_all() 成功唤醒该线程为止。调用该函数时需要传递一个已经加锁的 unique_lock<std::mutex> 对象,函数内部会自动释放锁。当该函数返回时,锁会再次被该线程持有。
  • void notify_one() / void notify_all(): 这两个函数用于唤醒一个或全部等待中的线程。这些被唤醒的线程会尝试重新获得锁,并继续执行相应的操作。如果没有线程处于等待状态,则这两个函数不会产生任何影响。image-20230529221439431

注意:由于 condition_variable 本身并不持有锁,因此在使用时通常需要与 mutex 配合使用。具体来说,一般会创建一个 mutex 对象和一个 condition_variable 对象,并在等待某个条件时使用 unique_lock<std::mutex> 对象进行加锁和解锁。这样可以确保线程在等待时不会占用 CPU 资源。

如下,通过条件变量,当 t1/t2 线程条件不满足时,线程就会直接阻塞,让出 CPU 资源,直到被通知唤醒。下面是基于 condition_variable 改进的代码:

#include <mutex>
#include <condition_variable>
int main()
{
	int val = 0;
	mutex mtx;
	condition_variable cv;
	//假设t1线程打印偶数,t2线程打印奇数
	thread t1([&]()
		{
			while (val <= 100)
			{
				unique_lock<mutex> lg(mtx);
				//如果val是奇数,则阻塞当前线程
				while (val % 2 == 1)
				{
					cv.wait(lg);
				}
				cout << "t1:" << this_thread::get_id() << "->" << val << endl;
				++val;
                //++之后唤醒在当前条件下的另一个线程
				cv.notify_one();
			}
		});

	thread t2([&]()
		{
			while (val < 100)
			{
				unique_lock<mutex> lg(mtx);
				//如果val是偶数,则阻塞当前线程
				while (val % 2 == 0)
				{
					cv.wait(lg);
				}
				cout << "t2:" << this_thread::get_id() << "->" << val << endl;
				++val;
				cv.notify_one();
			}
		});

	t1.join();
	t2.join();

	return 0;
}

image-20230529221632302


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

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

相关文章

ChatGPT赋能Scrum实践

对于Scrum实践者来说&#xff0c;以ChatGPT为代表的的大语言模型是很有效的工具&#xff0c;但要用好这个工具需要一些技巧&#xff0c;本文介绍了60个适用于Scrum实践的提示&#xff0c;可以帮助ChatGPT有效输出适用于Scrum实践的内容。原文: 60 ChatGPT Prompts Plus Prompt …

chatgpt赋能python:Python中的SEO

Python 中的 SEO 搜索引擎优化(SEO)是指通过改进网站的结构和内容&#xff0c;使其在搜索引擎中的排名更高&#xff0c;从而吸引更多的访问者。Python是一种流行的编程语言&#xff0c;其在SEO中也占有重要的地位。 Python 中的关键词密度 搜索引擎通常会关注网页中的关键词…

.Net Core——用代码写代码?

想要用代码写代码&#xff0c;肯定是绕不开反射的。反射的概念相比都不陌生&#xff0c;只是应用多少就因人而异&#xff0c;今天分享一个代码生成器的思路&#xff0c;仅供参考&#xff0c;不要过分依赖哦。 思路分析 众所周知&#xff0c;利用反射可以在程序运行时获取到任…

【赏】java:编写一个SortedList接口

下面是SortedList接口的示例代码:import java.util.List;public interface SortedList<T extends Comparable<T>> extends List<T> {List<T> sort(List<T> list);int binarySearch(T value); }下面是对于SortedList接口的一个实现示例:import …

如何系统地学习IT技术

从基础技术到高级应用&#xff0c;IT技术发展迅速&#xff0c;我们需要建立系统性的学习方法&#xff0c;才能跟上它的速度。接下来&#xff0c;我将分享我的个人经验&#xff0c;介绍如何系统地学习IT技术&#xff0c;以及如何在快速学习过程中确保自己的技术水平越来越高。 …

加湿助眠仪语音IC芯片 白噪音语音方案 WTN6040F-8S

近年来&#xff0c;随着人们健康意识的不断增强&#xff0c;助眠仪逐渐成为了一种备受欢迎的家居健康设备。随着科技的不断升级&#xff0c;助眠仪也在不断地进行改进&#xff0c;以满足用户需求。其中&#xff0c;一种值得注意的改进就是助眠仪音乐播报芯片的应用。加湿助眠仪…

c++ 11标准模板(STL) std::map(九)

定义于头文件<map> template< class Key, class T, class Compare std::less<Key>, class Allocator std::allocator<std::pair<const Key, T> > > class map;(1)namespace pmr { template <class Key, class T, clas…

国产系统:麒麟之人大金仓数据库部署

一、基本信息和资源 1.1 查看服务器信息 [root7PGxjKPL4 ~]# cat /etc/*release Kylin Linux Advanced Server release V10 (Sword) DISTRIB_IDKylin DISTRIB_RELEASEV10 DISTRIB_CODENAMEjuniper DISTRIB_DESCRIPTION"Kylin V10" DISTRIB_KYLIN_RELEASEV10 DISTRI…

Springboot异步执行

异步执行 1.基于Async注解的方式在异步的方法上加 Async注解&#xff0c;调用接口后基于Async注解的方式优缺点: 2.使用 CompletableFuture 实现异步任务在实现类中创建CompletableFuture 类型的方法优缺点: 3.使用 TaskExecutor 实现异步任务优缺点: 1.基于Async注解的方式 As…

电子企业使用MES管理系统有没有弊端

随着制造业的不断现代化和数字化&#xff0c;越来越多的电子企业开始使用MES生产管理系统。电子企业MES系统是一种用于监控和管理制造业生产过程的软件&#xff0c;能够帮助企业提高生产效率、降低成本、提高质量和灵活性。然而&#xff0c;电子企业使用MES管理系统也存在一些弊…

Autohotkey按键映射

文章目录 功能前缀鼠标按键键盘按键虚拟键码和扫描码实操 功能前缀 尽管在初步使用中已经对常见热键做了说明&#xff0c;但为了本文的完整性&#xff0c;这里还是重新表述一下 #!^<^>!winAltCtrlShiftAlt Gr 其中&#xff0c;<, >为修饰符&#xff0c;用于区分…

使用不同类型注释的小肠路径跟踪深度强化学习

文章目录 Deep Reinforcement Learning for Small Bowel Path Tracking Using Different Types of Annotations摘要本文方法环境stateActionreward 实验结果 Deep Reinforcement Learning for Small Bowel Path Tracking Using Different Types of Annotations 摘要 小肠路径…

令人惊艳的六大算法(哈希表、分治算法、动态规划算法、贪心算法、回溯算法、图论算法)

当谈到计算机科学时&#xff0c;算法是一个重要的话题&#xff0c;因为它们能帮助解决很多问题。有些算法尤其令人惊艳&#xff0c;因为它们不仅高效&#xff0c;而且有着惊人的表现。在这篇文章中&#xff0c;我将分享一些我认为令人惊艳的高效算法。 一、哈希表 哈希表是一种…

通用寄存器-汇编复习(1)

弄清寄存器表达,原理和配件及汇编实验验证。 往期文章: 汇编语言基础-汇编复习(0)_luozhonghua2000的博客-CSDN博客 一个典型的 CPU(此处讨论的不是某一具体的 CPU)由运算器、控制器、寄存器(CPU工作原理)等器件构成,这些器件靠内部总线相连。前一章所说的总线,相对于 CP…

4、USB协议学习:USB的数据包结构

文章目录 数据包结构包(Packet)PID令牌包SETUP&OUT&IN令牌包SOF令牌包 数据包握手包ACK 握手包NAK 握手包 事务(Transaction)Setup事务OUT事务IN事务 传输(Transfer)控制传输中断传输批量传输同步传输/等时传输 端点 数据包结构 USB的通讯数据由多个传输组成&#xff0…

Docker 构建多架构 ARM、x86 AMD image镜像

在当今的计算环境中&#xff0c;各种异构计算设备和平台层出不穷&#xff0c;如何保证应用程序能够在不同的平台和设备上顺利运行&#xff0c;已成为亟待解决的问题。 以一款应用程序为例&#xff0c;它可能需要在 ARM、x86 或 s390x 等不同架构的设备上运行。由于这些设备所使…

Linux—实操篇:关机,重启和用户登录注销

1、关机和重启命令 1.1、基本介绍 shutdown -h now 立即关机 shutdown -h 1 一分钟后关机 shutdown -r now 立即重启 halt 立即关机&#xff0c;作用和上面一样 reboot 立即重启 sync 把内存数据同步到磁盘 注意&#xff1a;仅输入shutdown 默认执行&#xff08;shutdow…

Go开发学习 | 如何快速读取json/yaml/ini等格式的配置文件使用示例

欢迎关注「全栈工程师修炼指南」公众号 点击 &#x1f447; 下方卡片 即可关注我哟! 设为「星标⭐」每天带你 基础入门 到 进阶实践 再到 放弃学习&#xff01; “ 花开堪折直须折&#xff0c;莫待无花空折枝。 ” 作者主页&#xff1a;[ https://www.weiyigeek.top ] 博客&…

docker 数据持久化

目录 一、将本地目录直接映射到容器里&#xff08;运行成容器时候进行映射&#xff09; 二、数据卷模式 1、创建数据卷 2、查看数据卷列表&#xff0c;有哪些数据卷 3、查看某个数据卷 4、容器目录挂载到数据卷 5、数据卷的优势&#xff1a;多个容器共享一个数据卷 默认…

元宇宙应用领域-教育

教育是一个国家发展的基础&#xff0c;在科技发展的时代&#xff0c;元宇宙将会帮助教育行业实现跨越式发展。 元宇宙与教育的结合将会对传统的教学模式带来翻天覆地的变化。它能将线上教学、线下体验、远程互动等优势集于一身&#xff0c;也能把教师从繁重的重复劳动中解放出…