C++ `std::shared_future`:多个 Future 共享一个异步结果

好的,没问题!让我们开始这场关于C++ std::shared_future 的技术讲座吧!

讲座主题:C++ std::shared_future:多个 Future 共享一个异步结果

大家好!欢迎来到今天的C++技术讲座。今天我们要聊的是一个非常有趣且实用的工具:std::shared_future

想象一下,你是一家快餐店的老板,顾客络绎不绝。每个顾客都想点一份美味的汉堡,而制作汉堡需要一段时间。如果你只有一个厨师(线程),那么每个顾客都必须排队等待,效率非常低。

为了提高效率,你决定雇佣多个服务员(线程),让他们同时为不同的顾客服务。但是,只有一份汉堡制作的配方(异步任务的结果),所有服务员都需要使用这份配方才能制作出正确的汉堡。

std::shared_future 就好比这份汉堡配方,它可以被多个服务员(线程)共享,让他们都能获取到汉堡的制作方法(异步任务的结果)。

什么是 std::future

在深入了解 std::shared_future 之前,我们先简单回顾一下 std::futurestd::future 是C++中用于获取异步操作结果的机制。它代表着一个异步任务的返回值,但这个返回值可能在将来某个时刻才能得到。

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

int calculate_sum(int a, int b) {
  std::cout << "Calculating sum in thread: " << std::this_thread::get_id() << std::endl;
  std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时操作
  return a + b;
}

int main() {
  std::future<int> result = std::async(std::launch::async, calculate_sum, 10, 20);

  std::cout << "Waiting for result..." << std::endl;
  int sum = result.get(); // 获取异步操作的结果,如果结果还没准备好,会阻塞直到结果可用
  std::cout << "Sum: " << sum << std::endl;

  return 0;
}

在这个例子中,std::async 启动了一个异步任务 calculate_sum,它会在另一个线程中计算两个数的和。result 是一个 std::future<int> 对象,它代表着这个异步任务的返回值。result.get() 会阻塞当前线程,直到异步任务完成并返回结果。

std::future 的局限性

std::future 有一个重要的限制:它只能被 get() 一次。也就是说,一旦你通过 future.get() 获取了异步任务的结果,这个 future 对象就失效了,再次调用 get() 会抛出异常。

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

int main() {
  std::future<int> result = std::async(std::launch::async, []() { return 42; });

  int value1 = result.get();
  std::cout << "Value 1: " << value1 << std::endl;

  try {
    int value2 = result.get(); // 再次调用 get() 会抛出异常
    std::cout << "Value 2: " << value2 << std::endl;
  } catch (const std::future_error& e) {
    std::cerr << "Exception: " << e.what() << std::endl; // 输出 "std::future_error: future_already_retrieved"
  }

  return 0;
}

这个限制在某些情况下会带来不便。例如,如果多个线程都需要访问同一个异步任务的结果,那么使用 std::future 就无法满足需求。

std::shared_future 的登场

std::shared_future 就是为了解决 std::future 的这个局限性而诞生的。它允许多个线程共享同一个异步任务的结果。

std::shared_future 可以从 std::future 对象移动构造而来,或者通过 future.share() 方法创建。一旦创建了 std::shared_future,多个线程就可以通过 shared_future.get() 方法安全地访问异步任务的结果,而不会使 shared_future 对象失效。

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

int main() {
  std::future<int> result = std::async(std::launch::async, []() {
    std::cout << "Calculating result in thread: " << std::this_thread::get_id() << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时操作
    return 42;
  });

  std::shared_future<int> shared_result = result.share(); // 从 future 创建 shared_future

  std::vector<std::thread> threads;
  for (int i = 0; i < 3; ++i) {
    threads.emplace_back([shared_result, i]() {
      std::cout << "Thread " << i << " waiting for result in thread: " << std::this_thread::get_id() << std::endl;
      int value = shared_result.get();
      std::cout << "Thread " << i << " got result: " << value << " in thread: " << std::this_thread::get_id() << std::endl;
    });
  }

  for (auto& thread : threads) {
    thread.join();
  }

  return 0;
}

在这个例子中,我们首先创建了一个 std::future<int> 对象 result,然后通过 result.share() 方法创建了一个 std::shared_future<int> 对象 shared_result。接下来,我们创建了 3 个线程,每个线程都通过 shared_result.get() 方法获取异步任务的结果。所有线程都可以安全地访问结果,而不会发生任何异常。

std::shared_future 的常用方法

方法 描述
get() 获取异步任务的结果。如果结果还没准备好,会阻塞直到结果可用。多个线程可以同时调用 get(),而不会使 shared_future 对象失效。
valid() 检查 shared_future 对象是否有效,即是否关联到一个异步任务。
wait() 阻塞当前线程,直到异步任务完成。
wait_for() 阻塞当前线程,直到异步任务完成或者超时。
wait_until() 阻塞当前线程,直到异步任务完成或者到达指定的时间点。

std::shared_future 的应用场景

std::shared_future 在以下场景中非常有用:

  • 多个线程需要访问同一个异步任务的结果: 这是 std::shared_future 最典型的应用场景。例如,多个线程需要从数据库中读取相同的数据,或者多个线程需要处理同一个图像。
  • 需要缓存异步任务的结果: 可以使用 std::shared_future 将异步任务的结果缓存起来,以便后续的线程可以直接访问缓存的结果,而无需重新执行异步任务。
  • 实现发布/订阅模式: 可以使用 std::shared_future 作为发布者,将异步任务的结果发布给多个订阅者(线程)。

代码示例:使用 std::shared_future 实现缓存

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

