C++ --- 多线程的使用

目录

一.什么是线程?

线程的特点:

线程的组成:

二.什么是进程?

进程的特点:

进程的组成:

三.线程与进程的关系:

四.C++的Thread方法的使用:

1.创建线程:

2.join()方法:

3.detach()方法:

detach()方法细节:

如何做到隔离线程?

4.joinable():

5.native_handle():

6.hardware_concurrency():

7.线程的休眠:

(1)std::this_thread::sleep_for():

 (2)std::this_thread::sleep_until():

8.线程的局部存储(Thread Local Storage): 

注意事项:

五.线程的同步与互斥:

1.互斥量(mutex):

2.C++的其他锁的拓展介绍:

(1)std::recursive_mutex:

(2)std::timed_mutex:

(3)std::shared_mutex:

特点:

3.死锁:

死锁的产生通常需要满足以下四个条件:

如何避免死锁?

解决方法:

4.条件变量(Condition Variable):

工作机制:

5.call_once的使用:

std::call_once 的工作机制:

6.atomic 原子操作:

原子操作的常用方法: 

 六.线程池的构建与使用:

 1.首先创建一个线程池类:

(1)创建成员变量:

(2) 构造函数:

什么是 function 函数模板? 

(3)析构函数:

(4)添加任务 enqueue() 方法:

什么是 std::bind ?

什么是 std::forward ?

 什么是 std::move ?

std::move 使用注意事项:

2.创建 main 内:


这篇博客主要讲述线程以及线程池等相关技术,狂码两万六千字,希望您可以耐心观看,如有不足以及不解,欢迎评论区留言跟我商讨,本博客为学习笔记,要是对您有帮助也请您给个三连支持一下,话不多说进入正题 ->

一.什么是线程?

线程(Thread)是进程中的一个执行单元,是操作系统能够进行运算调度的最小单位。线程是程序执行的基本单位,它包含了执行所需的所有信息,如程序计数器、栈和局部变量等。

优点:线程的创建和销毁开销小,线程之间可以方便地共享数据,减少了数据传输的开销,通过多线程可以充分利用多核处理器,提高程序的执行效率。

缺点:多个线程共享资源可能导致数据竞争和同步问题,需要使用锁等机制来保护共享数据。多线程程序的调试和错误排查相对复杂,可能会出现死锁、竞争条件等问题。

线程的特点:

  1. 轻量级:线程是比进程更小的执行单位,创建和销毁的开销较小。
  2. 共享资源:同一进程中的多个线程可以共享进程的资源,如内存空间、文件描述符等。
  3. 独立执行:每个线程拥有自己的执行栈和程序计数器,可以独立执行任务。
  4. 并发执行:多个线程可以并发执行,从而提高程序的执行效率。

线程的组成:

  • 线程ID:每个线程都有一个唯一的标识符。
  • 程序计数器:指向当前线程执行的指令。
  • :保存线程的局部变量和函数调用记录。
  • 寄存器:存储线程的上下文信息。

二.什么是进程?

进程(Process)是一个正在执行的程序的实例,具有自己独立的内存空间和系统资源。进程是操作系统进行资源分配和调度的基本单位。

优点:进程之间互不干扰,安全性高。一个进程的崩溃不会影响其他进程。

缺点:进程的创建和销毁需要较大的开销。进程之间的通信相对复杂,通常需要使用进程间通信(IPC)机制。

进程的特点:

  1. 资源独立:每个进程都有自己的内存空间和资源,进程之间相互独立,互不干扰。
  2. 开销大:进程的创建、销毁和切换开销相对较大。
  3. 更高的隔离性:进程之间不直接共享内存,安全性更高。

进程的组成:

  • 进程ID:每个进程都有一个唯一的标识符。
  • 内存空间:包括代码段、数据段、堆和栈。
  • 程序状态:记录进程的当前状态(就绪、运行、阻塞等)。
  • 资源信息:记录进程所使用的系统资源(文件描述符、信号量等)。

三.线程与进程的关系:

进程是线程的容器:一个进程可以包含多个线程。所有线程共享该进程的资源(如内存),但每个线程有自己的栈和寄存器。

调度与切换:进程切换开销较大,因为需要保存和恢复整个进程的上下文。而线程切换开销较小,因为只需保存和恢复线程的上下文。

并发与并行:多个进程可以并行执行,多个线程在同一进程内可以并发执行。多线程程序比多进程程序更易于实现并发。

四.C++的Thread方法的使用:

通常建议在较大的项目中和公共代码库中使用 std:: 前缀,以提高代码的可读性和可维护性。在小型项目或练习代码中,如果确实没有命名冲突,可以适当使用 using namespace std;,但最好在源文件的开头避免使用全局命名空间污染。一般来说,保持良好的命名空间管理是最佳实践。 

1.创建线程:

线程一共有三种创建方法:

  1. 函数指针:thread thread_name(函数方法名,参数1,参数2,....);
  2. 函数对象:thread thread_name(函数方法名(),参数1,参数2,....);
  3. Lambda表达式:thread thread_name([](typename name){...})
#include <iostream>
#include <thread>
using namespace std;

// 一个简单的函数,作为线程的入口函数
void thone1(int Z) {
    for (int i = 0; i < Z; i++) {
        cout << "线程使用函数指针作为可调用参数\n";
    }
}
//引用类型变量参数
void thone11(int& Z) {
    for (int i = 0; i < Z; i++) {
        cout << "线程使用函数指针作为可调用参数\n";
    }
}
// 可调用对象的类定义
class Threadtwo {
public:
    void operator()(int x) const {
        for (int i = 0; i < x; i++) {
            cout << "线程使用函数对象作为可调用参数\n";
        }
    }
};

int main() {
    cout << "线程 1 、2 、3 独立运行" << endl;

    // 使用函数指针创建线程
    thread th1(thone1, 3);
    // 传递引用类型参数需要使用ref函数进行传递
    // 使用ref函数将num转换成引用类型变量
    int num = 0;
    thread th11(thone11,ref(num));
    // 使用函数对象创建线程
    thread th2(Threadtwo(), 3);
    // 使用 Lambda 表达式创建线程
    thread th3([](int x) {
        for (int i = 0; i < x; i++) {
            cout << "线程使用 lambda 表达式作为可调用参数\n";
        }
    }, 3);

    return 0;
}

thread 对象不能被复制,因为线程的资源管理需要独占访问。尝试复制 std::thread 对象会导致编译错误。如果需要在多个对象间共享线程,通常需要使用智能指针 std::shared_ptr<std::thread>。 

2.join()方法:

join() 方法在 C++ 中用于等待线程的结束

  • 等待线程完成:当调用 join() 方法时,主线程(即 main 函数所在的线程)会阻塞,直到被调用的线程执行完毕。

  • 资源管理:线程在执行完后会被系统资源回收。如果不调用 join(),主线程在结束时可能会强行终止,而被调用的线程可能还在运行,导致程序的未定义行为。因此,调用 join() 确保了线程资源的正确管理。

#include <iostream>
#include <thread>
using namespace std;

