C++ 并发编程:`std::thread`, `std::mutex`, `std::future` 的高级用法

C++ 并发编程:让你的程序“左右互搏”

想象一下,你是一位厨师,正在准备一桌丰盛的晚餐。如果只有一个你,就得先切菜,再炒菜,然后煮饭,最后摆盘,客人可能早就饿晕了。但如果你有几个帮手,就可以同时切菜、炒菜和煮饭,大大提高效率。

在计算机的世界里,这就是并发编程的魅力所在。它可以让你的程序同时执行多个任务,充分利用多核CPU的性能,就像拥有了多个“帮手”一样。

C++ 提供了丰富的并发编程工具,其中 std::threadstd::mutexstd::future 可以说是三大法宝,掌握它们,就能让你的程序像一位技艺精湛的“左右互搏”高手,效率倍增。

std::thread: 开启多线程的钥匙

std::thread 是 C++ 中创建线程的基石。它可以让你把一个函数或者一个可调用对象(比如 lambda 表达式)放到一个新的线程中执行。

简单来说,你可以把它想象成一个“分身术”,把程序的一部分代码“复制”到一个新的线程中,然后让这个新的线程和主线程并行执行。

基础用法:

#include <iostream>
#include <thread>

void task1() {
  std::cout << "任务1开始执行..." << std::endl;
  // 模拟耗时操作
  std::this_thread::sleep_for(std::chrono::seconds(2));
  std::cout << "任务1执行完毕!" << std::endl;
}

int main() {
  std::cout << "主线程开始..." << std::endl;

  // 创建一个新线程,执行 task1 函数
  std::thread t1(task1);

  // 主线程继续执行
  std::cout << "主线程继续执行..." << std::endl;

  // 等待 t1 线程执行完毕
  t1.join();

  std::cout << "主线程结束!" << std::endl;
  return 0;
}

在这个例子中,std::thread t1(task1); 创建了一个新的线程,并让它执行 task1 函数。t1.join(); 的作用是让主线程等待 t1 线程执行完毕,再继续执行。如果不调用 join(),主线程可能会在 t1 线程结束之前就结束,导致程序崩溃。

进阶用法:传递参数

std::thread 还可以传递参数给线程函数。这就像给你的“分身”分配任务的时候,顺便把所需的工具和材料也一并交给他。

#include <iostream>
#include <thread>

void task_with_args(int id, const std::string& message) {
  std::cout << "线程 " << id << ": " << message << std::endl;
}

int main() {
  std::thread t(task_with_args, 1, "你好,我是线程 1!");
  t.join();
  return 0;
}

Lambda 表达式:简洁的线程任务

使用 lambda 表达式可以更简洁地定义线程任务,尤其是当任务比较简单的时候。这就像直接告诉你的“分身”:“你去把那个东西拿过来!”

#include <iostream>
#include <thread>

int main() {
  std::thread t([]() {
    std::cout << "Lambda 线程开始执行..." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "Lambda 线程执行完毕!" << std::endl;
  });
  t.join();
  return 0;
}

std::mutex: 保护共享资源的卫士

在多线程编程中,多个线程可能会同时访问和修改同一个共享资源,比如一个全局变量,一个文件,或者一个数据库连接。如果没有适当的保护措施,就会导致数据竞争(Data Race),最终导致程序出现意想不到的错误,就像多个厨师同时抢夺同一把菜刀,结果可想而知。

std::mutex 就是用来保护共享资源的卫士,它可以确保在同一时刻,只有一个线程可以访问被保护的资源。

基本用法:加锁和解锁

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx; // 定义一个互斥锁
int counter = 0; // 共享资源

void increment_counter() {
  for (int i = 0; i < 100000; ++i) {
    mtx.lock(); // 加锁:请求访问共享资源
    counter++;   // 访问共享资源
    mtx.unlock(); // 解锁:释放共享资源
  }
}

int main() {
  std::thread t1(increment_counter);
  std::thread t2(increment_counter);

  t1.join();
  t2.join();

  std::cout << "最终计数器值: " << counter << std::endl;
  return 0;
}

在这个例子中,mtx.lock() 会尝试获取互斥锁,如果锁已经被其他线程持有,那么当前线程就会阻塞,直到锁被释放。mtx.unlock() 会释放互斥锁,允许其他线程获取。

std::lock_guard: RAII 风格的互斥锁

手动加锁和解锁容易忘记解锁,导致死锁。 std::lock_guard 可以利用 RAII (Resource Acquisition Is Initialization) 的特性,自动在离开作用域时解锁互斥锁,避免忘记解锁的风险。

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;
int counter = 0;

void increment_counter() {
  for (int i = 0; i < 100000; ++i) {
    std::lock_guard<std::mutex> lock(mtx); // 加锁,离开作用域自动解锁
    counter++;
  }
}

int main() {
  std::thread t1(increment_counter);
  std::thread t2(increment_counter);

  t1.join();
  t2.join();

  std::cout << "最终计数器值: " << counter << std::endl;
  return 0;
}

