目录
std::this_thread::sleep_until()
3. std::this_thread::sleep_for()
4. std::this_thread::sleep_until()
4.3、std::this_thread::sleep_for 与 std::this_thread::sleep_until 的区别
注:sleep_for 与 sleep_until 对比示例
4. 使用 std::scoped_lock 同时锁定多个互斥量
std::unique_lock 与 std::lock_guard 的区别
七、std::condition_variable::wait
std::condition_variable::wait 的原理
A、线程/多线程基础
一、C++11 创建线程的几种方式
1.1 使用函数指针
#include <iostream>
#include <thread>
void threadFunction(int x) {
std::cout << "Thread function called with value: " << x << std::endl;
}
int main() {
std::thread t(threadFunction, 10);
t.join(); // 等待线程执行完毕
return 0;
}
1.2 使用 lambda 表达式
#include <iostream>
#include <thread>
int main() {
std::thread t([](int x) {
std::cout << "Lambda thread called with value: " << x << std::endl;
}, 20);
t.join(); // 等待线程执行完毕
return 0;
}
1.3 使用成员函数
#include <iostream>
#include <thread>
class MyClass {
public:
void memberFunction(int x) {
std::cout << "Member function called with value: " << x << std::endl;
}
};
int main() {
MyClass obj;
std::thread t(&MyClass::memberFunction,
t.join(); // 等待线程执行完毕
return 0;
}
1.4 使用可调用对象 (Functor)
#include <iostream>
#include <thread>
class Functor {
public:
void operator()(int x) {
std::cout << "Functor called with value: " << x << std::endl;
}
};
int main() {
Functor functor;
std::thread t(functor, 40);
t.join(); // 等待线程执行完毕
return 0;
}
二、定义一个线程类
#include <iostream>
#include <thread>
class ThreadClass {
private:
std::thread t;
public:
// 构造函数,接受函数和参数来启动线程
template <typename Callable, typename... Args>
explicit ThreadClass(Callable&& func, Args&&... args) {
t = std::thread(std::forward<Callable>(func), std::forward<Args>(args)...);
}
// 加入线程(等待线程完成)
void join() {
if (t.joinable()) {
t.join();
}
}
// 分离线程(让线程独立运行)
void detach() {
if (t.joinable()) {
t.detach();
}
}
// 析构函数,确保线程对象在销毁前已被处理
~ThreadClass() {
if (t.joinable()) {
t.join(); // 通常在析构时,选择 join 或 detach 来处理未处理的线程
}
}
};
void threadFunction(int x) {
std::cout << "Thread function running with value: " << x << std::endl;
}
int main() {
// 使用类来管理线程
ThreadClass threadObj(threadFunction, 50);
threadObj.join(); // 等待线程完成
return 0;
}
三、join()
与 detach()
的详细用法及区别
3.1 join()
的用法
#include <iostream>
#include <thread>
void task() {
std::cout << "Task is running in thread." << std::endl;
}
int main() {
std::thread t(task);
// 主线程等待子线程完成
std::cout << "Waiting for thread to finish..." << std::endl;
t.join(); // 阻塞,直到子线程完成
std::cout << "Thread has finished." << std::endl;
return 0;
}
解释:
3.2 detach()
的用法
示例:
#include <iostream>
#include <thread>
#include <chrono>
void task() {
std::cout << "Detached thread is running." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时操作
std::cout << "Detached thread finished." << std::endl;
}
int main() {
std::thread t(task);
// 分离线程,允许其在后台运行
t.detach();
// 主线程不等待子线程完成,继续执行
std::cout << "Main thread is not waiting for detached thread." << std::endl;
// 模拟主线程的其他操作
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Main thread finished." << std::endl;
return 0;
}
解释:
3.3 join()
与 detach()
的区别总结
四、std::this_thread
4.1、主要功能
4.2、各方法的用法及适用场景
总结
std::this_thread
提供了一个简单而强大的接口,用于控制当前线程的行为,包括:
- 获取当前线程的 ID:有助于在调试和日志记录中识别不同线程。
- 让出线程的执行权:通过
yield()
提高 CPU 使用效率,允许系统调度其他线程。 - 让线程休眠:可以使用
sleep_for
或sleep_until
暂时暂停线程的执行,应用场景包括定时任务、延迟执行等。
这些工具使得 C++11 开始的多线程编程变得更加灵活和易于控制。
五、std::mutex
详解
std::mutex
是 C++11 引入的一种线程同步机制,用于在多线程环境下防止数据竞争(race condition)。在多线程程序中,如果多个线程同时访问并修改同一个共享数据,会导致数据竞争问题,造成不可预知的错误。std::mutex
(互斥量)提供了一种机制,确保每次只有一个线程能够访问共享资源,其他线程必须等待,直到前一个线程释放互斥量。
主要功能
lock()
:锁定互斥量。如果互斥量已经被其他线程锁定,则调用lock()
的线程将会阻塞,直到该互斥量被解锁。unlock()
:解锁互斥量,允许其他线程锁定它。try_lock()
:尝试锁定互斥量。如果互斥量已被其他线程锁定,则返回false
,否则锁定并返回true
。
使用场景
代码示例
1. 基本使用 std::mutex
这是一个简单的例子,展示了如何使用 std::mutex
来保护共享数据(计数器),防止多个线程同时修改它。
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx; // 全局互斥量,用于同步对共享数据的访问
int counter = 0; // 共享资源
void increaseCounter() {
for (int i = 0; i < 10000; ++i) {
mtx.lock(); // 锁定互斥量,进入临界区
++counter; // 对共享数据进行操作
mtx.unlock(); // 解锁互斥量,离开临界区
}
}
int main() {
// 创建两个线程,执行 increaseCounter 函数
std::thread t1(increaseCounter);
std::thread t2(increaseCounter);
// 等待两个线程完成
t1.join();
t2.join();
// 输出最终的计数器值
std::cout << "Final counter value: " << counter << std::endl;
return 0;
}
解释:
- 互斥量
mtx
:在每个线程访问共享资源之前使用lock()
来锁定互斥量,保证在任何时候只有一个线程可以修改counter
变量。 - 临界区:临界区是指
mtx.lock()
和mtx.unlock()
之间的代码区域,即对共享资源的访问部分。 - 线程安全:在互斥量的保护下,两个线程不会同时修改
counter
,因此保证了线程安全。
输出示例:
Final counter value: 20000
如果没有使用 std::mutex
,两个线程可能同时访问并修改 counter
,导致最后输出的值小于预期的 20000
,因为多个线程可能在同一时刻操作同一内存位置,发生了数据竞争。
2. 使用 try_lock()
try_lock()
是 std::mutex
提供的另一个方法,它不会阻塞线程。如果互斥量已经被锁定,try_lock()
会立即返回 false
,而不是阻塞等待。
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx; // 全局互斥量
void printMessage(const std::string& msg) {
if (mtx.try_lock()) { // 尝试获取锁,如果成功则执行
std::cout << msg << std::endl;
mtx.unlock(); // 解锁
} else {
std::cout << "Lock busy, couldn't print: " << msg << std::endl;
}
}
int main() {
std::thread t1(printMessage, "Hello from Thread 1");
std::thread t2(printMessage, "Hello from Thread 2");
t1.join();
t2.join();
return 0;
}
解释:
try_lock()
方法尝试锁定互斥量。如果成功获取锁,它就进入临界区执行输出操作;如果未能获取锁,说明其他线程已锁定了互斥量,则输出 "Lock busy"。
输出示例:
Hello from Thread 1
Lock busy, couldn't print: Hello from Thread 2
3. 使用 std::lock_guard
简化互斥锁管理
C++11 提供了 std::lock_guard
,它是一个 RAII 风格的锁管理器,在作用域结束时自动释放锁,避免了手动 lock()
和 unlock()
可能引发的错误。
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx; // 全局互斥量
int counter = 0; // 共享资源
void increaseCounter() {
for (int i = 0; i < 10000; ++i) {
std::lock_guard<std::mutex> lock(mtx); // 自动锁定互斥量,作用域结束时自动解锁
++counter;
}
}
int main() {
std::thread t1(increaseCounter);
std::thread t2(increaseCounter);
t1.join();
t2.join();
std::cout << "Final counter value: " << counter << std::endl;
return 0;
}
解释:
std::lock_guard
是一个模板类,管理互斥锁的生命周期。它在构造时自动锁定互斥量,在作用域结束时(例如函数返回或异常抛出时)自动解锁,避免了忘记解锁或出现死锁的情况。
输出示例:
Final counter value: 20000
4. 使用 std::scoped_lock
同时锁定多个互斥量
C++17 引入了 std::scoped_lock
,它不仅可以同时锁定多个互斥量,还能防止死锁。std::scoped_lock
是 std::lock_guard
的改进版,特别适用于需要同时锁定多个资源的场景。
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx1, mtx2; // 两个互斥量
void task1() {
std::scoped_lock lock(mtx1, mtx2); // 同时锁定 mtx1 和 mtx2
std::cout << "Task 1 has locked both mutexes." << std::endl;
}
void task2() {
std::scoped_lock lock(mtx1, mtx2); // 同时锁定 mtx1 和 mtx2
std::cout << "Task 2 has locked both mutexes." << std::endl;
}
int main() {
std::thread t1(task1);
std::thread t2(task2);
t1.join();
t2.join();
return 0;
}
解释:
std::scoped_lock
同时锁定多个互斥量,并且能避免死锁。它会确保在解锁时,以相反顺序解锁,从而避免了交叉锁定不同互斥量时可能发生的死锁问题。
输出示例:
Task 1 has locked both mutexes.
Task 2 has locked both mutexes.
重要注意事项
- 锁的生命周期:互斥量应该在所有访问共享资源的线程中共享。一个线程加锁后,其他线程必须等待该锁被解锁才能继续执行。
- 防止死锁:在多个互斥量之间锁定时(如同时访问多个资源),应小心防止死锁。死锁发生在多个线程相互等待其他线程释放锁的情况下。使用
std::scoped_lock
可以简化锁定多个资源时的死锁管理。 lock_guard
和scoped_lock
的推荐使用:lock_guard
和scoped_lock
是 RAII 风格的锁管理工具,推荐使用这些工具来管理锁的生命周期,避免手动lock()
和unlock()
可能引发的错误。
总结
std::mutex
是 C++11 提供的基本线程同步工具,用于解决多线程编程中的数据竞争问题。通过互斥量可以确保同一时刻只有一个线程能够访问共享资源,防止多个线程同时修改数据而导致不可预测的行为。结合 std::lock_guard
和 std::scoped_lock
,可以更简便地管理锁的生命周期,确保代码更加安全和高效。
六、std::unique_lock
- 可以延迟锁定互斥量(懒惰锁定)。
- 可以在需要时手动解锁和重新锁定。
- 可以使用
std::try_lock
、std::defer_lock
和std::adopt_lock
等高级锁定策略。
主要功能:
使用示例
1. 基本使用:自动锁定与解锁
std::unique_lock
可以像 std::lock_guard
一样,在构造时自动锁定互斥量,并在析构时自动解锁。
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx; // 全局互斥量
int counter = 0; // 共享资源
void increaseCounter() {
for (int i = 0; i < 10000; ++i) {
std::unique_lock<std::mutex> lock(mtx); // 自动锁定互斥量
++counter; // 临界区
// lock 会在作用域结束时自动解锁
}
}
int main() {
std::thread t1(increaseCounter);
std::thread t2(increaseCounter);
t1.join();
t2.join();
std::cout << "Final counter value: " << counter << std::endl;
return 0;
}
解释:
std::unique_lock
的构造函数会立即锁定互斥量mtx
,并且当lock
作用域结束时,它会自动解锁。这种机制避免了手动调用unlock()
的必要。
2. 手动锁定与解锁
你可以通过 std::unique_lock
对互斥量进行手动锁定和解锁操作,控制更为灵活。
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
void task() {
std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // 延迟锁定互斥量
std::cout << "Before locking" << std::endl;
lock.lock(); // 手动锁定互斥量
std::cout << "Locked and working..." << std::endl;
lock.unlock(); // 手动解锁
std::cout << "Unlocked" << std::endl;
}
int main() {
std::thread t(task);
t.join();
return 0;
}
解释:
- 这里使用了
std::defer_lock
,std::unique_lock
在构造时不会自动锁定互斥量。随后,我们手动调用了lock()
和unlock()
来控制互斥量的锁定和解锁过程。
3. 使用 try_lock
尝试锁定
std::unique_lock
还可以用于尝试锁定互斥量,而不阻塞线程。如果锁定失败,线程可以继续执行其他任务。
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
void task() {
std::unique_lock<std::mutex> lock(mtx, std::try_to_lock); // 尝试锁定互斥量
if (lock.owns_lock()) {
std::cout << "Lock acquired, working..." << std::endl;
} else {
std::cout << "Couldn't acquire lock, doing other work..." << std::endl;
}
}
int main() {
std::thread t1(task);
std::thread t2(task);
t1.join();
t2.join();
return 0;
}
解释:
- 通过
std::try_to_lock
,线程会尝试锁定互斥量。如果无法锁定,程序可以继续执行其他任务,而不是阻塞等待锁的释放。 owns_lock()
可以检查std::unique_lock
是否成功锁定互斥量。
4. 使用 adopt_lock
接管已经锁定的互斥量
如果你手动锁定了互斥量,但希望将它交给 std::unique_lock
来管理,可以使用 std::adopt_lock
。这会告诉 std::unique_lock
,互斥量已经被锁定,不再需要再次锁定。
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
void task() {
mtx.lock(); // 手动锁定互斥量
std::unique_lock<std::mutex> lock(mtx, std::adopt_lock); // 接管锁
std::cout << "Lock adopted, working..." << std::endl;
// lock 会在作用域结束时自动解锁
}
int main() {
std::thread t(task);
t.join();
return 0;
}
std::unique_lock
与 std::lock_guard
的区别
-
灵活性:
std::lock_guard
在构造时立即锁定互斥量,并在销毁时自动解锁。它适合用于简单的锁管理。std::unique_lock
提供了更多灵活的功能,如延迟锁定、尝试锁定、手动解锁等。它适用于需要复杂锁定逻辑的场景。
-
锁的控制:
std::lock_guard
不允许手动解锁,只能在作用域结束时解锁。std::unique_lock
允许在代码中手动调用unlock()
,并且可以手动重新锁定。
-
性能:
- 由于
std::lock_guard
更简单,通常它的性能要比std::unique_lock
稍高一些。但如果你的程序不需要std::unique_lock
的灵活性,优先使用std::lock_guard
会更高效。
- 由于
总结
std::unique_lock
是一个灵活的锁管理工具,适合处理复杂的线程同步场景。它与 std::lock_guard
的区别在于其灵活的控制能力:支持延迟锁定、手动解锁、尝试锁定等操作。此外,std::unique_lock
可以使用 std::defer_lock
、std::try_to_lock
和 std::adopt_lock
等锁定策略,适用于需要更精细控制的场景。在简单的锁定场景中,std::lock_guard
更适合,而 std::unique_lock
则适用于需要更多灵活性的场景。
七、std::condition_variable::wait
是 C++11 中引入的一个机制,用于在线程同步时协调多个线程之间的执行。它与 std::mutex
和 std::unique_lock
配合使用,用于阻塞线程直到特定条件满足。
std::condition_variable::wait
的原理
- 互斥锁:
wait()
必须与一个互斥锁(std::unique_lock
)一起使用。它在调用wait()
时会自动释放锁,进入等待状态,并在被通知时重新获取锁。 - 条件变量:
wait()
用于阻塞线程,直到另一个线程通过条件变量调用notify_one()
或notify_all()
来通知等待线程可以继续执行。 - 通知机制:
notify_one()
:唤醒一个等待的线程。notify_all()
:唤醒所有等待的线程。
wait()
的使用场景
典型的 wait()
使用场景是生产者-消费者问题。在这种情况下,消费者等待生产者生成数据,而生产者在生成数据后通知消费者继续处理。
代码示例
1. 基本使用 wait()
和 notify_one()
这是一个简单的示例,其中一个线程等待某个条件满足,而另一个线程在一定时间后设置条件并通知等待的线程继续执行。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx; // 互斥锁
std::condition_variable cv; // 条件变量
bool ready = false; // 条件标志
void waitForWork() {
std::unique_lock<std::mutex> lock(mtx); // 获取锁
std::cout << "Waiting for work to be ready...\n";
// 当 ready 为 false 时,线程进入等待状态,锁被自动释放
cv.wait(lock, [] { return ready; });
// 当 ready 为 true 时,wait 结束,线程重新获取锁
std::cout << "Work is ready, proceeding...\n";
}
void prepareWork() {
std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟工作准备
{
std::lock_guard<std::mutex> lock(mtx); // 获取锁并设置条件
ready = true;
std::cout << "Work is prepared, notifying worker...\n";
}
cv.notify_one(); // 通知等待线程可以继续执行
}
int main() {
std::thread worker(waitForWork); // 创建等待线程
std::thread preparer(prepareWork); // 创建准备线程
worker.join();
preparer.join();
return 0;
}
解释:
wait()
:在waitForWork()
函数中,调用cv.wait(lock, [] { return ready; })
,它将当前线程阻塞,直到ready
变为true
。wait()
在等待期间自动释放锁,避免其他线程无法获取锁。notify_one()
:在prepareWork()
函数中,调用cv.notify_one()
通知等待的线程(waitForWork
)可以继续执行。- 锁的管理:
wait()
和notify_one()
都需要与互斥锁配合使用,以保证线程之间的同步。
输出示例:
Waiting for work to be ready...
Work is prepared, notifying worker...
Work is ready, proceeding...
2. 使用 notify_all()
唤醒所有等待线程
notify_all()
可以唤醒所有因条件不满足而阻塞的线程。这对于多个线程等待同一个条件的场景非常有用。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <vector>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void worker(int id) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return ready; }); // 所有线程都等待 ready 为 true
std::cout << "Worker " << id << " is proceeding.\n";
}
void prepareWork() {
std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟工作准备
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
std::cout << "Work is ready, notifying all workers...\n";
}
cv.notify_all(); // 通知所有等待的线程
}
int main() {
std::vector<std::thread> workers;
// 创建多个线程
for (int i = 1; i <= 5; ++i) {
workers.emplace_back(worker, i);
}
std::thread preparer(prepareWork); // 创建准备线程
// 等待所有线程完成
for (auto& t : workers) {
t.join();
}
preparer.join();
return 0;
}
解释:
notify_all()
:notify_all()
会唤醒所有等待的线程,这里唤醒了 5 个worker
线程。- 多线程等待:所有
worker
线程都会在cv.wait()
中等待,直到ready
变为true
并且收到通知。
输出示例:
Work is ready, notifying all workers...
Worker 1 is proceeding.
Worker 2 is proceeding.
Worker 3 is proceeding.
Worker 4 is proceeding.
Worker 5 is proceeding.
3. 防止虚假唤醒
虚假唤醒(spurious wakeups)是指线程在没有被条件变量显式唤醒的情况下,也可能会被唤醒。这是操作系统的特性,因此我们在使用 wait()
时应该总是配合条件判断来避免这种情况。
为了避免虚假唤醒,C++ 中的 wait()
提供了一种安全的模式,即使用条件判断的 lambda 表达式来确保条件满足时才继续执行。
cv.wait(lock, [] { return ready; });
这个版本的 wait()
会在 ready
为 false
时继续阻塞线程,直到条件真正满足。
4. wait_for()
和 wait_until()
除了 wait()
之外,C++11 还提供了 wait_for()
和 wait_until()
方法,允许我们指定等待的时间。如果在指定的时间内条件未满足,线程会超时并继续执行。
wait_for()
:阻塞线程一段时间,超时后线程自动返回。wait_until()
:阻塞线程直到某个时间点,超时后线程自动返回。
4.1 使用 wait_for()
超时等待
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <chrono>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void worker() {
std::unique_lock<std::mutex> lock(mtx);
if (cv.wait_for(lock, std::chrono::seconds(3), [] { return ready; })) {
std::cout << "Work is ready, proceeding...\n";
} else {
std::cout << "Timeout, work is not ready.\n";
}
}
void prepareWork() {
std::this_thread::sleep_for(std::chrono::seconds(5)); // 模拟准备时间
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
std::cout << "Work is prepared, notifying worker...\n";
}
cv.notify_one();
}
int main() {
std::thread t1(worker);
std::thread t2(prepareWork);
t1.join();
t2.join();
return 0;
}
解释:
wait_for()
:等待 3 秒,如果ready
在 3 秒内没有变为true
,线程会超时并继续执行。- 超时处理:如果超过等待时间,线程会输出 "Timeout, work is not ready."。
输出示例:
Timeout, work is not ready.
Work is prepared, notifying worker...
4.2 使用 wait_until()
等待到指定时间点
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <chrono>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void worker() {
auto timeout = std::chrono::system_clock::now() + std::chrono::seconds(3);
std::unique_lock<std::mutex> lock(mtx);
if (cv.wait_until(lock, timeout, [] { return ready; })) {
std::cout << "Work is ready, proceeding...\n";
} else {
std::cout << "Timeout, work is not ready.\n";
}
}
void prepareWork() {
std::this_thread::sleep_for(std::chrono::seconds(5)); // 模拟准备时间
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
std::cout << "Work is prepared, notifying worker...\n";
}
cv.notify_one();
}
int main() {
std::thread t1(worker);
std::thread t2(prepareWork);
t1.join();
t2.join();
return 0;
}
解释:
wait_until()
:等待到指定时间点,如果在这个时间点前ready
变为true
,线程会继续执行;否则超时。
输出示例:
Timeout, work is not ready.
Work is prepared, notifying worker...
总结
std::condition_variable::wait
用于协调线程之间的同步,它可以让一个线程在等待某个条件满足时进入等待状态,并通过条件变量的notify_one()
或notify_all()
来唤醒线程。wait()
需要与std::mutex
和std::unique_lock
配合使用,以保证线程在等待期间不会造成数据竞争。wait_for()
和wait_until()
提供了超时等待机制,允许线程在指定的时间内等待条件满足,否则超时返回。