void threadFunction() {
    std::cout << "线程正在运行..." << std::endl;
}

int main() {
    thread t(threadFunction); // 创建线程
    t.join(); // 等待线程完成
    cout << "线程已结束." << endl;
    return 0;
}

3.detach()方法:

detach() 方法用于将线程与其调用的线程分离,使得分离后的线程与主线程独立运行

detach()方法细节:

  • 分离线程:调用 detach() 后,线程会独立于主线程执行。主线程和分离的线程之间不再有直接的关系。
  • 资源管理:分离的线程在完成后会自动释放其资源,主线程不需要显式地调用 join() 来等待它完成。
  • 非阻塞执行:主线程可以继续执行,不会因等待分离线程而被阻塞。

如何做到隔离线程?

  • 线程状态管理:当调用 detach() 方法时,线程的状态会被设置为 "可分离"。这意味着线程在后台运行,与创建它的线程(如主线程)没有绑定关系。
  • 不再访问:一旦线程被分离,主线程不能再调用 join()joinable() 来等待或检查该线程。分离线程的生命周期不再受到主线程的影响。
  • 独立运行:分离线程将继续执行直到其任务完成,即使主线程已经结束。完成后,线程的资源会自动被操作系统回收。
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;

void threadFunction() {
    cout << "分离的线程正在运行..." << endl;
    this_thread::sleep_for(chrono::seconds(2)); // 模拟工作
    cout << "分离的线程结束." << endl;
}

int main() {
    thread t(threadFunction);
    t.detach(); // 分离线程
    cout << "主线程继续运行..." << endl;
    this_thread::sleep_for(chrono::seconds(1)); // 等待主线程结束
    cout << "主线程结束." << endl;
    return 0;
}

4.joinable():

检查线程是否可以被 join()。如果线程处于可加入状态(即尚未调用 join()detach()),返回 true;否则返回 false

#include <iostream>
#include <thread>
using namespace std;

void threadFunction() {
    cout << "线程正在运行..." << endl;
}

int main() {
    thread t(threadFunction);
    
    if (t.joinable()) { // 检查线程是否可加入
        t.join(); // 等待线程完成
    }
    
    cout << "线程已结束." << endl;
    return 0;
}

5.native_handle():

native_handle() 方法用于获取与线程相关的原生句柄。这个句柄通常是底层操作系统为线程分配的一个标识符,允许与特定于平台的线程功能进行交互。

在计算机科学中,句柄(Handle)是一种用于标识系统资源的抽象引用。句柄通常是一个整数或指针,它提供了对底层资源的间接访问,而不需要用户直接操作该资源的内部表示。句柄是一个“指针”的替代,它使得程序能够通过该句柄访问某个资源而不直接引用资源的地址。

  • 在 POSIX 系统中native_handle() 返回一个 pthread_t 类型的句柄,代表一个 POSIX 线程。
  • 在 Windows 系统中native_handle() 返回一个 HANDLE 类型,代表 Windows 线程。
#include <iostream>
#include <thread>
#include <pthread.h> // POSIX 线程库

void threadFunction() {
    std::cout << "线程正在运行..." << std::endl;
}

int main() {
    std::thread t(threadFunction);
    
    // 获取原生线程句柄
    auto nativeHandle = t.native_handle(); // 在 POSIX 系统中为 pthread_t 类型
    std::cout << "原生线程句柄: " << nativeHandle << std::endl;

    t.join(); // 等待线程完成
    return 0;
}

6.hardware_concurrency():

返回系统可以支持的并发线程数量(通常是 CPU 核心的数量)。虽然这不是 std::thread 的方法,但它与线程相关,提供了可用的并行硬件线程数量(即 CPU 核心数量)。返回一个 unsigned 整数,表示可用的硬件线程数量。返回值可能是 0,表示无法确定。

#include <iostream>
#include <thread>
using namespace std;

int main() {
    unsigned int numThreads = thread::hardware_concurrency();
    cout << "系统支持的并发线程数量: " << numThreads << :endl;
    return 0;
}

7.线程的休眠:

std::this_thread::sleep_for()std::this_thread::sleep_until()用于让线程暂停执行指定时间。

(1)std::this_thread::sleep_for():

std::this_thread::sleep_for()用于让当前线程休眠一段时间。它接收一个表示时间长度的参数(std::chrono::duration),使线程暂停指定的时间。

std::this_thread::sleep_for(duration);

 duration:传入一个std::chrono::duration对象,表示线程需要休眠的时间长度。支持的时间单位包括std::chrono::secondsstd::chrono::millisecondsstd::chrono::microseconds等。

#include <iostream>
#include <thread>
#include <chrono>

int main() {
    std::cout << "Starting 3-second sleep..." << std::endl;
    // 线程会“睡眠”并在指定时间后恢复运行
    std::this_thread::sleep_for(std::chrono::seconds(3));
    std::cout << "Awake after 3 seconds!" << std::endl;
    return 0;
}

 (2)std::this_thread::sleep_until():

std::this_thread::sleep_until()用于让当前线程休眠至某个指定的时间点。它接收一个表示未来时间的参数(std::chrono::time_point),线程会暂停执行直到到达该时间点。

std::this_thread::sleep_until(time_point);

 time_point:传入一个std::chrono::time_point对象,表示线程需要休眠的时间点。time_point通常通过std::chrono::system_clock::now()获取当前时间,然后加上偏移时间来指定。

#include <iostream>
#include <thread>
#include <chrono>

int main() {
    auto start_time = std::chrono::system_clock::now();
    auto wake_time = start_time + std::chrono::seconds(3);

    std::cout << "Sleeping until specified time point..." << std::endl;
    // 线程会休眠至从start_time算起的3秒钟后,恢复时刻为wake_time
    // sleep_until 会直接等待到 wake_time,不论当前时间距离 wake_time 还有多长时间。
    std::this_thread::sleep_until(wake_time);// 表示当前线程会一直等待到 3 秒后才继续执行。
    std::cout << "Awake after reaching the time point!" << std::endl;

    return 0;
}

8.线程的局部存储(Thread Local Storage): 

在C++中,线程局部存储(Thread Local Storage,简称 TLS)允许每个线程有自己的独立数据副本。这对于需要在线程间共享的全局状态,但又希望每个线程有其独立的值的情况非常有用。

在多线程环境中需要避免数据竞争的情况下,使用线程局部存储是一种有效的方法。每个线程的任务需要保存一些状态信息,但这些信息不应该被其他线程共享或干扰。


使用thread_local关键字来定义一个线程局部变量。该变量的生命周期与线程的生命周期相同,线程结束时,该变量的内存将自动释放。

#include <iostream>
#include <thread>

thread_local int threadLocalVar = 0;  // 声明一个线程局部变量

void threadFunction(int id) {
    // 每个线程都会有自己的 threadLocalVar 副本
    threadLocalVar = id;  // 设置线程局部变量
    std::cout << "Thread " << id << ": threadLocalVar = " << threadLocalVar << std::endl;
}

