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.死锁:
死锁的产生通常需要满足以下四个条件:
如何避免死锁?
解决方法:
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)是进程中的一个执行单元,是操作系统能够进行运算调度的最小单位。线程是程序执行的基本单位,它包含了执行所需的所有信息,如程序计数器、栈和局部变量等。
优点:线程的创建和销毁开销小,线程之间可以方便地共享数据,减少了数据传输的开销,通过多线程可以充分利用多核处理器,提高程序的执行效率。
缺点:多个线程共享资源可能导致数据竞争和同步问题,需要使用锁等机制来保护共享数据。多线程程序的调试和错误排查相对复杂,可能会出现死锁、竞争条件等问题。
线程的特点:
- 轻量级:线程是比进程更小的执行单位,创建和销毁的开销较小。
- 共享资源:同一进程中的多个线程可以共享进程的资源,如内存空间、文件描述符等。
- 独立执行:每个线程拥有自己的执行栈和程序计数器,可以独立执行任务。
- 并发执行:多个线程可以并发执行,从而提高程序的执行效率。
线程的组成:
- 线程ID:每个线程都有一个唯一的标识符。
- 程序计数器:指向当前线程执行的指令。
- 栈:保存线程的局部变量和函数调用记录。
- 寄存器:存储线程的上下文信息。
二.什么是进程?
进程(Process)是一个正在执行的程序的实例,具有自己独立的内存空间和系统资源。进程是操作系统进行资源分配和调度的基本单位。
优点:进程之间互不干扰,安全性高。一个进程的崩溃不会影响其他进程。
缺点:进程的创建和销毁需要较大的开销。进程之间的通信相对复杂,通常需要使用进程间通信(IPC)机制。
进程的特点:
- 资源独立:每个进程都有自己的内存空间和资源,进程之间相互独立,互不干扰。
- 开销大:进程的创建、销毁和切换开销相对较大。
- 更高的隔离性:进程之间不直接共享内存,安全性更高。
进程的组成:
- 进程ID:每个进程都有一个唯一的标识符。
- 内存空间:包括代码段、数据段、堆和栈。
- 程序状态:记录进程的当前状态(就绪、运行、阻塞等)。
- 资源信息:记录进程所使用的系统资源(文件描述符、信号量等)。
三.线程与进程的关系:
进程是线程的容器:一个进程可以包含多个线程。所有线程共享该进程的资源(如内存),但每个线程有自己的栈和寄存器。
调度与切换:进程切换开销较大,因为需要保存和恢复整个进程的上下文。而线程切换开销较小,因为只需保存和恢复线程的上下文。
并发与并行:多个进程可以并行执行,多个线程在同一进程内可以并发执行。多线程程序比多进程程序更易于实现并发。
四.C++的Thread方法的使用:
通常建议在较大的项目中和公共代码库中使用
std::
前缀,以提高代码的可读性和可维护性。在小型项目或练习代码中,如果确实没有命名冲突,可以适当使用using namespace std;
,但最好在源文件的开头避免使用全局命名空间污染。一般来说,保持良好的命名空间管理是最佳实践。
1.创建线程:
线程一共有三种创建方法:
- 函数指针:thread thread_name(函数方法名,参数1,参数2,....);
- 函数对象:thread thread_name(函数方法名(),参数1,参数2,....);
- 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::seconds
、std::chrono::milliseconds
、std::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
允许多个线程同时读取共享资源,但在写入时会独占访问。这种锁在读多写少(读取操作的次数远远超过写入操作的次数)的场景中特别有效。
特点:
- 读取频繁:大多数时间,系统会执行读取操作,例如从数据库查询数据或从缓存中获取数据。
- 写入不频繁:写入操作相对较少,通常是在数据更新或新增时进行。
在“读多写少”的场景下,使用
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.死锁:
死锁是指两个或多个线程在执行过程中,因为争夺资源而造成的一种互相等待的状态。此时,所有线程都无法继续执行,导致程序停止运行。
死锁的产生通常需要满足以下四个条件:
- 互斥条件:至少有一个资源被一个线程持有,其他线程请求该资源时必须等待。
- 占有并等待条件:一个线程至少持有一个资源,并等待获取其他资源。
- 非抢占条件:已经分配给线程的资源不能被其他线程强行抢占。
- 循环等待条件:存在一种线程资源的循环等待关系。
如何避免死锁?
- 总是以相同的顺序请求资源。
- 使用超时来尝试获取资源。
- 使用死锁检测算法。
#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;
}
有两个互斥量
mtx1
和mtx2
作为共享资源。thread1
首先锁定mtx1
,然后尝试锁定mtx2,
而thread2
首先锁定mtx2
,然后尝试锁定mtx1
。当thread1
锁定mtx1
后,若thread2
锁定了mtx2
,thread1
将无法继续执行,等待mtx2
的释放。同时,thread2
等待mtx1
的释放,导致两个线程相互等待,形成死锁。
解决方法:
我们可以将两个方法都先对mtx1加锁,然后再mtx2加锁,随后先将mtx1解锁,在解锁mtx2,这样在获取mtx1互斥量如果mtx1没有解锁就不会进行另外一个方法,这样可以有效避免死锁。
锁的获取顺序:
thread1
和thread2
都以相同的顺序首先获取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_one
或notify_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
不为空或stop
为true
,条件满足,继续执行。- 否则,阻塞线程直到条件满足。(底层代码是一个while循环的递归操作)
- 如果
stop
为true
且tasks
为空,则退出线程。- 取出队列中的任务并移除(
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::bind
和std::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::bind
将print_sum
函数与参数2
和3
绑定,生成了一个新的可调用对象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;
}