C++ : 线程库

C++ : 线程库

  • 一、线程thread
    • 1.1 thread类
      • 1.1.1 thread对象构造函数
      • 1.1.2 thread类的成员函数
      • 1.1.3 线程函数的参数问题
    • 1.2 this_thread 命名空间域
      • 1.2.1 chrono
  • 二、mutex互斥量库
    • 2.1 mutex的四种类型
      • 2.1.1 mutex 互斥锁
      • 2.2.2 timed_mutex 时间锁
      • 2.2.3 recursive_muetx 递归锁
      • 2.2.4 recursive_timed_muetx 时间递归锁
    • 2.2 RAII的加锁策略
      • 2.2.1 lock_guard 作用域锁
      • 2.2.2 unique_lock 独占锁
  • 三、condition_variable 条件变量
    • 3.1 wait系列函数
    • 3.2 notify系列函数
    • 3.3 简单示例
  • 四、atomic 原子性操作库
    • 4.1 atomic使用
    • 4.2 CAS

一、线程thread

操作线程需要头文件,头文件包含线程相关操作,内含两个内容:

  • thread类:操作线程的基本类
  • this_thread命名空间域:用于操作当前线程

1.1 thread类

1.1.1 thread对象构造函数

  1. 调用无参的构造函数
      thread提供了无参的构造函数,调用无参的构造函数创建出来的线程对象没有关联任何线程函数,即没有启动任何线程。
      由于thread提供了移动赋值函数,因此当后续需要让该线程对象与线程函数关联时,可以以带参的方式创建一个匿名对象,然后调用移动赋值将该匿名对象关联线程的状态转移给该线程对象。
void func(int n)
{for (int i = 0; i <= n; i++){cout << i << endl;}
}
int main()
{thread t1;//...t1 = thread(func, 10);t1.join();return 0;
}

使用场景
  实现线程池的时候就是需要先创建一批线程,但一开始这些线程什么也不做,当有任务到来时再让这些线程来处理这些任务。


  1. 调用带参的构造函数
template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);

参数说明:

  • fn:可调用对象,比如函数指针、仿函数、lambda表达式、被包装器包装后的可调用对象等。
  • args…:调用可调用对象fn时所需要的若干参数。

调用带参的构造函数创建线程对象,能够将线程对象与线程函数fn进行关联。

void func(int n)
{for (int i = 0; i <= n; i++){cout << i << endl;}
}
int main()
{thread t2(func, 10);t2.join();return 0;
}

  1. 调用移动构造函数
void func(int n)
{for (int i = 0; i <= n; i++){cout << i << endl;}
}
int main()
{thread t3 = thread(func, 10); //右边值对象t3.join();return 0;
}

1.1.2 thread类的成员函数

成员函数功能描述
join对该线程进行等待,在等待的线程返回之前,调用 join 函数的线程将会被阻塞
joinable判断该线程是否已经执行完毕,如果是则返回 true,否则返回 false
detach将该线程与创建线程进行分离,被分离后的线程不再需要创建线程调用 join 函数等待
get_id获取该线程的 id
swap将两个线程对象关联线程的状态进行交换

joinable函数同样也可以判断线程是否有效,下面情况都是线程无效的情况:

  • 采用无参构造函数构造的线程对象。(该线程对象没有关联任何线程)
  • 线程对象的状态已经转移给其他线程对象。(已经将线程交给其他线程对象管理)
  • 线程已经调用join或detach结束。(线程已经结束)

1.1.3 线程函数的参数问题

  线程函数的参数是以值拷贝方式拷贝到线程空间中的,就算线程函数的参数为引用类型,在线程函数中修改后也不会影响到外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。

#include <thread>
void ThreadFunc1(int& x)
{x += 10;
}int main()
{int a = 10;// 在线程函数中对a修改,不会影响外部实参,因为:线程函数参数虽然是引用方式,但其实际引用的是线程栈中的拷贝thread t1(ThreadFunc1, a);t1.join();cout << a << endl;
}

  要通过线程函数的形参改变外部的实参,**下面有三种方式:
1. 借助std::ref函数
  线程函数的参数类型为引用类型时,如果要想线程函数形参引用的是外部传入的实参,而不是线程栈空间中的拷贝,那么在传入实参时需要借助ref函数保持对实参的引用。