int main() {
    std::thread t1(threadFunction, 1);
    std::thread t2(threadFunction, 2);
    
    t1.join();
    t2.join();
    
    return 0;
}
// Thread 1: threadLocalVar = 1
// Thread 2: threadLocalVar = 2

注意事项:

  • 性能考虑:虽然线程局部存储提供了方便,但过度使用可能导致内存使用增加,特别是在多线程程序中。
  • 静态存储:由于线程局部变量在程序的整个运行期间都是存在的,可能会导致更多的静态内存使用。
  • 跨线程访问:如果线程需要共享数据,仍然需要使用互斥锁等同步机制来管理对共享资源的访问。

我们还可以在结构体或类中使用thread_local,使整个类的成员或特定成员成为线程局部变量

#include <iostream>
#include <thread>

struct ThreadLocalData {
    thread_local static int value;  // 静态成员变量为线程局部变量
};

thread_local int ThreadLocalData::value = 0;

void threadFunction(int id) {
    ThreadLocalData::value = id;  // 修改线程局部变量
    std::cout << "Thread " << id << ": value = " << ThreadLocalData::value << std::endl;
}

int main() {
    std::thread t1(threadFunction, 1);
    std::thread t2(threadFunction, 2);
    
    t1.join();
    t2.join();
    
    return 0;
}

五.线程的同步与互斥:

多个线程同时访问共享数据时,可能导致数据竞争。C++提供了多种同步机制,如互斥锁(mutex)、条件变量(condition_variable)和原子操作(atomic)。 

1.互斥量(mutex):

线程同步是指在多线程环境中,控制线程的执行顺序,以确保多个线程在访问共享资源时不会出现冲突。常用的同步机制有条件变量和信号量。

请看下面的例子:

#include <iostream>
#include <thread>

void print_message(int& a) {
    for(int i = 0;i < 1000;i++){
        a++;
    }
}

int main() {
    int a = 0;
    std::thread thread1(print_message,std::ref(a));
    std::thread thread2(print_message,std::ref(a));

    thread1.join();
    thread2.join();

    std::cout << "a = " << a << std::endl;
    return 0;
}

当两个线程开启,这两个线程会同时对a进行+1操作,但是如果出现例如线程1与线程二同时拿到a并对a进行操作,那么同时返回就会造成a最终仅进行一次+1操作,这就意味着数据处理错误,也就是两个线程对数据的竞争造成的错误,那么如何解决这种问题呢?

我们不难想到,只需要在对a执行+1操作仅有一个线程在执行,另一个线程阻塞就可以了。这就需要提到mutex互斥量的概念了。

std::mutex 是一个简单的互斥量,提供了基本的锁定机制。它确保在同一时刻只有一个线程能够访问被保护的共享资源。

要使用 std::mutex,我们需要包含 <mutex> 头文件。

  • 创建一个 std::mutex 对象。
  • 在访问共享资源之前调用 lock() 方法加锁。
  • 访问共享资源。
  • 调用 unlock() 方法解锁。
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;// 创建互斥量

void print_message(int& a) {
    for(int i = 0;i < 1000;i++){
        mtx.lock();//加锁
        //访问共享资源
        a++;
        mtx.unlock();//解锁
    }
}

int main() {
    int a = 0;
    std::thread thread1(print_message,std::ref(a));
    std::thread thread2(print_message,std::ref(a));

    thread1.join();
    thread2.join();

    std::cout << "a = " << a << std::endl;
    return 0;
}

需要记住,我们在使用锁的时候必须要记得解锁,以免出现死锁现象(死锁产生的条件:不可剥夺/持有并等待/互斥条件不共享/循环等待)。而为了简化并且更安全的使用互斥量,C++给我们提供了std::lock_guard 来帮助我们。

lock_guard 的特点是锁在作用域结束时自动解锁,从而无需手动调用 unlock。使用 lock_guard 可以简化代码,并避免因异常或提前返回而导致的锁未释放的问题。


我们分析该锁的底层源码发现,当构造函数被调用时,该锁会自动加锁,当析构函数被调用时,该锁会自动解锁,所以分析后我们明白了lock_guard创建的对象不能被复制或者移动,只能在其局部作用域范围使用

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;// 创建互斥量

void print_message(int& a) {
    for(int i = 0;i < 1000;i++){
        // 使用 lock_guard 自动加锁
        std::lock_guard<std::mutex> lock(mtx);
        // 修改共享变量
        a++;
        // lock_guard 在作用域结束时自动解锁,无需手动调用 unlock
    }
}

int main() {
    int a = 0;
    std::thread thread1(print_message,std::ref(a));
    std::thread thread2(print_message,std::ref(a));

    thread1.join();
    thread2.join();

    std::cout << "a = " << a << std::endl;
    return 0;
}

如果我们想要解锁后手动加锁就需要使用 unique_lock 。

std::unique_lock 是一个更灵活的锁管理器,支持手动解锁、延迟加锁和条件变量的使用。适用于需要更复杂控制的场景。std::unique_lock 是一个更灵活的锁管理器,我们可以用于复杂的控制逻辑,例如在某个条件下释放锁以允许其他线程执行。[不支持拷贝因为底层代码明确写出 ...=delete 这段代码(=delete的作用是用于显式地禁止特定的函数或构造函数),但支持移动]

  • try_lock():try_lock() 方法尝试锁定互斥量,如果锁定成功则返回 true,否则返回 false,并不会阻塞当前线程。
  • try_lock_for(std::chrono::milliseconds(...)):try_lock_for() 方法尝试获取锁,并在指定的时间段内进行尝试。如果在指定的时间内未能获取锁,则返回 false
  • try_lock_until():try_lock_until() 方法尝试获取锁,直到指定的时间点。如果在指定的时间点之前未能获取锁,则返回 false
  • release():release() 方法将 unique_lock 对象的所有权转移给调用者,返回互斥量的引用,但不会解锁互斥量。
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

std::mutex mtx;

void tryLockForExample() {
    std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // 不立即锁定
    if (lock.try_lock_for(std::chrono::milliseconds(100))) { // 100ms
        std::cout << "Thread " << std::this_thread::get_id() << " acquired the lock.\n";
        // 执行临界区代码
        lock.unlock(); // 手动解锁
    } else {
        std::cout << "Thread " << std::this_thread::get_id() << " failed to acquire the lock.\n";
    }
}

int main() {
    std::thread t1(tryLockForExample);
    std::thread t2(tryLockForExample);

    t1.join();
    t2.join();
    return 0;
}

2.C++的其他锁的拓展介绍:

(1)std::recursive_mutex:

std::recursive_mutex 允许同一线程多次锁定同一互斥量而不会造成死锁。适用于需要递归调用的场景。

#include <iostream>
#include <thread>
#include <mutex>

std::recursive_mutex rmtx;
int sharedCounter = 0;

void recursiveIncrement(int count) {
    if (count <= 0) return;
    rmtx.lock();
    ++sharedCounter;
    recursiveIncrement(count - 1);// 递归操作
    rmtx.unlock();
}

int main() {
    std::thread t1(recursiveIncrement, 5);
    t1.join();

    std::cout << "Final Counter: " << sharedCounter << std::endl;
    return 0;
}

