C++并发编程入门:std::thread与std::mutex的基本用法

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!

注意事项:

  1. join() vs detach()

    • join():主线程会等待子线程完成后才继续。
    • detach():子线程独立运行,主线程不会等待它。
  2. 如果你不调用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在构造时自动加锁,在析构时自动解锁,非常方便。


并发编程中的常见问题

在并发编程中,有几个常见的坑需要注意:

  1. 死锁(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;
    }

    在这个例子中,t1t2可能会陷入死锁状态。解决方法是始终以相同的顺序获取锁。

  2. 竞态条件(Race Condition)
    我们已经通过std::mutex解决了这个问题。

  3. 性能问题
    锁的使用会带来一定的开销。如果锁的竞争过于激烈,可能会降低程序性能。因此,尽量减少锁的粒度。


总结

今天我们学习了C++并发编程的基础知识,包括:

  • 如何使用std::thread创建线程。
  • 如何用std::mutex保护共享资源。
  • 如何避免死锁和竞态条件。

为了方便记忆,这里总结成一张表格:

概念 描述 示例关键字
std::thread 创建和管理线程 join(), detach()
std::mutex 保护共享资源,防止竞态条件 lock(), unlock()
std::lock_guard 自动管理锁的生命周期 构造函数, 析构函数
死锁 线程互相等待资源,无法继续执行 锁顺序冲突

希望这篇文章能让你对C++并发编程有一个初步的认识。下一次,我们将深入探讨更多高级话题,比如条件变量和原子操作。期待下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注