#include <thread>
void ThreadFunc1(int& x)
{x += 10;
}int main()
{int a = 10;// 如果想要通过形参改变外部实参时,必须借助std::ref()函数thread t2(ThreadFunc1, std::ref(a));t2.join();cout << a << endl;
}

2. 地址的拷贝
  将线程函数的参数类型改为指针类型,将实参的地址传入线程函数,此时在线程函数中可以通过修改该地址处的变量,进而影响到外部实参。

#include <thread>
void ThreadFunc2(int* x)
{*x += 10;
}int main()
{int a = 10;// 地址的拷贝thread t3(ThreadFunc2, &a);t3.join();cout << a << endl;return 0;
}

3. 借助lambda表达式
   将lambda表达式作为线程函数,利用lambda函数的捕捉列表,以引用的方式对外部实参进行捕捉,此时在lambda表达式中对形参的修改也能影响到外部实参。

#include <thread>int main()
{int a = 10;// 借助lambda表达式thread t3([&a]{a+=10;});t3.join();cout << a << endl;return 0;
}

1.2 this_thread 命名空间域

std::this_thread 是一个命名空间,用于访问当前线程。它提供了一组函数来操作当前线程。

函数名功能描述
yield当前线程"放弃"执行,让操作系统调度另一线程继续执行
get_id返回当前线程的 ID
sleep_until让当前线程休眠到一个具体时间点
sleep_for让当前线程休眠一个时间段

get_idyield都可以直接执行,不用传入参数。而后两个函数与时间相关,要用到C++封装的时间库chrono

1.2.1 chrono

是一个头文件,内包含chrono命名空间域,该域内部封装了各种时间的相关操作。

  1. std::chrono::system_clock:系统时钟,表示从 Unix 纪元开始的时间(1970 年 1 月 1 日 00:00:00 UTC)。
  2. std::chrono::steady_clock:稳定时钟,表示从程序启动开始的时间。

这两个时钟都有一个now成员函数,返回当前的时间。但是system_clock会受到系统时钟影响,如果用户调整了系统时间,就有可能造成时间错误,而稳定时钟不受系统时钟影响。如下:

auto t1 = std::chrono::system_clock::now();
auto t2 = std::chrono::steady_clock::now();

这两个函数都返回一个time_point类型,表示当前时间点。

duration用于表示一个时间段,这个类的用法比较复杂,因此C++封装了一些可以直接使用的类:

  • std::chrono::nanoseconds (纳秒)
    std::chrono::microseconds (微秒)
    std::chrono::milliseconds (毫秒)
    std::chrono::seconds (秒)
    std::chrono::minutes (分钟)
    std::chrono::hours (小时)

这些类都是typedef后的duration,如果想要表示一个时间段,直接传数字即可:

auto dur1 = std::chrono::seconds(3); // 3秒
auto dur2 = std::chrono::minutes(5); // 5分钟

sleep_until需要传入一个时间点time_point,比如想要睡眠10秒,就可以用当前时间 + 10秒得到一个时间点,再用sleep_until完成睡眠:

auto t1 = std::chrono::steady_clock::now(); // 获取当前时间	
auto dur = std::chrono::seconds(10); // 获取十秒时间段
auto t2 = t1 + dur; // 时间点 + 时间段 = 时间点,十秒后std::this_thread::sleep_until(t2); // 睡眠到 10 秒后

sleep_for需要传入一个时间段duration,同样的睡眠十秒:

auto dur = std::chrono::seconds(10); // 获取十秒时间段
std::this_thread::sleep_for(dur); // 睡眠到 10 秒后

二、mutex互斥量库

mutex的种类:

  1. mutex:互斥锁
  2. recursive_muetx:递归锁
  3. timed_mutex:时间锁
  4. recursive_timed_muetx:时间递归锁

两种基于RAII的加锁策略:

  1. lock_guard:作用域锁
  2. unique_lock:独占锁

2.1 mutex的四种类型

2.1.1 mutex 互斥锁

mutex锁是C++11提供的最基本的互斥量,mutex对象之间不能进行拷贝,也不能进行移动。
成员函数如下:

成员函数功能描述
lock对互斥量进行加锁
unlock对互斥量进行解锁,释放互斥量的所有权
try_lock对互斥量尝试进行加锁