std::recursive_mutex 允许同一线程多次加锁,避免死锁。适合递归调用的场景,但性能相对 std::mutex 较低。

(2)std::timed_mutex:

std::timed_mutex 提供超时功能,可以在一定时间内尝试加锁,避免长时间等待。

#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

std::timed_mutex tmtx;
int sharedCounter = 0;

void tryIncrement() {
    //计时,超过100ms解锁
    if (tmtx.try_lock_for(std::chrono::milliseconds(100))) {
        ++sharedCounter;
        tmtx.unlock();
    } else {
        std::cout << "Failed to lock." << std::endl;
    }
}

int main() {
    std::thread t1(tryIncrement);
    std::thread t2(tryIncrement);

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

    std::cout << "Final Counter: " << sharedCounter << std::endl;
    return 0;
}

(3)std::shared_mutex:

std::shared_mutex 允许多个线程同时读取共享资源,但在写入时会独占访问。这种锁在读多写少(读取操作的次数远远超过写入操作的次数)的场景中特别有效。

特点:
  1. 读取频繁:大多数时间,系统会执行读取操作,例如从数据库查询数据或从缓存中获取数据。
  2. 写入不频繁:写入操作相对较少,通常是在数据更新或新增时进行。

在“读多写少”的场景下,使用 std::shared_mutex 或类似的锁机制,可以允许多个线程同时读取数据,从而提高并发性能,减少读取操作的延迟。通过降低写入操作的锁定时间,可以减轻对共享资源的争用,优化系统资源的使用。

#include <iostream>
#include <thread>
#include <shared_mutex>
#include <vector>

std::shared_mutex smtx;
std::vector<int> sharedData;

void readData(int id) {
    std::shared_lock<std::shared_mutex> lock(smtx);
    std::cout << "Reader " << id << " sees data size: " << sharedData.size() << std::endl;
}

void writeData(int value) {
    std::unique_lock<std::shared_mutex> lock(smtx);
    sharedData.push_back(value);
    std::cout << "Writer added: " << value << std::endl;
}

int main() {
    std::thread writers[3];
    std::thread readers[3];

    // 启动写线程
    for (int i = 0; i < 3; ++i) {
        writers[i] = std::thread(writeData, i);
    }

    // 启动读线程
    for (int i = 0; i < 3; ++i) {
        readers[i] = std::thread(readData, i);
    }

    // 等待所有线程完成
    for (int i = 0; i < 3; ++i) {
        writers[i].join();
        readers[i].join();
    }

    return 0;
}

3.死锁:

死锁是指两个或多个线程在执行过程中,因为争夺资源而造成的一种互相等待的状态。此时,所有线程都无法继续执行,导致程序停止运行。

死锁的产生通常需要满足以下四个条件:

  1. 互斥条件:至少有一个资源被一个线程持有,其他线程请求该资源时必须等待。
  2. 占有并等待条件:一个线程至少持有一个资源,并等待获取其他资源。
  3. 非抢占条件:已经分配给线程的资源不能被其他线程强行抢占。
  4. 循环等待条件:存在一种线程资源的循环等待关系。

如何避免死锁?

  • 总是以相同的顺序请求资源。
  • 使用超时来尝试获取资源。
  • 使用死锁检测算法。
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

std::mutex mtx1;
std::mutex mtx2;

void thread1(){
    for(int i = 0;i < 100;i++){
        mtx1.lock();
        mtx2.lock();
        mtx2.unlock();
        mtx1.unlock();
    }
}
void thread2(){
    for(int i = 0;i < 100;i++){
        mtx2.lock();
        mtx1.lock();
        mtx1.unlock();
        mtx2.unlock();
    }
}

int main() {
    std::thread t1(thread1);
    std::thread t2(thread2);

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

    std::cout << "Both threads finished executing." << std::endl;

    return 0;
}

有两个互斥量 mtx1mtx2 作为共享资源。thread1 首先锁定 mtx1,然后尝试锁定 mtx2,而 thread2 首先锁定 mtx2,然后尝试锁定 mtx1。当 thread1 锁定 mtx1 后,若 thread2 锁定了 mtx2thread1 将无法继续执行,等待 mtx2 的释放。同时,thread2 等待 mtx1 的释放,导致两个线程相互等待,形成死锁。

解决方法:

我们可以将两个方法都先对mtx1加锁,然后再mtx2加锁,随后先将mtx1解锁,在解锁mtx2,这样在获取mtx1互斥量如果mtx1没有解锁就不会进行另外一个方法,这样可以有效避免死锁。

  • 锁的获取顺序thread1thread2 都以相同的顺序首先获取 mtx1,然后获取 mtx2。这意味着,无论哪个线程先执行,获取锁的顺序始终是一致的。

  • 没有交叉等待:死锁通常发生在两个或多个线程相互等待对方持有的锁。在这个例子中,线程1和线程2都在同一时刻尝试以相同的顺序获取锁,所以它们不会互相阻塞。即使一个线程持有了一个锁,另一个线程也会以相同的顺序去请求锁,从而避免了交叉等待的情况。

  • 简化的示例:尽管这个示例不会死锁,但它仍然是一个不推荐的做法,因为在更复杂的情况下,可能会引入更多的互斥量,且不同线程获取锁的顺序可能不一致,这时就可能导致死锁。因此,在实际开发中,应该尽量避免嵌套锁定,或者使用其他策略(例如死锁检测、超时锁等)来处理潜在的死锁问题。

#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

std::mutex mtx1;
std::mutex mtx2;

void thread1(){
    for(int i = 0;i < 100;i++){
        mtx1.lock();
        mtx2.lock();
        mtx1.unlock();
        mtx2.unlock();
    }
}
void thread2(){
    for(int i = 0;i < 100;i++){
        mtx1.lock();
        mtx2.lock();
        mtx1.unlock();
        mtx2.unlock();
    }
}

int main() {
    std::thread t1(thread1);
    std::thread t2(thread2);

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

    std::cout << "Both threads finished executing." << std::endl;

    return 0;
}

这样太麻烦而且还需要思考,于是我们就会使用 lock() 同时锁定两个互斥量来避免死锁。 

#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

std::mutex mtx1;
std::mutex mtx2;

void thread1(){
    for(int i = 0; i < 100; i++){
        std::lock(mtx1, mtx2); // 同时锁定两个互斥量
        std::lock_guard<std::mutex> lg1(mtx1, std::adopt_lock);
        std::lock_guard<std::mutex> lg2(mtx2, std::adopt_lock);
        // 这里可以进行共享资源的操作
    }
}

void thread2(){
    for(int i = 0; i < 100; i++){
        std::lock(mtx1, mtx2); // 同时锁定两个互斥量
        std::lock_guard<std::mutex> lg1(mtx1, std::adopt_lock);
        std::lock_guard<std::mutex> lg2(mtx2, std::adopt_lock);
        // 这里可以进行共享资源的操作
    }
}

