C++并发编程入门:std::thread与std::mutex的基本用法
大家好!欢迎来到今天的C++并发编程讲座。如果你对多线程编程还一头雾水,或者觉得它像一团乱麻,那么恭喜你,今天我们将一起揭开它的神秘面纱。别担心,我们会用轻松幽默的方式讲解,并通过代码示例和表格来帮助理解。准备好了吗?让我们开始吧!
什么是并发编程?
在正式进入主题之前,我们先简单聊聊“并发编程”是什么。简单来说,它是让程序中的多个任务同时运行的技术。想象一下,你在厨房里做饭,一边煮饭,一边炒菜,甚至还能顺便洗碗。这就是并发的魅力——提高效率,节省时间。
但在现实世界中,并发并不总是那么简单。比如,两个厨师同时去拿同一个锅,可能会发生冲突。在编程中,这种冲突被称为竞态条件(Race Condition),而解决这些问题的工具之一就是我们今天要讲的std::mutex
。
std::thread:创建线程的基本方法
C++11引入了std::thread
类,用于简化多线程编程。我们可以用它轻松地创建线程并执行任务。下面是一个简单的例子:
#include <iostream>
#include <thread>
void sayHello() {
std::cout << "Hello from thread!" << std::endl;
}
int main() {
// 创建一个线程,执行sayHello函数
std::thread t(sayHello);
// 主线程继续执行
std::cout << "Main thread is running..." << std::endl;
// 等待子线程完成
t.join();
return 0;
}
运行结果可能如下:
Main thread is running...
Hello from thread!
注意事项:
-
join() vs detach()
join()
:主线程会等待子线程完成后才继续。detach()
:子线程独立运行,主线程不会等待它。
-
如果你不调用
join()
或detach()
,程序会在主线程结束时抛出异常。
std::mutex:保护共享资源
当我们有多个线程访问同一个资源时,问题就来了。例如,两个线程同时修改一个全局变量,可能会导致数据不一致。为了解决这个问题,我们需要一把“锁”,也就是std::mutex
。
示例:没有互斥锁的情况
假设我们有两个线程同时对一个计数器进行加1操作:
#include <iostream>
#include <thread>
int counter = 0;
void incrementCounter() {
for (int i = 0; i < 100000; ++i) {
counter++;
}
}
int main() {
std::thread t1(incrementCounter);
std::thread t2(incrementCounter);
t1.join();
t2.join();
std::cout << "Final counter value: " << counter << std::endl;
return 0;
}
运行结果可能如下:
Final counter value: 184735
显然,结果不对劲!这是因为两个线程在同时修改counter
,导致丢失了一些更新。
使用std::mutex保护共享资源
为了解决上述问题,我们可以使用std::mutex
来确保同一时间只有一个线程可以修改counter
:
#include <iostream>
#include <thread>
#include <mutex>
int counter = 0;
std::mutex mtx; // 定义一个互斥锁
void incrementCounter() {
for (int i = 0; i < 100000; ++i) {
mtx.lock(); // 加锁
counter++;
mtx.unlock(); // 解锁
}
}
int main() {
std::thread t1(incrementCounter);
std::thread t2(incrementCounter);
t1.join();
t2.join();
std::cout << "Final counter value: " << counter << std::endl;
return 0;
}
运行结果:
Final counter value: 200000
这次结果正确了!std::mutex
确保了每次只有一个线程能够修改counter
。
更优雅的方式:std::lock_guard
手动调用lock()
和unlock()
容易出错,比如忘记解锁或在异常情况下未解锁。C++提供了一个更安全的工具——std::lock_guard
,它可以自动管理锁的生命周期。
#include <iostream>
#include <thread>
#include <mutex>
int counter = 0;
std::mutex mtx;
void incrementCounter() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex> lock(mtx); // 自动加锁和解锁
counter++;
}
}
int main() {
std::thread t1(incrementCounter);
std::thread t2(incrementCounter);
t1.join();
t2.join();
std::cout << "Final counter value: " << counter << std::endl;
return 0;
}
运行结果:
Final counter value: 200000
std::lock_guard
在构造时自动加锁,在析构时自动解锁,非常方便。
并发编程中的常见问题
在并发编程中,有几个常见的坑需要注意:
-
死锁(Deadlock)
当两个线程互相等待对方释放资源时,就会发生死锁。例如:std::mutex mtx1, mtx2; void thread1() { std::lock_guard<std::mutex> lock1(mtx1); std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟延迟 std::lock_guard<std::mutex> lock2(mtx2); // 尝试获取第二个锁 } void thread2() { std::lock_guard<std::mutex> lock2(mtx2); std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::lock_guard<std::mutex> lock1(mtx1); // 尝试获取第一个锁 } int main() { std::thread t1(thread1); std::thread t2(thread2); t1.join(); t2.join(); return 0; }
在这个例子中,
t1
和t2
可能会陷入死锁状态。解决方法是始终以相同的顺序获取锁。 -
竞态条件(Race Condition)
我们已经通过std::mutex
解决了这个问题。 -
性能问题
锁的使用会带来一定的开销。如果锁的竞争过于激烈,可能会降低程序性能。因此,尽量减少锁的粒度。
总结
今天我们学习了C++并发编程的基础知识,包括:
- 如何使用
std::thread
创建线程。 - 如何用
std::mutex
保护共享资源。 - 如何避免死锁和竞态条件。
为了方便记忆,这里总结成一张表格:
概念 | 描述 | 示例关键字 |
---|---|---|
std::thread | 创建和管理线程 | join(), detach() |
std::mutex | 保护共享资源,防止竞态条件 | lock(), unlock() |
std::lock_guard | 自动管理锁的生命周期 | 构造函数, 析构函数 |
死锁 | 线程互相等待资源,无法继续执行 | 锁顺序冲突 |
希望这篇文章能让你对C++并发编程有一个初步的认识。下一次,我们将深入探讨更多高级话题,比如条件变量和原子操作。期待下次再见!