线程函数调用lock时,可能会发生以下三种情况:

  • 如果该互斥量当前没有被其他线程锁住,则调用线程将该互斥量锁住,直到调用unlock之前,该线程一致拥有该锁。
  • 如果该互斥量已经被其他线程锁住,则当前的调用线程会被阻塞。
  • 如果该互斥量被当前调用线程锁住,则会产生死锁(deadlock)。

线程调用try_lock时,类似也可能会发生以下三种情况:

  • 如果该互斥量当前没有被其他线程锁住,则调用线程将该互斥量锁住,直到调用unlock之前,该线程一致拥有该锁。
  • 如果该互斥量已经被其他线程锁住,则try_lock调用返回false,当前的调用线程不会被阻塞。
  • 如果该互斥量被当前调用线程锁住,则会产生死锁(deadlock)。

简单实例:

int num = 0;void test(int n, std::mutex& mtx)
{for (int i = 0; i < n; i++){mtx.lock();num++;mtx.unlock();}
}int main()
{std::mutex mtx;std::thread t1(test, 2000, std::ref(mtx));std::thread t2(test, 2000, std::ref(mtx));t1.join();t2.join();std::cout << "num = " << num << std::endl;return 0;
}

2.2.2 timed_mutex 时间锁

时间锁就是限定每次申请锁的时长,如果超过一定时间没有申请到锁,就返回。
成员函数:

成员函数功能描述
lock对互斥量进行加锁
unlock对互斥量进行解锁,释放互斥量的所有权
try_lock对互斥量尝试进行加锁
try_lock_until如果到指定时间还没申请到锁就返回false,申请到锁返回true
try_lock_for如果一段时间内没申请到锁就返回false,申请到锁返回true
  • try_lock_for:接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间之内还是没有获得锁),则返回false。
  • try_lock_untill:接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间点到来时还是没有获得锁),则返回false。

简单示例:

int num = 0;void test(int n, std::timed_mutex& mtx)
{while (n){bool ret = mtx.try_lock_for(std::chrono::microseconds(1));if (ret){num++;n--;mtx.unlock();}else{std::cout << "加锁超时" << std::endl;}}
}int main()
{std::timed_mutex mtx;std::thread t1(test, 2000, std::ref(mtx));std::thread t2(test, 2000, std::ref(mtx));t1.join();t2.join();std::cout << "num = " << num << std::endl;return 0;
}

2.2.3 recursive_muetx 递归锁

recursive_mutex叫做递归互斥锁,该锁专门用于递归函数中的加锁操作。

  • 如果在递归函数中使用mutex互斥锁进行加锁,那么在线程进行递归调用时,可能会重复申请已经申请到但自己还未释放的锁,进而导致死锁问题。
  • 而recursive_mutex允许同一个线程对互斥量多次上锁(即递归上锁),来获得互斥量对象的多层所有权,但是释放互斥量时需要调用与该锁层次深度相同次数的unlock。

简单实例:

int num = 0;void test(int n, std::recursive_mutex& mtx)
{if (n <= 0)return;mtx.lock();test(n - 1, mtx);mtx.unlock();
}int main()
{std::recursive_mutexmtx;std::thread t1(test, 2000, std::ref(mtx));t1.join();std::cout << "num = " << num << std::endl;return 0;
}

如果是使用mutex.lock()时,申请不到锁,不论是谁占有这把锁,都会陷入阻塞,直到锁被释放。recursive_mutex则会记录是谁占有这把锁,在recursive_mutex.lock()时,会检查申请锁的线程和占有锁的线程是不是同一个,如果是同一个,则直接申请成功,因此可以避免递归死锁。

2.2.4 recursive_timed_muetx 时间递归锁

与时间和递归锁效果相加。


2.2 RAII的加锁策略

2.2.1 lock_guard 作用域锁

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

template <class Mutex>
class lock_guard;

C++是用的是RAII方法进行加锁:

  • 在需要加锁的地方,用互斥锁实例化一个lock_guard对象,在lock_guard的构造函数中会调用lock进行加锁。
  • 当lock_guard对象出作用域前会调用析构函数,在lock_guard的析构函数中会调用unlock自动解锁