int main() {
    std::thread t1(thread1);
    std::thread t2(thread2);

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

    std::cout << "Both threads finished executing." << std::endl;

    return 0;
}

4.条件变量(Condition Variable):

条件变量(std::condition_variable)是C++11引入的一种用于线程同步的机制,主要用于解决多个线程之间的协调问题。条件变量配合互斥锁可以让线程在特定条件下等待或被唤醒。

工作机制:

  • 等待线程:调用 wait(lock,状态) 后,线程会进入阻塞状态,直到满足条件或被其他线程唤醒。wait 需要配合 std::unique_lock<std::mutex> 一起使用,以便解锁和重新锁定。
  • 唤醒线程notify_one() 会唤醒一个等待的线程,notify_all 则唤醒所有等待的线程。当条件满足时,调用 notify_onenotify_all 可以让等待的线程继续执行。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>

std::queue<int> q; // 消息队列(共享队列,表示生产的商品)
std::condition_variable cv; // 条件变量,用于线程间同步
std::mutex mtx; // 互斥锁,保护共享资源

// 生产者
void producer() {
	for (int i = 0; i < 10; i++) {
		{
			std::unique_lock<std::mutex> lock(mtx);
			// 共享变量
			q.push(i);
			// 通知消费者来获取
			cv.notify_one();
			std::cout << "Producer task: " << i << std::endl;
		}
		std::this_thread::sleep_for(std::chrono::microseconds(100));
	}
}

// 消费者
void consumer() {
	while (1) {
		std::unique_lock<std::mutex> lock(mtx);
		// 如果队列为空,需要等待
		//bool isempty = q.empty();
		//cv.wait(lock,!isempty);// 如果为true则不阻塞往下走,如果为false则阻塞等待
		cv.wait(lock, [](){ // lambda表达式
			return !q.empty();
		});
		int value = q.front();
		q.pop();
		std::cout << "Consumer value: " << value << std::endl;
	}
}

int main() {
	std::thread t1(producer);
	std::thread t2(consumer);

	t1.join();
	t2.join();
	return 0;
}
  • 条件变量的优点:条件变量能够有效解决线程之间的同步问题,减少不必要的轮询,提升多线程程序的效率。
  • 适用场景:条件变量适用于各种等待和通知的场景,尤其适合需要线程等待某个条件的情况,如生产者-消费者模式、延迟初始化等。
  • 注意事项:在使用 wait 时建议传入条件判断,防止虚假唤醒。

5.call_once的使用:

std::call_once 是 C++11 引入的一个用于线程安全的函数,主要用于确保某段代码在多线程环境中只执行一次。它通常用于初始化操作,特别适合只需要执行一次的操作,比如单例模式中的实例创建或资源初始化。

 首先我们创建一个Log日志类并且满足单例模式(全局只有一个实例对象,以至于初始化操作只能执行一次)(饿汉模式):

#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

class Log {
public:
	Log() {};
	// =delete代表禁用下面两个方法
	Log(const Log& log) = delete;
	Log& operator=(const Log& log) = delete;
	// 饿汉模式:声明后无需掉用构造方法,直接使用即可,但是切记只能有一个对象
	static Log& GetInstance() {
		static Log* log = nullptr;
		if (!log) {
			log = new Log;
		}
		return *log;
	}
	void printLog(std::string msg) {
		std::cout << __TIME__ << ' ' << msg << std::endl;
	}
};
void print_error() {
	Log::GetInstance().printLog("error");
}

int main() {
	std::thread th1(print_error);
	std::thread th2(print_error);
    
    th1.join();
    th2.join();
	return 0;
}

在这段代码中,如果两个线程同时进行,那么将会有种情况能够同时通过指针创建对象,这样就会在程序内创建两个Log对象,不满足我们的要求,所以我们需要使用 call_once() 函数确保函数仅能调用以此。

  • 它需要一个 std::once_flag 对象,用于标记某段代码是否已经执行过。
  • 使用 std::call_once 传入 std::once_flag 和需要执行的函数,确保函数只执行一次。
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

Log* log = nullptr;
static std::once_flag once;// once_flag对象,用于标记是否已经执行

class Log {
public:
	Log() {};
	// =delete代表禁用下面两个方法
	Log(const Log& log) = delete;
	Log& operator=(const Log& log) = delete;
	// 饿汉模式:声明后无需掉用构造方法,直接使用即可,但是切记只能有一个对象
	static Log& GetInstance() {
		// 确保 init() 只会被执行一次
		std::call_once(once,init);
		return *log;
	}
	static void init() {
		if (!log) {
			log = new Log;
		}
	}
	void printLog(std::string msg) {
		std::cout << __TIME__ << ' ' << msg << std::endl;
	}
};

void print_error() {
	Log::GetInstance().printLog("error");
}

int main() {
	std::thread th1(print_error);
	std::thread th2(print_error);
		
	th1.join();
	th2.join();
	return 0;
}

std::call_once 的工作机制:

  • std::call_once 使用 std::once_flag 标记调用状态,每个 std::once_flag 对象只能与 std::call_once 绑定一次。
  • 在多线程环境中,std::call_once 会确保只有一个线程执行给定的函数,其他线程会被阻塞,直到第一次调用完成。

6.atomic 原子操作:

在多线程编程中,当多个线程同时访问和修改共享变量时容易出现数据竞争问题。我们在学习互斥量就使用锁的知识来处理,但是这样无外乎是将一段代码锁起来而没做到变量的独立。

C++标准库提供了std::atomic类模板,用于确保对变量的访问是原子的,即不会在一个线程中修改变量的过程中被另一个线程中断。 std::atomic可以用于多种类型的数据,例如整数、布尔值、指针等。它提供了多种原子操作,避免了手动加锁的复杂性。

std::atomic操作的效率通常高于锁,但并不适合复杂的同步场景。

#include <iostream>
#include <atomic>
#include <thread>
#include <vector>

std::atomic<int> counter = 0;  // 使用std::atomic包装一个整型变量

void increment(int n) {
    for (int i = 0; i < n; ++i) {
        counter.fetch_add(1, std::memory_order_relaxed);  // 原子加1
    }
}

int main() {
    int num_threads = 10;
    int increments_per_thread = 1000;
    std::vector<std::thread> threads;

    // 创建多个线程来并发地执行increment函数
    for (int i = 0; i < num_threads; ++i) {
        threads.emplace_back(increment, increments_per_thread);
    }

    // 等待所有线程完成
    for (auto& th : threads) {
        th.join();
    }

    std::cout << "Final counter value: " << counter.load() << std::endl;
    return 0;
}

原子操作的常用方法: 

  • fetch_add(val): 原子加法,增加指定的值并返回旧值。
  • fetch_sub(val): 原子减法,减少指定的值并返回旧值。
  • store(val): 将一个值存储到 atomic对象中。
  • load(): 从atomic对象中读取值。
  • exchange(val): 原子地设置新值,并返回旧值。
  • compare_exchange_weak(expected,val)【适合循环使用】 / compare_exchange_strong(expected,val)【适合单词执行】: 比较并交换值。在修改atomic变量的值前,先检查变量的当前值是否符合预期值(expected),如果符合就执行交换操作,否则不做任何修改。如果变量的当前值等于expected,则将其修改为给定的新值 (desired),并返回true表示成功。如果变量的当前值不等于expected,则修改expected为当前的实际值,并返回false表示操作失败。
