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

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))) { // 100msstd::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; // 输出 8return 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; // 输出 15return 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);       // 输出 8executeOperation(multiply, 3, 5);  // 输出 15return 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_threeauto 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::moveint main() {std::vector<int> v1 = {1, 2, 3};std::vector<int> v2;// 使用 std::move 将 v1 的资源移动到 v2v2 = std::move(v1);std::cout << "v1 size: " << v1.size() << std::endl;  // 输出 v1 size: 0std::cout << "v2 size: " << v2.size() << std::endl;  // 输出 v2 size: 3return 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;
}


http://www.mrgr.cn/news/64343.html

相关文章:

  • 使用onnxruntime-web 运行yolov8-nano推理
  • yarn : 无法加载文件,未对文件 进行数字签名。无法在当前系统上运行该脚本。
  • 【含文档】基于ssm+jsp的智慧篮球馆预约(含源码+数据库+lw)
  • STM32CUBEMX安富莱STM32-V6开发板使用FMC驱动SDRAM芯片MT48LC4M32B2TG
  • 【机器学习】27. 马尔科夫链和隐马模型HMM
  • 打响反对人工智能的第一枪
  • 《程序内存需求估算:职场中的“暗礁”与“灯塔”》
  • 网络通信与并发编程(九)asyncio
  • 【ReactPress】一款基于React的开源博客CMS内容管理平台—ReactPress
  • Python Turtle模块详解与使用教程
  • ITK-膨胀
  • ‌频率和波长之间存在反比关系‌
  • 算法妙妙屋-------1.递归的深邃回响:C++ 算法世界的优雅之旅
  • (八)JavaWeb后端开发——Tomcat
  • 超好用的视频剪辑软件分享:10款剪辑软件推荐
  • UE5 猎户座漂浮小岛 06 角色
  • opengl学习-2vao和vbo(通义千问的例子)
  • 4.2.4 根据DTS完成timer初始化
  • 491.递增子序列
  • 爬虫学习2
  • LeetCode25:K个一组翻转链表
  • 【面渣逆袭】JavaSE笔记
  • Gin入门笔记
  • 深度学习基础—序列采样
  • 网络:ARP的具体过程和ARP欺骗
  • MATLAB中sort函数用法