简单示例:

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;mutex mtx;
bool Stop()
{return false;
}void func()
{// 保护一段匿名的代码区间{lock_guard<mutex> lg(mtx); // 调用构造函数构造if (!Stop())return; // 调用析构函数析构}}// 生命周期结束后调用析构函数析构
int main()
{func();return 0;
}

模拟实现lock_guard:

namespace JRH
{template<class Mutex>class lock_guard{public:lock_guard(Mutex& mtx):_mtx(mtx){_mtx.lock(); // 加锁}~lock_guard(){_mtx.unlock();}// 防拷贝lock_guard(const Mutex&) = delete;lock_guard& operator=(const Mutex&) = delete;private:Mutex& _mtx;};
}

1.lock_guard类中包含一个锁成员变量(引用类型),这个锁就是每个lock_guard对象管理的互斥锁。
2.调用lock_guard的构造函数时需要传入一个被管理互斥锁,用该互斥锁来初始化锁成员变量后,调用互斥锁的lock函数进行加锁。
3.lock_guard的析构函数中调用互斥锁的unlock进行解锁。
4.需要删除lock_guard类的拷贝构造和拷贝赋值,因为lock_guard类中的锁成员变量本身也是不支持拷贝的。(防拷贝)

2.2.2 unique_lock 独占锁

lock_guard的可操作性很低,只有构造和析构两个函数,也就是只有自动释放锁的能力。而unique_lock功能更加丰富,而且可以自由操作锁。

unique_lock在构造时,可以传入一把锁,在构造的同时会对该锁进行加锁。在unique_lock析构时,判断当前的锁有没有加锁,如果加锁了就先释放锁,后销毁对象。

而在构造与析构之间,也就是整个unique_lock的生命周期,可以自由的加锁解锁:

  • lock:加锁
    unlock:解锁
    try_lock:如果没上锁就加锁,上锁了就返回
    try_lock_until:如果到指定时间还没申请到锁就返回false,申请到锁返回true
    try_lock_for:如果一段时间内没申请到锁就返回false,申请到锁返回true

提供了以上五个接口,也就是说可以作用于前面的任何一款锁。另外的unique_lcok还允许赋值operator=,调用赋值时,如果当前锁没有持有锁,那么直接拷贝。如果当前锁持有锁,那么把锁的所有权转移给新的unique_lcok,自己不再持有锁。


三、condition_variable 条件变量

谈到锁,自然也要谈条件变量,这是线程同步的重要手段,C++将条件变量放在头文件<condition_variable>中。

条件变量库有很多函数,但总体分为两类函数,分别是wait系列函数和notify系列函数。

3.1 wait系列函数

wait函数提供了两个不同版本的接口:

void wait (unique_lock<mutex>& lck);template <class Predicate>
void wait (unique_lock<mutex>& lck, Predicate pred);

其有两个重载,第一个只有一个参数,也就是我刚刚提到的只要传入一个unique_lock<mutex>。第二个重载允许传入第二个参数pred,这是一个可调用对象,用于作为条件变量的判断值。

wait的第二个参数要求是一个可调用对象,返回值类型伪bool,作用如下:

  • 返回true:表示条件成立,wait直接返回,不进入等待队列
  • 返回false:表示条件不成立,wait阻塞,进入等待队列直到被唤醒

为什么调用wait系列函数时需要传入一个互斥锁

  • 因为wait系列函数一般是在临界区中调用的,为了让当前线程调用wait阻塞时其他线程能够获取到锁,因此调用wait系列函数时需要传入一个互斥锁,当线程被阻塞时这个互斥锁会被自动解锁,而当这个线程被唤醒时,又会自动获得这个互斥锁。
  • 因此wait系列函数实际上有两个功能,一个是让线程在条件不满足时进行阻塞等待,另一个是让线程将对应的互斥锁进行解锁。

wait_for和wait_until函数

  • wait_for:进入条件变量的等待队列,一定时间后如果没有被唤醒,则不再等待返回false
  • wait_until:进入条件变量的等待队列,到指定时间后如果没有被唤醒,则不再等待返回false
  • 这两个与时间相关的等待,分别要传入时间段duration和时间点time_point。至于为什么要传入一把锁,这属于并发编程的知识,就不在博客中讲解了。

3.2 notify系列函数

条件变量下可能会有多个线程在进行阻塞等待,这些线程会被放到一个等待队列中进行排队。
notify系列成员函数的作用就是唤醒等待的线程,包括notify_one和notify_all。

  • notify_one:唤醒等待队列中的首个线程,如果等待队列为空则什么也不做。
  • notify_all:唤醒等待队列中的所有线程,如果等待队列为空则什么也不做。

3.3 简单示例

下面我们通过 实现两个线程交替打印1-100数 的例子来更好的认识条件变量

int n = 0;
bool flag = true;std::mutex mtx;
std::condition_variable cv; // 条件变量void func(bool run) // run用于标识是否轮到当前线程输出
{while (n < 100){std::unique_lock<std::mutex> lock(mtx);while (flag != run) // 使用while代替if,防止伪唤醒cv.wait(lock);  // 没轮到当前线程,进入条件变量等待std::cout << n << std::endl;n++;flag = !flag;cv.notify_one();}
}int main()
{std::thread t1(func, true);  // falg == true 输出偶数std::thread t2(func, false); // falg == false 输出奇数t1.join();t2.join();return 0;
}

while循坏防止伪唤醒。


四、atomic 原子性操作库

在多线程情况下要加锁,就是因为很多操作不是原子性的。但是有一些简单的操作,比如num++,每次都加锁解锁,性能必然会降低。因此C++又提供了原子库,其实现了简单操作的原子化,一些简单的++,–等操作都实现了原子化,可以在不加锁的情况下保证线程安全,需要头文件<atomic>

4.1 atomic使用

在C++11中,程序员不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥的
访问。更为普遍的,程序员可以使用atomic类模板,定义出需要的任意原子类型。

atomic<类型> 变量名;

简单示例:

std::atomic<int> num = 0;void test(int n)
{for (int i = 0; i < n; i++){num++;}
}int main()
{std::thread t1(test, 2000);std::thread t2(test, 2000);t1.join();t2.join();std::cout << "num = " << num << std::endl;return 0;
}

atomic类成员函数::

  • operator++operator--,自增自减的操作是原子的。
  • fetch_* :
    • fetch_add:原子性,增加指定的值
    • fetch_sub:原子性,减少指定的值
    • fetch_and:原子性,与指定值按位与
    • fetch_or:原子性,与指定值按位或
    • fetch_xor:原子性,与指定值按位异或
std::atomic<int> num = 3;
num.fetch_add(5);
例如fetch_add,用于实现对一个原子类型增加指定值,
以上代码完成了3 + 5的计算,且过程是原子性的
  • store :用于设定原子类型为指定值
std::atomic<int> num = 3;
num.store(100);
--num.store(100)相当于num = 100,但是过程是原子性的
  • load用于获取原子类型当前的值,也是原子的。

  • operator T是隐式类型转换,也就是从atomic转化为T类型,此时就可以把原子类型当作一般类型来使用了,不过要注意的是,隐式转换后就是一般类型,不再具有原子性。


4.2 CAS

CAS(Compare and Set)是一种原子操作,用于实现并发编程中的无锁同步。它通过比较内存位置的当前值与预期值,如果相等则更新为新值,从而避免竞态条件。CAS操作广泛应用于多线程环境。

C++之所以可以实现变量的原子操作,是基于CAS的原子操作,这是一个硬件级别的操作,其涉及三个操作数:

  • 内存位置
  • 预期值
  • 更新值

操作流程为:读取内存位置的当前值,判断是否与预期值相等,如果相等,将其变为更新值,如果不相等,返回当前值。

下面是一个CAS的伪代码:

boolean CAS(内存R, 寄存器A, 寄存器B) {if (R == A) {  // 比较内存位置R的当前值是否等于寄存器A中的预期值R = B;     // 如果相等,则将内存位置R更新为寄存器B中的新值return true;  // 返回操作成功}return false;  // 否则返回操作失败
}
  • 参数说明
    • 内存R:共享变量的内存位置。
    • 寄存器A:预期值(expected value)。
    • 寄存器B:新值(new value)。
  • 工作流程
    1. 首先,读取内存位置RRR的当前值。
    2. 比较当前值是否等于预期值AAA
    3. 如果相等,原子性地将RRR更新为BBB,并返回true
    4. 如果不相等,说明RRR已被其他线程修改,操作失败,返回false
  • 原子性保证:在实际硬件中,CAS操作由一条CPU指令(如x86的CMPXCHG)实现,确保整个比较和更新过程不可中断。

CAS操作的优缺点
CAS操作在并发编程中高效,但也存在一些限制:

  • 优点:避免线程阻塞,减少上下文切换开销,适用于高并发场景(如计数器或锁实现)。
  • 缺点
    • ABA问题:如果RRR的值从AAA变为BBB后又变回AAA,CAS会误以为值未变,可能导致逻辑错误。解决方法包括添加版本号(如Java的AtomicStampedReference)。
    • 仅支持单个变量的原子操作,对多变量复合操作需额外机制(如循环CAS)。
    • 在高竞争环境下,频繁失败会消耗CPU资源。

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

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

相关文章

idea的使用小技巧,个人向

idea的使用小技巧&#xff0c;个人向 一、前言二、过程1、显示内存的使用情况2、去掉xml文件中的黄色背景3、显示所有打开文件4、显示工具栏到菜单下面5、使用JDK8 一、前言 每次重装idea都需要重新设置一下&#xff0c;这里做个记录。 这些技巧只是个人感觉的好用 演示用的…

debian及衍生发行版apt包管理常见操作

好的&#xff0c;这是 Debian 及其衍生版&#xff08;如 Ubuntu&#xff09;使用的 apt 包管理器的常用命令速查表。 一点说明&#xff1a;apt 是新一代的命令行工具&#xff0c;整合了 apt-get 和 apt-cache 的常用功能&#xff0c;并提供了更友好的交互体验。本表主要使用现…

vue调用函数

好的&#xff0c;我们来讲解如何在 Vue 模板中调用函数。您提供的代码是一个非常棒的、很实用的例子。 在 Vue 模板中&#xff0c;你可以在两个主要地方调用函数&#xff1a; 文本插值中&#xff1a;像 {{ formatDate(date) }} 这样&#xff0c;函数的返回值会作为文本被渲染到…

前端常用构建工具介绍及对比

打包构建工具是现代软件开发中必不可少的,它们帮助开发者自动化构建、打包、部署等流程,提升开发效率。不过,不同时期构建工具略有差异。 每个构建工具都有其擅长的领域,我们需要知道其优势,才能在我们实际开发中选择合适的构建工具进行构建处理。 1. Gulp Gulp 是一个…

Web后端开发-SpringBootWeb入门、Http协议、Tomcat

文章目录Web后端开发简介SpringBootWeb入门HTTP协议HTTP-概述HTTP-请求协议HTTP-响应协议HTTP-协议解析Web服务器-Tomcat简介基本使用SpringBootWeb入门程序解析Web后端开发简介 SpringBootWeb入门 package com.wuxuan.javaweb_wushuang.controller;import org.springframework…

物联网通信技术全景剖析:从LoRa到5G的深度对比与选型指南

物联网通信技术全景剖析&#xff1a;从LoRa到5G的深度对比与选型指南在万物互联时代&#xff0c;选择合适的通信技术如同为设备构建“神经网络”。本文将深入解析七大主流物联网通信技术&#xff0c;助您在技术选型中精准决策。一、低功耗广域网&#xff08;LPWAN&#xff09;技…

俄罗斯方块AI深度解析:从算法原理到实现细节

俄罗斯方块AI深度解析:从算法原理到实现细节 前言 俄罗斯方块,这个诞生于1984年的经典游戏,至今仍然是人工智能研究领域的热门课题。当简单的几何形状在网格中不断下落时,看似简单的规则背后却隐藏着复杂的策略决策问题。本文将深入剖析一个基于Python实现的俄罗斯方块AI…

Spring Boot 框架创建一个简单的后端接口,并介绍如何使用 Apifox 连接该接口

目录 一、配置 二、使用 IntelliJ IDEA 创建 Spring Boot 项目 1.打开 IntelliJ IDEA&#xff0c;选择 File > New > Project 2.在左侧面板选择 Spring Initializr&#xff0c;项目名称设置为HelloWorldAPI 3.点击 Create 完成项目创建 三、创建控制器类 四、运行项…

CICD[导航]、docker+gitlab+harbor+jenkins从安装到部署

一、安装 CICD[软件安装]&#xff1a;docker安装gitlab-CSDN博客 CICD[软件安装]&#xff1a;ubuntu安装jenkins-CSDN博客 CICD[软件安装]&#xff1a;ubuntu安装私有镜像仓库-Harbor-CSDN博客 CICD[软件安装]&#xff1a;ubuntu24安装Docker-CSDN博客 二、镜像执行 CICD[…

深度学习图像分类数据集—蘑菇识别分类

该数据集为图像分类数据集&#xff0c;适用于ResNet、VGG等卷积神经网络&#xff0c;SENet、CBAM等注意力机制相关算法&#xff0c;Vision Transformer等Transformer相关算法。 数据集信息介绍&#xff1a;蘑菇识别分类&#xff1a;[Agaricus, Amanita, Boletus, Cortinarius, …

iOS 多线程导致接口乱序?抓包还原 + 请求调度优化实战

在一次性能优化过程中&#xff0c;我们将 iOS App 内多处请求改为并行处理&#xff0c;以提高页面加载速度。但上线后却收到部分用户反馈&#xff1a;进入页面后数据加载错乱&#xff0c;有时展示前一次页面内容&#xff0c;有时同一个接口请求重复返回不同内容。 日志仅显示正…

PDFBox 在 Linux 报 “No glyph for U+535A (博)” —— 一次子集化踩坑与完整排查清单

PDFBox 在 Linux 报 “No glyph for U535A (博)” —— 一次子集化踩坑与完整排查清单关键词&#xff1a;PDFBox、PDType0Font、子集嵌入&#xff08;subset embedding&#xff09;、SimHei、思源黑体、字体回退1. 背景业务场景 后端使用 Apache PDFBox 填充含 AcroForm 的中文…

网安系列【8】之暴力破解入门

文章目录 引用资料一 什么是暴力破解&#xff1f;二 暴力破解的工作原理三 暴力破解的类型3.1 传统暴力破解3.2 字典攻击3.3 混合攻击3.4 彩虹表攻击 四 暴力破解实战演示4.1 环境和工具4.2 破解操作 五 防御暴力破解的策略六 暴力破解的相关法律七 延伸学习总结 引用资料 Bur…

使用tensorflow的线性回归的例子(四)

与经典线性回归比较 import matplotlib.pyplot as plt %matplotlib inline import tensorflow as tf import numpy as np from sklearn.linear_model import LinearRegression #from sklearn.datasets.samples_generator import make_regression Xdata np.array([4.0, …

服务器中故障转移机制是指什么意思?

在企业服务器和数据中心当中&#xff0c;电源冗余机制和故障转移机制是保障系统高可用性和稳定性的重要组成部分&#xff0c;电源故障转移系统可以帮助企业有效减少服务器因为硬件故障导致业务中断的情况&#xff0c;本文就来详细了解一下服务器中故障转移机制。服务器中的故障…

rook-ceph的osd没有启动rook-ceph-osd-prepare状态异常处理

rook-ceph搭建好ceph之后&#xff0c;查看ceph集群状态&#xff0c;发现三节点只有两个osd状态正常注&#xff1a;这里是已经恢复后的截图。 使用kubectl get pod -n rook-ceph查看pod都是处于运行状态 rook-ceph-osd-prepare也都是Completed没问题&#xff0c;实际使用kubectl…

ubuntu手动编译VTK9.3 Generating qmltypes file 失败

​在Ubuntu上手动编译VTK 9.3时&#xff0c;可能会遇到 Generating qmltypes file失败的问题。这个问题通常与VTK在处理Qt依赖时发生的错误有关。以下是解决该问题的详细步骤和相关解释。一、确保系统依赖正确安装在编译VTK之前&#xff0c;需要确保所有依赖项已经正确安装&…

计算机科学导论(1)哈佛架构

文章目录一、哈佛架构的定义与起源二、哈佛架构的核心组成与工作原理1. **物理结构&#xff1a;独立的存储与总线**2. **工作流程&#xff1a;并行处理的实现**三、哈佛架构与冯诺依曼架构的对比四、哈佛架构的优缺点分析1. **优势**2. **局限性**五、哈佛架构的实际应用场景1.…

VBScript 安装使用教程

一、VBScript 简介 VBScript&#xff08;Visual Basic Scripting Edition&#xff09;是微软推出的一种轻量级脚本语言&#xff0c;语法类似于 Visual Basic&#xff0c;广泛应用于系统管理、自动化脚本、网页客户端&#xff08;IE 专属&#xff09;以及 Windows 批处理等场景…

RSTP 拓扑收敛机制

RSTP拓扑收敛触发条件 RSTP中检测到拓扑变化只有一个标准&#xff1a;以一个非边缘端口迁移到Forwarding状态 收敛过程 为本交换设备的所有非边缘指定端口和根端口启动TC While Timer&#xff0c;该计时器是Hello Time&#xff08;默认2s&#xff09;的两倍&#xff0c;然后…