#include <iostream>
#include <atomic>
#include <thread>

std::atomic<int> value(100);

void try_change_value(int expected, int new_value) {
    int temp = expected;
    if (value.compare_exchange_weak(temp, new_value)) {
        std::cout << "Thread " << std::this_thread::get_id() << " successfully changed value to " << new_value << std::endl;
    } else {
        std::cout << "Thread " << std::this_thread::get_id() << " failed to change value. Current value is " << value.load() << std::endl;
    }
}

int main() {
    std::thread t1(try_change_value, 100, 200);
    std::thread t2(try_change_value, 100, 300);

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

    std::cout << "Final value: " << value.load() << std::endl;
    return 0;
}
// Thread 28892 successfully changed value to 200
// Thread 20096 failed to change value. Current value is 200
// Final value: 200

 六.线程池的构建与使用:

 1.首先创建一个线程池类:

(1)创建成员变量:

class ThreadPool {
private:
    std::vector<std::thread> threads; // 线程池
    std::queue<std::function<void()>> tasks; // 任务队列
    std::mutex mtx; // 保护任务队列的互斥锁
    std::condition_variable cv; // 条件变量,用于线程同步
    bool stop; // 标志位,指示线程池是否应该停止
}

(2) 构造函数:

ThreadPool(int numThreads) :stop(false) {
    // 根据参数创建 numThreads 个线程到线程池当中
    for (int i = 0; i < numThreads; i++) {
        // 创建线程至线程池内
        threads.emplace_back([this] {
            //使用线程在队列拿任务
            while (1) {
                std::unique_lock<std::mutex> lock(mtx);
                // 如果队列为空,需要等待(如果为true则不阻塞往下走,如果为false则阻塞等待)
                cv.wait(lock, [this] {
                    return !tasks.empty() || stop;
                });
                // 如果线程状态停止并且任务队列为空,直接返回
                if (stop && tasks.empty()) {
                    return;
                }
                //线程没有终止,需要取列表最左边的任务
                std::function<void()> task(std::move(tasks.front()));
                tasks.pop();
                lock.unlock();
                task();
            }
        }); // 直接构造新变量以此节省资源,而push_back是通过拷贝构造函数来实现的
    }
}

首先传递参数来创建 numThreads 个线程,将它们加入到 threads 向量中。每个线程执行一个匿名函数 [this] { ... }。 

在线程的循环中:

  • 使用 std::unique_lock<std::mutex> 加锁(保护 tasks 队列)。
  • 使用 cv.wait(lock, [this] { return !tasks.empty() || stop; }); 等待条件变量:
    • 条件:如果 tasks 不为空或 stoptrue,条件满足,继续执行。
    • 否则,阻塞线程直到条件满足。(底层代码是一个while循环的递归操作)
  • 如果 stoptruetasks 为空,则退出线程。
  • 取出队列中的任务并移除(tasks.pop())。
  • 解锁 mtx,防止任务执行期间持锁,影响其他线程。
  • 执行任务 task()
什么是 function 函数模板? 

std::function 是 C++11 标准库中引入的一个通用函数包装器,用于存储、传递和调用任意可调用的目标对象,包括普通函数、lambda 表达式、函数对象、以及成员函数指针等。std::function 可以让我们在编写通用代码时不必关心具体的函数类型,从而提高代码的灵活性和可扩展性。

#include <functional> // 引入头文件
std::function<返回类型(参数类型列表)> 函数变量名;

例:

#include <iostream>
#include <functional>
int add(int a, int b) {
    return a + b;
}
int main() {
    std::function<int(int, int)> func = add;
    std::cout << "Result of add: " << func(3, 5) << std::endl; // 输出 8
    return 0;
}

 包装 lambda 表达式:

#include <iostream>
#include <functional>
int main() {
    std::function<int(int, int)> func = [](int a, int b) {
        return a * b;
    };
    std::cout << "Result of lambda: " << func(3, 5) << std::endl; // 输出 15
    return 0;
}

使用 std::function 作为参数进行回调:

#include <iostream>
#include <functional>
void executeOperation(const std::function<int(int, int)>& operation, int x, int y) {
    std::cout << "Result: " << operation(x, y) << std::endl;
}
int main() {
    std::function<int(int, int)> add = [](int a, int b) { return a + b; };
    std::function<int(int, int)> multiply = [](int a, int b) { return a * b; };

    executeOperation(add, 3, 5);       // 输出 8
    executeOperation(multiply, 3, 5);  // 输出 15

    return 0;
}

(3)析构函数:

~ThreadPool() {
    {
        std::unique_lock<std::mutex> lock(mtx);
        stop = true;
    }
    //通知所有线程将任务队列内的所有任务取完
    cv.notify_all();
    for (auto& t : threads) {
        t.join();
    }
}
  • stop 设置为 true,通知线程池中的所有线程退出。
  • 调用 cv.notify_all() 唤醒所有等待线程。
  • 使用 join() 等待所有线程完成工作。

(4)添加任务 enqueue() 方法:

template<class T, class... Args>
void enqueue(T &&t, Args&&... args) {
    //将一个函数与特定参数绑定起来,以便在后续调用时可以通过调用返回的可调用对象来执行该函数
    std::function<void()> task = std::bind(std::forward<T>(t), std::forward<Args>(args)...);
    {
        std::unique_lock<std::mutex> lock(mtx);
        tasks.emplace(std::move(task));
    }
    cv.notify_one();
}
  • enqueue 是一个模板函数,用于将任务添加到线程池。
  • 使用 std::bindstd::function 将传入的任务 t 及其参数 args 绑定成一个可调用的 task 对象。
  • task 被放入任务队列 tasks 中,加入互斥锁 mtx 确保线程安全。
  • 调用 cv.notify_one() 唤醒一个等待中的线程。
什么是 std::bind ?

std::bind 是一个函数模板,它的作用是将一个函数及其参数进行绑定,生成一个新的函数对象,该函数对象可以被调用,执行时使用绑定的参数。

std::bind(可调用对象, 参数列表);
#include <iostream>
#include <functional>
void print_sum(int a, int b) {
    std::cout << "Sum: " << a + b << std::endl;
}
int main() {
    // 绑定 print_sum 函数和参数 2, 3,生成一个新函数对象 add_two_and_three
    auto add_two_and_three = std::bind(print_sum, 2, 3);
    add_two_and_three(); // 输出 "Sum: 5"
    return 0;
}

在这个例子中,std::bindprint_sum 函数与参数 23 绑定,生成了一个新的可调用对象 add_two_and_three,后续调用时会执行 print_sum(2, 3)。 

什么是 std::forward ?

std::forward 是 C++11 引入的一个工具,用于实现“完美转发”(perfect forwarding)。它的作用是将函数参数原封不动地传递给另一个函数,同时保持参数的“值类别”(左值或右值)。