class DataCache {
public:
  DataCache(std::function<int()> data_provider) : data_provider_(data_provider) {}

  std::shared_future<int> get_data() {
    std::lock_guard<std::mutex> lock(mutex_);
    if (!cached_data_.valid()) {
      cached_data_ = std::async(std::launch::async, data_provider_).share();
    }
    return cached_data_;
  }

private:
  std::function<int()> data_provider_;
  std::shared_future<int> cached_data_;
  std::mutex mutex_;
};

int main() {
  // 模拟一个耗时的数据获取操作
  auto data_provider = []() {
    std::cout << "Fetching data..." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(3));
    return 123;
  };

  DataCache cache(data_provider);

  std::vector<std::thread> threads;
  for (int i = 0; i < 5; ++i) {
    threads.emplace_back([&cache, i]() {
      std::cout << "Thread " << i << " requesting data..." << std::endl;
      std::shared_future<int> data = cache.get_data();
      int value = data.get();
      std::cout << "Thread " << i << " got data: " << value << std::endl;
    });
  }

  for (auto& thread : threads) {
    thread.join();
  }

  return 0;
}

在这个例子中,DataCache 类使用 std::shared_future 来缓存数据。第一次调用 get_data() 时,会启动一个异步任务来获取数据,并将结果存储在 cached_data_ 中。后续的调用 get_data() 时,会直接返回 cached_data_,而不会重新执行异步任务。这样就实现了数据的缓存,提高了程序的效率。

std::shared_future 的注意事项

  • 异常处理: 如果异步任务抛出异常,那么所有调用 shared_future.get() 的线程都会收到相同的异常。因此,在使用 std::shared_future 时,需要注意异常处理。
  • 避免死锁: 在使用 std::shared_future 时,需要注意避免死锁。例如,如果一个线程在等待 shared_future 的结果,而另一个线程又在持有锁的情况下尝试访问 shared_future,就可能导致死锁。
  • 生命周期管理: std::shared_future 对象必须在所有使用它的线程都结束后才能被销毁。否则,可能会导致程序崩溃。 通常,使用基于RAII(Resource Acquisition Is Initialization)的智能指针可以自动管理对象生命周期。

总结

std::shared_future 是 C++ 中一个非常有用的工具,它允许多个线程共享同一个异步任务的结果。通过使用 std::shared_future,可以提高程序的效率,并简化多线程编程。

std::future vs std::shared_future

为了更好地理解std::shared_future,我们用一个表格来总结一下它和std::future的区别:

特性 std::future std::shared_future
可访问性 只能被 get() 一次 可以被多个线程多次 get()
所有权 拥有独占的异步结果所有权 共享异步结果的所有权
使用场景 单个线程需要获取异步结果 多个线程需要共享同一个异步结果
创建方式 通过 std::async, std::promise等创建 std::future 对象移动构造或使用 future.share()

高级话题:std::promisestd::packaged_task

虽然今天的主题是 std::shared_future,但为了更全面地理解它的使用场景,我们还需要简单了解一下 std::promisestd::packaged_task

  • std::promise std::promise 允许你手动设置 std::future 的结果。这在某些情况下非常有用,例如,当你需要从外部事件(例如,网络连接)中获取异步任务的结果时。

    #include <iostream>
    #include <future>
    #include <thread>
    
    int main() {
      std::promise<int> promise;
      std::future<int> future = promise.get_future();
      std::shared_future<int> shared_future = future.share();
    
      std::thread t([&promise]() {
        std::cout << "Performing some work..." << std::endl;
        std::this_thread::sleep_for(std::chrono::seconds(2));
        promise.set_value(100); // 设置 future 的结果
      });
    
      std::cout << "Waiting for result..." << std::endl;
      int result = shared_future.get();
      std::cout << "Result: " << result << std::endl;
    
      t.join();
      return 0;
    }
  • std::packaged_task std::packaged_task 允许你将一个函数或可调用对象包装成一个异步任务。它会自动创建一个 std::future 对象,并将其与异步任务关联起来。

    #include <iostream>
    #include <future>
    #include <thread>
    
    int main() {
      std::packaged_task<int(int, int)> task([](int a, int b) {
        std::cout << "Calculating sum..." << std::endl;
        std::this_thread::sleep_for(std::chrono::seconds(2));
        return a + b;
      });
    
      std::future<int> future = task.get_future();
      std::shared_future<int> shared_future = future.share();
    
      std::thread t(std::move(task), 10, 20); // 启动异步任务
    
      std::cout << "Waiting for result..." << std::endl;
      int result = shared_future.get();
      std::cout << "Result: " << result << std::endl;
    
      t.join();
      return 0;
    }

std::shared_futurestd::shared_ptr 的关系

虽然名字相似,但 std::shared_futurestd::shared_ptr 是完全不同的概念。std::shared_ptr 是一个智能指针,用于管理动态分配的内存,而 std::shared_future 用于共享异步任务的结果。

不过,它们之间也有一些联系。例如,你可以使用 std::shared_ptr 来管理 std::shared_future 对象,以确保在所有使用它的线程都结束后才释放 std::shared_future 对象。

总结的总结

今天我们深入探讨了 C++ 中的 std::shared_future,了解了它的用途、常用方法、应用场景以及与其他相关概念的关系。希望通过今天的讲座,大家能够更好地理解和使用 std::shared_future,并在实际开发中发挥它的优势。

记住,std::shared_future 就像一份可以被无数人复制的美味汉堡配方,它让你的多线程程序可以高效地共享异步任务的结果,让你的代码更加优雅和强大!

感谢大家的参与!希望下次还能和大家一起探讨更多有趣的 C++ 技术。

发表回复

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