std::lock_guard 相当于给你的菜刀加上了一个安全锁,用完之后会自动归位,避免被其他厨师抢夺。

std::unique_lock: 更灵活的互斥锁

std::unique_lockstd::lock_guard 更加灵活,它允许延迟锁定(deferred locking)、尝试锁定(try locking)和所有权转移(ownership transfer)。

  • 延迟锁定: std::defer_lock 可以创建一个未锁定的 unique_lock 对象,可以在稍后手动调用 lock() 方法进行锁定。
  • 尝试锁定: try_lock() 尝试获取互斥锁,如果成功则返回 true,否则返回 false,不会阻塞。
  • 所有权转移: unique_lock 可以通过移动语义转移互斥锁的所有权。
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

std::mutex mtx;

void task_try_lock() {
  std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // 延迟锁定

  if (lock.try_lock()) { // 尝试锁定
    std::cout << "线程获得了锁!" << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟耗时操作
  } else {
    std::cout << "线程未能获得锁!" << std::endl;
  }
}

int main() {
  std::thread t1(task_try_lock);
  std::thread t2(task_try_lock);

  t1.join();
  t2.join();

  return 0;
}

std::future: 获取异步任务的结果

std::future 提供了一种机制来获取异步任务的结果。它可以让你在启动一个异步任务后,不必立即等待结果,而是可以继续执行其他任务,等到需要结果的时候再来获取。

可以把 std::future 想象成一张“提货单”,你把任务交给“快递公司”(异步任务),然后拿到一张提货单,等快递到了(任务完成)就可以凭提货单去取货(获取结果)。

基本用法:

#include <iostream>
#include <thread>
#include <future>

int calculate_sum(int a, int b) {
  std::cout << "开始计算..." << std::endl;
  std::this_thread::sleep_for(std::chrono::seconds(2));
  std::cout << "计算完成!" << std::endl;
  return a + b;
}

int main() {
  // 使用 std::async 启动一个异步任务
  std::future<int> result = std::async(std::launch::async, calculate_sum, 10, 20);

  // 主线程可以继续执行其他任务
  std::cout << "主线程正在执行其他任务..." << std::endl;

  // 等待异步任务完成并获取结果
  int sum = result.get();

  std::cout << "结果是: " << sum << std::endl;
  return 0;
}

在这个例子中,std::async 用于启动一个异步任务,它会返回一个 std::future 对象。result.get() 会等待异步任务完成,并返回其结果。如果异步任务还没有完成,get() 方法会阻塞,直到任务完成。

std::promise: 传递结果的桥梁

std::promise 可以让你在线程中设置结果,然后通过 std::future 对象获取这个结果。

可以把 std::promise 想象成一个“承诺”,你承诺在将来会设置一个值,然后把这个承诺(std::promise 对象)交给你的“分身”,你的“分身”在完成任务后,会兑现这个承诺,把结果设置到 std::promise 对象中,然后你就可以通过 std::future 对象获取这个结果。

#include <iostream>
#include <thread>
#include <future>

void task_with_promise(std::promise<int> p) {
  std::cout << "线程开始计算..." << std::endl;
  std::this_thread::sleep_for(std::chrono::seconds(2));
  int result = 42;
  std::cout << "线程计算完成,设置结果..." << std::endl;
  p.set_value(result); // 设置结果
}

int main() {
  std::promise<int> promise; // 创建一个 promise 对象
  std::future<int> future = promise.get_future(); // 获取 future 对象

  std::thread t(task_with_promise, std::move(promise)); // 启动线程,传递 promise 对象

  std::cout << "主线程等待结果..." << std::endl;
  int result = future.get(); // 获取结果
  std::cout << "结果是: " << result << std::endl;

  t.join();
  return 0;
}

异常处理:

如果异步任务抛出异常,future.get() 也会抛出相同的异常。因此,在使用 future.get() 获取结果时,需要进行异常处理。

#include <iostream>
#include <thread>
#include <future>

int task_that_throws() {
  throw std::runtime_error("任务执行失败!");
}

int main() {
  std::future<int> future = std::async(std::launch::async, task_that_throws);

  try {
    int result = future.get();
    std::cout << "结果是: " << result << std::endl;
  } catch (const std::exception& e) {
    std::cerr << "捕获到异常: " << e.what() << std::endl;
  }

  return 0;
}

总结:

std::threadstd::mutexstd::future 是 C++ 并发编程的三大利器。std::thread 让你能够创建和管理线程,std::mutex 保护共享资源,避免数据竞争,std::future 让你能够获取异步任务的结果,提高程序的效率。

掌握这三个工具,你就可以像一位技艺精湛的“左右互搏”高手,让你的程序同时执行多个任务,充分利用多核CPU的性能,最终编写出高效、可靠的并发程序。

当然,并发编程并非易事,需要深入理解线程同步、数据竞争、死锁等概念,并进行充分的测试和调试。 但只要你掌握了这些基础,就能在并发编程的世界里游刃有余,创造出更加强大的程序!

发表回复

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