在模板函数中,如果我们需要保持传递参数的左值或右值属性,就需要用到 std::forward。这样可以避免不必要的拷贝,提升效率。

#include <iostream>
#include <utility>

void print(int& n) { std::cout << "Left value: " << n << std::endl; }
void print(int&& n) { std::cout << "Right value: " << n << std::endl; }

template<typename T>
void wrapper(T&& t) {
    print(std::forward<T>(t));  // 保持 t 的值类别
}

int main() {
    int x = 5;
    wrapper(x);       // 输出 "Left value: 5"
    wrapper(10);      // 输出 "Right value: 10"
    return 0;
}
  • wrapper 接受左值 x 时,std::forward 将其原样传递为左值。
  • wrapper 接受右值 10 时,std::forward 将其原样传递为右值。
 什么是 std::move ?

std::move 是 C++11 引入的标准库函数,用于将对象转换为右值引用。它本身并不真正“移动”对象,而是强制将一个左值转换为右值引用,这样编译器就可以优先选择调用可“移动”的版本的构造函数或赋值运算符,而非复制版本。

std::move 用于将左值转换为右值引用,以便调用移动构造或移动赋值等高效的移动操作,避免拷贝。这样可以优化性能,尤其是在处理大型对象或容器时。

#include <iostream>
#include <vector>
#include <utility> // 包含std::move

int main() {
    std::vector<int> v1 = {1, 2, 3};
    std::vector<int> v2;

    // 使用 std::move 将 v1 的资源移动到 v2
    v2 = std::move(v1);

    std::cout << "v1 size: " << v1.size() << std::endl;  // 输出 v1 size: 0
    std::cout << "v2 size: " << v2.size() << std::endl;  // 输出 v2 size: 3

    return 0;
}
  • std::move(v1)v1 转换为右值引用,使得赋值操作 v2 = std::move(v1) 使用了 v1 的移动构造函数或移动赋值运算符,而不是拷贝构造函数或拷贝赋值运算符。
  • 这意味着 v1 的数据会被“移动”到 v2,之后 v1 的数据将变为空(但对象依然有效)。
  • 结果v1 不再包含数据,v2 获得了 v1 的资源。这种操作避免了内存分配和复制,提高了效率。
std::move 使用注意事项:
  • std::move 仅仅是一个类型转换工具,它不会真正移动对象。
  • 使用 std::move 后,原对象仍然存在,但处于未定义状态,只能做销毁或重新赋值,不能继续使用原数据。
  • 确保只在不再需要使用原对象时才使用 std::move,否则可能导致意外问题。

2.创建 main 内:

int main() {
    ThreadPool pool(4);
    for (int i = 0; i < 10; i++) {
        pool.enqueue([i] {
            {
                std::unique_lock<std::mutex> lock(mtxall);
                std::cout << "task: " << i << " is running" << std::endl;
            }
            std::this_thread::sleep_for(std::chrono::seconds(1));
            {
                std::unique_lock<std::mutex> lock(mtxall);
                std::cout << "task: " << i << " is done" << std::endl;
            }
        });
    }
    return 0;
}
  • 创建 ThreadPool pool(4);,初始化一个包含 4 个线程的线程池。
  • 向线程池中提交 10 个任务,每个任务输出当前任务编号 i 的状态。
    • 使用 std::unique_lock<std::mutex> lock(mtxall); 保护 std::cout,确保任务编号和状态按顺序输出,不发生竞态。
  • 线程池自动执行任务。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <vector>
#include <functional> // 函数模版

std::mutex mtxall; // 互斥锁,保护共享资源

class ThreadPool {
private:
	std::vector<std::thread> threads; // 线程池
	std::queue<std::function<void()>> tasks; // 消息队列(共享队列,表示生产的商品)

	std::mutex mtx; // 互斥锁,保护共享资源
	std::condition_variable cv; // 条件变量,用于线程间同步

	bool stop; // 线程池状态

public:
	ThreadPool(int numThreads) :stop(false) {
		for (int i = 0; i < numThreads; i++) {
			// 创建线程至线程池内
			threads.emplace_back([this] {
				//使用线程在队列拿任务
				while (1) {
					std::unique_lock<std::mutex> lock(mtx);
					// 如果队列为空,需要等待(如果为true则不阻塞往下走,如果为false则阻塞等待)
					cv.wait(lock, [this] {
						return !tasks.empty() || stop;
					});
					// 如果线程状态停止并且任务队列为空,直接返回
					if (stop && tasks.empty()) {
						return;
					}
					//线程没有终止,需要取列表最左边的任务
					std::function<void()> task(std::move(tasks.front()));
					tasks.pop();
					lock.unlock();// 解锁
					task();
				}
			}); // 直接构造新变量以此节省资源,而push_back是通过拷贝构造函数来实现的
		}
	}
	~ThreadPool() {
		{
			std::unique_lock<std::mutex> lock(mtx);
			stop = false;
		}
		//通知所有线程将任务队列内的所有任务取完
		cv.notify_all();
		for (auto& t : threads) {
			t.join();
		}
	}
	template<class T,class... Args>
	void enqueue(T &&t,Args&&... args) { // 在函数模版内,右值引用是万能引用
		// 获取任务
		std::function<void()>task = 
			std::bind(std::forward<T>(t), std::forward<T>(args)...);
		// 将任务放入任务列表内
		{
			std::unique_lock<std::mutex> lock(mtx);
			tasks.emplace(std::move(task));
		}
		cv.notify_one();
	}
};

int main() {
	ThreadPool pool(4);
	for (int i = 0; i < 10; i++) {
		pool.enqueue([i] {
			// 为了避免线程竞争而打印乱码,加入锁就可以解决或者使用printf()函数打印
			{
				std::unique_lock<std::mutex> lock(mtxall);
				std::cout << "task: " << i << " is runing" << std::endl;
			}
			std::this_thread::sleep_for(std::chrono::seconds(1));
			{
				std::unique_lock<std::mutex> lock(mtxall);
				std::cout << "task: " << i << " is done" << std::endl;
			}
		});
	}
	return 0;
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于Spring Boot的医疗陪护系统设计与实现(源码+定制+开发)病患陪护管理平台、医疗服务管理系统、医疗陪护信息平台

博主介绍&#xff1a; ✌我是阿龙&#xff0c;一名专注于Java技术领域的程序员&#xff0c;全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师&#xff0c;我在计算机毕业设计开发方面积累了丰富的经验。同时&#xff0c;我也是掘金、华为云、阿里云、InfoQ等平台…

ViT面试知识点

文章目录 VITCLIPSAMYOLO系列问题 VIT 介绍一下Visual Transformer&#xff1f; 介绍一下自注意力机制&#xff1f; 介绍一下VIT的输出方式 介绍一下VIT做分割任务 VIT是将NLP的transformer迁移到cv领域&#xff0c;他的整个流程大概如下&#xff1a;将一张图片切成很多个pat…

【Comsol教程】计算流道中的流量

在进行微流控方面的仿真的时候可能需要计算某一流道中流量的大小&#xff0c;下面展示如何计算。 流量分为质量流量和体积流量&#xff0c;我们常采用体积流量。在COMSOL中有两种方法计算&#xff0c; 1.使用Comsol内置的函数 这里我使用的是蠕动流模块【spf】,定义了3个开放边…

LeetCode 3226. 使两个整数相等的位更改次数

. - 力扣&#xff08;LeetCode&#xff09; 题目 给你两个正整数 n 和 k。你可以选择 n 的 二进制表示 中任意一个值为 1 的位&#xff0c;并将其改为 0。 返回使得 n 等于 k 所需要的更改次数。如果无法实现&#xff0c;返回 -1。 示例 1&#xff1a; 输入&#xff1a; n …

项目升级到.Net8.0 Autofac引发诡异的问题

前两天把项目升级到.Net8.0了&#xff0c;把.Net框架升级了&#xff0c;其他一些第三方库升级了一部分&#xff0c;升级完以后项目跑不起来了&#xff0c;报如下错误&#xff1a; An unhandled exception occurred while processing the request. DependencyResolutionExcepti…

RabbitMQ 七种工作模式介绍

目录 1.简单模式队列 2.WorkQueue(⼯作队列) 3 Publish/Subscribe(发布/订阅) 4 Routing(路由模式) 5.Topics(通配符模式) 6 RPC(RPC通信) 7 Publisher Confirms(发布确认) RabbitMQ 共提供了7种⼯作模式供我们进⾏消息传递,接下来一一介绍它的实现与目的 1.简单模式队列…

自动化测试类型与持续集成频率的关系

持续集成是敏捷开发的一个重要实践&#xff0c;可是究竟多频繁的集成才算“持续”集成&#xff1f; 一般来说&#xff0c;持续集成有3种常见的集成频率&#xff0c;分别是每分钟集成、每天集成和每迭代集成。项目组应当以怎样的频率进行集成&#xff0c;这取决于测试策略&…

操作系统期中复习2-4单元

Chapter-2 第一个图形界面——Xerox Alto 早期操作系统&#xff1a;规模小&#xff0c;简单&#xff0c;功能有限&#xff0c;无结构(简单结构)。&#xff08;MS-DOS,早期UNIX&#xff09; 层次结构&#xff1a;最底层为硬件&#xff0c;最高层为用户层&#xff0c;自下而上构…

2-141 怎么实现ROI-CS压缩感知核磁成像

怎么实现ROI-CS压缩感知核磁成像&#xff0c;这个案例告诉你。基于matlab的ROI-CS压缩感知核磁成像。ROI指在图像中预先定义的特定区域或区域集合&#xff0c;选择感兴趣的区域&#xff0c;通过减少信号重建所需的数据来缩短信号采样时间&#xff0c;减少计算量&#xff0c;并在…

Android中同步屏障(Sync Barrier)介绍

在 Android 中&#xff0c;“同步屏障”&#xff08;Sync Barrier&#xff09;是 MessageQueue 中的一种机制&#xff0c;允许系统临时忽略同步消息&#xff0c;以便优先处理异步消息。这在需要快速响应的任务&#xff08;如触摸事件和动画更新&#xff09;中尤为重要。 在 An…

【tomcat系列漏洞利用】

Tomcat 服务器是一个开源的轻量级Web应用服务器&#xff0c;在中小型系统和并发量小的场合下被普遍使用。主要组件&#xff1a;服务器Server&#xff0c;服务Service&#xff0c;连接器Connector、容器Container。连接器Connector和容器Container是Tomcat的核心。一个Container…

【压力测试】如何确定系统最大并发用户数?

一、明确测试目的与了解需求 明确测试目的&#xff1a;首先需要明确测试的目的&#xff0c;即为什么要确定系统的最大并发用户数。这通常与业务需求、系统预期的最大用户负载以及系统的稳定性要求相关。 了解业务需求&#xff1a;深入了解系统的业务特性&#xff0c;包括用户行…

深入理解Redis的四种模式

Redis是一个内存数据存储系统&#xff0c;支持多种不同的部署模式。以下是Redis的四种主要部署模式。 1、单机模式 单机模式是最简单的部署模式&#xff0c;Redis将数据存储在单个节点上。这个节点包括一个Redis进程和一个持久化存储。单机模式非常适合小型应用程序或者开发和…

【多态】析构函数的重写

析构函数的重写&#xff08;面试常见题&#xff09; 基类的析构函数为虚函数&#xff0c;此时派生类析构函数只要定义&#xff0c;⽆论是否加virtual关键字&#xff0c;都与基类的析构函数构成重写。 虽然基类与派⽣类析构函数名字不同看起来不符合重写的规则&#xff0c;实际…

合并区间 leetcode56

合并区间leetcode 目录一、题目二、踩坑过程三、上官方解答四、含泪体会彩蛋 目录 一、题目 二、踩坑过程 一开始想使用一个数组来标记区间&#xff0c;但是仔细想不好实现&#xff0c;单纯把区间里出现的设置为1&#xff0c;不好体现重叠的概念&#xff0c;如果使用三种状态…

机器人领域中的scaling law:通过复现斯坦福机器人UMI——探讨数据规模化定律(含UMI的复现关键)

前言 在24年10.26/10.27两天&#xff0c;我司七月在线举办的七月大模型机器人线下营时&#xff0c;我们带着大家一步步复现UMI「关于什么是UMI&#xff0c;详见此文&#xff1a;UMI——斯坦福刷盘机器人&#xff1a;从手持夹持器到动作预测Diffusion Policy(含代码解读)」&…

MybatisPlus入门(六)MybatisPlus-空值处理

一、MybatisPlus-空值处理 1.1&#xff09;问题引入&#xff1a; 在查询中遇到如下情况&#xff0c;有部分筛选条件没有值&#xff0c;如商品价格有最大值和最小值&#xff0c;商品价格部分时候没有值。 1.2&#xff09;解决办法&#xff1a; 步骤一&#xff1a;新建查询实体…

3.2链路聚合

1、链路聚合手动配置 将交换机S1、S2的GE0/0/1、GE0/0/2口来进行链路聚合。 交换机S1配置命令; [S1]interface eth-trunk 1 [S1-Eth-Trunk1]trunkport GigabitEthernet 0/0/1 to 0/0/2 [S1-Eth-Trunk1]port link-type trunk [S1-Eth-Trunk1]port trunk allow-pass vlan all …

Pinctrl子系统中Pincontroller构造过程驱动分析:imx_pinctrl_soc_info结构体

往期内容 本专栏往期内容&#xff1a; Pinctrl子系统和其主要结构体引入Pinctrl子系统pinctrl_desc结构体进一步介绍Pinctrl子系统中client端设备树相关数据结构介绍和解析 input子系统专栏&#xff1a; 专栏地址&#xff1a;input子系统input角度&#xff1a;I2C触摸屏驱动分析…

基于YOLO11/v10/v8/v5深度学习的维修工具检测识别系统设计与实现【python源码+Pyqt5界面+数据集+训练代码】

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推…