当前位置: 首页 > article >正文

C++并发编程指南03

文章目录

      • 传递参数
        • 2.2.1 基本参数传递
          • 示例:
        • 2.2.2 注意动态变量指针的传递
          • 错误示例:
          • 正确示例:
        • 2.2.3 引用参数的传递
          • 错误示例:
          • 正确示例:
        • 2.2.4 成员函数和对象指针的传递
          • 示例:
          • 带参数的成员函数示例:
        • 2.2.5 支持移动语义的对象传递
          • 示例:
        • 2.2.6 `std::thread`的不可复制性和可移动性
          • 示例:
      • 总结
      • 转移所有权
        • 2.3.1 移动操作与线程所有权
          • 示例:
        • 2.3.2 函数返回`std::thread`对象
          • 示例:
        • 2.3.3 `std::thread`作为参数传递
          • 示例:
        • 2.3.4 `scoped_thread`类
          • 示例:
        • 2.3.5 `joining_thread`类
          • 示例:
        • 2.3.6 使用`std::vector<std::thread>`管理多个线程
          • 示例:
      • 总结
      • 确定线程数量
        • 2.4.1 `std::thread::hardware_concurrency`
        • 2.4.2 并行版的 `std::accumulate`
          • 示例代码:
        • 2.4.3 关键步骤解释
        • 2.4.4 注意事项
      • 总结
      • 线程标识
        • 2.5.1 获取线程标识
        • 2.5.2 比较和操作 `std::thread::id`
        • 2.5.3 使用 `std::thread::id` 的场景
          • 示例1:主线程与其他线程区分
          • 示例2:存储线程信息
        • 2.5.4 输出线程标识符
      • 总结

传递参数

2.2.1 基本参数传递

向可调用对象或函数传递参数非常简单,只需要将这些参数作为std::thread构造函数的附加参数即可。这些参数会被复制到新线程的内存空间中。

示例:
void f(int i, std::string const& s);
std::thread t(f, 3, "hello");
  • 说明:创建了一个调用f(3, "hello")的新线程。即使函数f需要一个std::string对象作为第二个参数,这里使用的是字符串字面值(char const*类型),线程上下文会完成字面值向std::string的转换。
2.2.2 注意动态变量指针的传递

当传递指向动态变量的指针时,必须小心处理,以避免悬空指针问题。

错误示例:
void f(int i, std::string const& s);
void oops(int some_param) {
  char buffer[1024]; // 1
  sprintf(buffer, "%i", some_param);
  std::thread t(f, 3, buffer); // 2
  t.detach();
}
  • 问题buffer是一个局部变量指针,如果函数oopsbuffer转换成std::string之前结束,会导致未定义行为。
正确示例:
void not_oops(int some_param) {
  char buffer[1024];
  sprintf(buffer, "%i", some_param);
  std::thread t(f, 3, std::string(buffer));  // 使用std::string,避免悬空指针
  t.detach();
}
2.2.3 引用参数的传递

如果期望传递引用而不是拷贝整个对象,可以使用std::ref

错误示例:
void update_data_for_widget(widget_id w, widget_data& data); // 1
void oops_again(widget_id w) {
  widget_data data;
  std::thread t(update_data_for_widget, w, data); // 2
  display_status();
  t.join();
  process_widget_data(data);
}
  • 问题:虽然update_data_for_widget期望一个引用,但std::thread构造函数会盲目地拷贝已提供的变量,导致编译错误。
正确示例:
std::thread t(update_data_for_widget, w, std::ref(data));
  • 说明:使用std::ref将参数转换成引用形式,确保传递的是引用而非副本。
2.2.4 成员函数和对象指针的传递

可以传递成员函数指针以及合适的对象指针作为第一个参数。

示例:
class X {
public:
  void do_lengthy_work();
};

X my_x;
std::thread t(&X::do_lengthy_work, &my_x); // 调用my_x.do_lengthy_work()
  • 说明:新线程将调用my_x.do_lengthy_work(),其中my_x的地址作为对象指针提供给函数。
带参数的成员函数示例:
class X {
public:
  void do_lengthy_work(int);
};

X my_x;
int num(0);
std::thread t(&X::do_lengthy_work, &my_x, num);
2.2.5 支持移动语义的对象传递

对于仅支持移动操作的对象(如std::unique_ptr),可以通过显式移动来传递所有权。

示例:
void process_big_object(std::unique_ptr<big_object>);

std::unique_ptr<big_object> p(new big_object);
p->prepare_data(42);
std::thread t(process_big_object, std::move(p));
  • 说明:通过在std::thread构造函数中执行std::move(p)big_object对象的所有权首先被转移到新创建线程的内部存储中,之后再传递给process_big_object函数。
2.2.6 std::thread的不可复制性和可移动性

std::thread实例是不可复制的,但可以移动。这意味着在一个时间点上,一个std::thread实例只能关联一个执行线程,且线程的所有权可以在多个std::thread实例之间转移。

示例:
std::thread t1(f, 3, "hello");
std::thread t2 = std::move(t1); // t1不再关联任何线程,t2现在关联了f(3, "hello")

总结

  • 基本参数传递:直接将参数作为std::thread构造函数的附加参数传递。
  • 动态变量指针传递:注意避免悬空指针问题,必要时先将数据转换为合适类型(如std::string)。
  • 引用参数传递:使用std::ref将参数转换为引用形式。
  • 成员函数和对象指针传递:传递成员函数指针及对象指针作为第一个参数。
  • 支持移动语义的对象传递:使用std::move显式移动对象所有权。
  • std::thread的特性:不可复制但可移动,允许灵活管理线程所有权。

通过掌握这些参数传递技巧,可以更有效地利用C++标准库中的多线程功能。

转移所有权

2.3.1 移动操作与线程所有权

C++标准库中的资源占有类型(如std::ifstreamstd::unique_ptrstd::thread)是可移动但不可复制的。这意味着执行线程的所有权可以在std::thread实例之间转移,而不允许复制。

示例:
void some_function();
void some_other_function();

std::thread t1(some_function);            // 1: 创建新线程
std::thread t2 = std::move(t1);           // 2: 将t1的所有权转移到t2
t1 = std::thread(some_other_function);    // 3: 创建并赋值新线程给t1
std::thread t3;                           // 4: 默认构造,无关联线程
t3 = std::move(t2);                       // 5: 将t2的所有权转移到t3
t1 = std::move(t3);                       // 6: 尝试赋值会导致程序崩溃
  • 说明
    • 步骤①:创建一个新线程t1,它调用some_function
    • 步骤②:使用std::movet1的所有权转移到t2,此时t1不再关联任何线程。
    • 步骤③:创建一个临时std::thread对象,并将其所有权隐式地转移到t1,使其调用some_other_function
    • 步骤④:默认构造t3,它不关联任何线程。
    • 步骤⑤:显式使用std::movet2的所有权转移到t3
    • 步骤⑥:尝试将t3的所有权再次转移到t1,但由于t1已经关联了一个线程,这将导致程序调用std::terminate()终止。
2.3.2 函数返回std::thread对象

通过支持移动操作,std::thread对象可以在函数中创建并返回。

示例:
std::thread f() {
  void some_function();
  return std::thread(some_function);
}

std::thread g() {
  void some_other_function(int);
  std::thread t(some_other_function, 42);
  return t;
}
  • 说明:在函数fg中,std::thread对象被创建并返回,利用了移动语义来转移所有权。
2.3.3 std::thread作为参数传递

std::thread对象可以作为参数传递给其他函数。

示例:
void f(std::thread t);

void g() {
  void some_function();
  f(std::thread(some_function));          // 临时对象隐式移动
  std::thread t(some_function);
  f(std::move(t));                        // 显式移动
}
2.3.4 scoped_thread

为了确保线程在程序退出前完成,可以定义一个scoped_thread类,类似于RAII模式。

示例:
class scoped_thread {
  std::thread t;
public:
  explicit scoped_thread(std::thread t_) :  // 1
    t(std::move(t_)) {
    if (!t.joinable())                     // 2
      throw std::logic_error("No thread");
  }
  ~scoped_thread() {
    t.join();                              // 3
  }
  scoped_thread(scoped_thread const&) = delete;
  scoped_thread& operator=(scoped_thread const&) = delete;
};

struct func; // 定义在代码2.1中

void f() {
  int some_local_state;
  scoped_thread t(std::thread(func(some_local_state))); // 4
  do_something_in_current_thread();
} // 5: 当到达末尾时,析构函数自动调用join()
  • 说明
    • 步骤①:构造函数接受一个std::thread对象并将其移动到成员变量t
    • 步骤②:检查线程是否可汇入,如果不可汇入则抛出异常。
    • 步骤③:析构函数中调用join()确保线程完成。
    • 步骤④:创建scoped_thread对象,直接传递std::thread对象。
    • 步骤⑤:当函数结束时,scoped_thread对象销毁,析构函数自动调用join()
2.3.5 joining_thread

C++17建议添加joining_thread类型,其行为类似于scoped_thread,但在C++20中讨论时名称为std::jthread

示例:
class joining_thread {
  std::thread t;
public:
  joining_thread() noexcept = default;

  template<typename Callable, typename ... Args>
  explicit joining_thread(Callable&& func, Args&& ... args) :
    t(std::forward<Callable>(func), std::forward<Args>(args)...) {}

  explicit joining_thread(std::thread t_) noexcept :
    t(std::move(t_)) {}

  joining_thread(joining_thread&& other) noexcept :
    t(std::move(other.t)) {}

  joining_thread& operator=(joining_thread&& other) noexcept {
    if (joinable()) {
      join();
    }
    t = std::move(other.t);
    return *this;
  }

  joining_thread& operator=(std::thread other) noexcept {
    if (joinable())
      join();
    t = std::move(other);
    return *this;
  }

  ~joining_thread() noexcept {
    if (joinable())
      join();
  }

  void swap(joining_thread& other) noexcept {
    t.swap(other.t);
  }

  std::thread::id get_id() const noexcept {
    return t.get_id();
  }

  bool joinable() const noexcept {
    return t.joinable();
  }

  void join() {
    t.join();
  }

  void detach() {
    t.detach();
  }

  std::thread& as_thread() noexcept {
    return t;
  }

  const std::thread& as_thread() const noexcept {
    return t;
  }
};
2.3.6 使用std::vector<std::thread>管理多个线程

可以通过std::vector<std::thread>批量创建并管理多个线程。

示例:
void do_work(unsigned id);

void f() {
  std::vector<std::thread> threads;
  for (unsigned i = 0; i < 20; ++i) {
    threads.emplace_back(do_work, i); // 创建线程
  }
  for (auto& entry : threads)         // 对每个线程调用 join()
    entry.join();
}
  • 说明:此代码创建了20个线程,每个线程调用do_work函数,并在所有线程完成后调用join()等待它们完成。

总结

  • 移动操作std::thread对象是可移动但不可复制的,允许在线程实例之间转移所有权。
  • 函数返回std::thread对象:通过移动语义,可以从函数中返回std::thread对象。
  • std::thread作为参数传递:可以将std::thread对象作为参数传递给其他函数。
  • scoped_thread:提供了一种确保线程在作用域结束时正确完成的方式。
  • joining_thread:类似于scoped_thread,但在析构时自动调用join(),避免未完成的线程导致程序崩溃。
  • 批量管理线程:使用std::vector<std::thread>可以方便地创建和管理多个线程,确保它们在算法结束前完成。

通过这些技术,可以更灵活且安全地管理多线程程序中的线程所有权和生命周期。

确定线程数量

2.4.1 std::thread::hardware_concurrency

std::thread::hardware_concurrency()函数返回系统支持的并发线程数,通常对应于CPU核心的数量。如果无法获取该值,则返回0。

2.4.2 并行版的 std::accumulate

代码2.9展示了如何实现并行版本的std::accumulate。通过将任务分割成多个小块,并分配给多个线程来加速计算。

示例代码:
template<typename Iterator, typename T>
struct accumulate_block {
  void operator()(Iterator first, Iterator last, T& result) {
    result = std::accumulate(first, last, T());
  }
};

template<typename Iterator, typename T>
T parallel_accumulate(Iterator first, Iterator last, T init) {
  unsigned long const length = std::distance(first, last);

  // 如果输入范围为空,直接返回初始值
  if (!length) return init;

  // 每个线程处理的最小元素数量
  unsigned long const min_per_thread = 25;
  // 最大线程数
  unsigned long const max_threads = (length + min_per_thread - 1) / min_per_thread;

  // 获取硬件支持的并发线程数
  unsigned long const hardware_threads = std::thread::hardware_concurrency();
  // 计算实际使用的线程数
  unsigned long const num_threads = std::min(hardware_threads != 0 ? hardware_threads : 2, max_threads);

  // 每个线程处理的块大小
  unsigned long const block_size = length / num_threads;

  // 存储每个线程的结果
  std::vector<T> results(num_threads);
  // 创建线程容器
  std::vector<std::thread> threads(num_threads - 1);

  Iterator block_start = first;
  for (unsigned long i = 0; i < (num_threads - 1); ++i) {
    Iterator block_end = block_start;
    std::advance(block_end, block_size);
    threads[i] = std::thread(
        accumulate_block<Iterator, T>(),
        block_start, block_end, std::ref(results[i]));
    block_start = block_end;
  }

  // 处理最后一个块
  accumulate_block<Iterator, T>()(block_start, last, results[num_threads - 1]);

  // 等待所有线程完成
  for (auto& entry : threads)
    entry.join();

  // 将所有结果累加并返回最终结果
  return std::accumulate(results.begin(), results.end(), init);
}
2.4.3 关键步骤解释
  1. 检查输入范围是否为空

    if (!length) return init;
    
    • 如果输入范围为空,直接返回初始值init
  2. 确定最大线程数

    unsigned long const max_threads = (length + min_per_thread - 1) / min_per_thread;
    
    • 根据最小任务数(min_per_thread)和总任务数(length),计算出可以启动的最大线程数。
  3. 确定实际使用的线程数

    unsigned long const num_threads = std::min(hardware_threads != 0 ? hardware_threads : 2, max_threads);
    
    • 取硬件支持的线程数与最大线程数中的较小值作为实际使用的线程数。如果没有获取到硬件线程数,则默认使用2个线程。
  4. 计算每个线程处理的块大小

    unsigned long const block_size = length / num_threads;
    
  5. 创建存储结果的容器和线程容器

    std::vector<T> results(num_threads);
    std::vector<std::thread> threads(num_threads - 1);
    
  6. 启动线程处理每个块

    for (unsigned long i = 0; i < (num_threads - 1); ++i) {
      Iterator block_end = block_start;
      std::advance(block_end, block_size);
      threads[i] = std::thread(
          accumulate_block<Iterator, T>(),
          block_start, block_end, std::ref(results[i]));
      block_start = block_end;
    }
    
  7. 处理最后一个块

    accumulate_block<Iterator, T>()(block_start, last, results[num_threads - 1]);
    
  8. 等待所有线程完成

    for (auto& entry : threads)
      entry.join();
    
  9. 累加所有结果并返回

    return std::accumulate(results.begin(), results.end(), init);
    
2.4.4 注意事项
  • 结合律问题:对于不满足结合律的操作(如浮点数加法),并行计算的结果可能与顺序计算的结果不同。
  • 迭代器要求:算法需要前向迭代器才能正常工作。
  • 结果存储:需要确保类型T有默认构造函数,以便在results容器中存储中间结果。
  • 线程标识符:C++线程库为每个线程附加了唯一的标识符,可以在设计时利用这些标识符来传递必要的信息。

总结

  • std::thread::hardware_concurrency:用于获取系统支持的并发线程数。
  • 并行版 std::accumulate:通过分割任务并分配给多个线程来加速计算。
  • 关键步骤:包括检查输入范围、确定线程数、分割任务、启动线程、等待线程完成以及累加结果。
  • 注意事项:考虑结合律问题、迭代器要求和结果存储方式等。

通过上述技术,可以有效地利用多线程加速计算密集型任务,并确保程序的正确性和高效性。

线程标识

2.5.1 获取线程标识

C++ 提供了两种方式来获取线程的标识符(std::thread::id):

  1. 通过 std::thread 对象的成员函数 get_id()

    std::thread t(some_function);
    std::thread::id thread_id = t.get_id();
    
    • 如果 std::thread 对象没有与任何执行线程相关联,get_id() 将返回默认构造的 std::thread::id 值,表示“无关联线程”。
  2. 通过调用当前线程的 std::this_thread::get_id()

    #include <thread>
    std::thread::id current_thread_id = std::this_thread::get_id();
    
    • 这个函数可以在当前线程中调用,以获取当前线程的标识符。
2.5.2 比较和操作 std::thread::id
  • 拷贝和比较std::thread::id 对象可以自由地拷贝和比较。如果两个 std::thread::id 对象相等,则它们代表同一个线程或都代表“无关联线程”。

  • 排序和哈希std::thread::id 类型提供了丰富的对比操作,包括排序和哈希支持。这使得它们可以作为容器的键值使用,例如:

    std::unordered_map<std::thread::id, int> thread_data;
    
2.5.3 使用 std::thread::id 的场景
示例1:主线程与其他线程区分

在某些情况下,主线程可能需要执行与其他线程不同的任务。可以通过 std::this_thread::get_id() 来获取主线程的 ID,并在其他线程中进行比较。

std::thread::id master_thread;

void some_core_part_of_algorithm() {
  if (std::this_thread::get_id() == master_thread) {
    do_master_thread_work();  // 主线程的工作
  }
  do_common_work();  // 所有线程共有的工作
}

int main() {
  master_thread = std::this_thread::get_id();  // 获取主线程ID
  std::thread t(some_core_part_of_algorithm);
  t.join();
  return 0;
}
示例2:存储线程信息

可以将 std::thread::id 存储在数据结构中,用于后续的操作决策。例如,在多个线程之间传递信息时,可以根据线程 ID 来决定是否允许或需要执行某些操作。

#include <iostream>
#include <unordered_map>
#include <thread>

std::unordered_map<std::thread::id, int> thread_info;

void store_thread_info(int value) {
  std::thread::id current_id = std::this_thread::get_id();
  thread_info[current_id] = value;  // 存储当前线程的信息
}

void process_thread_info() {
  std::thread::id current_id = std::this_thread::get_id();
  if (thread_info.find(current_id) != thread_info.end()) {
    std::cout << "Thread " << current_id << " has value: " << thread_info[current_id] << std::endl;
  } else {
    std::cout << "No information for thread " << current_id << std::endl;
  }
}

int main() {
  store_thread_info(42);  // 在主线程中存储信息
  std::thread t(store_thread_info, 100);  // 在新线程中存储信息
  t.join();

  process_thread_info();  // 在主线程中处理信息
  std::thread t2(process_thread_info);  // 在新线程中处理信息
  t2.join();

  return 0;
}
2.5.4 输出线程标识符

可以使用输出流(如 std::cout)来记录一个 std::thread::id 对象的值。具体的输出结果依赖于具体实现,但 C++ 标准要求相同线程的 ID 必须有相同的输出。

#include <iostream>
#include <thread>

void print_thread_id() {
  std::cout << "Current thread ID: " << std::this_thread::get_id() << std::endl;
}

int main() {
  print_thread_id();  // 输出主线程的ID
  std::thread t(print_thread_id);  // 输出新线程的ID
  t.join();
  return 0;
}

总结

  • 获取线程标识:可以通过 std::thread 对象的 get_id() 方法或 std::this_thread::get_id() 函数获取线程标识符。
  • 比较和操作std::thread::id 对象可以自由拷贝和比较,并且支持排序和哈希操作,适合作为容器的键值。
  • 应用场景:常见应用包括区分主线程和其他线程、存储线程特定信息以及根据线程 ID 决策操作。
  • 输出线程 ID:可以使用 std::cout 输出线程 ID,具体格式依赖于实现,但相同线程的 ID 输出必须一致。

通过这些特性,开发者可以更灵活地管理和识别多线程程序中的各个线程,确保程序的正确性和高效性。


http://www.kler.cn/a/525189.html

相关文章:

  • Go反射指南
  • .NET MAUI 入门学习指南
  • Java 在包管理与模块化中的优势:与其他开发语言的比较
  • 代码随想录算法训练营第三十八天-动态规划-完全背包-279.完全平方数
  • C语言实现统计数组正负元素相关数据
  • KNN算法学习实践
  • 【JavaWeb】利用IntelliJ IDEA 2024.1.4 +Tomcat10 搭建Java Web项目开发环境(图文超详细)
  • 商品信息管理自动化测试
  • 落地基于特征的图像拼接
  • 研发的立足之本到底是啥?
  • 跨平台物联网漏洞挖掘算法评估框架设计与实现文献综述之Gemini
  • 我的求职之路合集
  • zsh安装插件
  • Vue演练场基础知识(七)插槽
  • sentence_transformers安装
  • BGP分解实验·15——路由阻尼(抑制/衰减)实验
  • 关于Java的HttpURLConnection重定向问题 响应码303
  • 《DeepSeek R1:开启AI推理新时代》
  • C++实现2025刘谦魔术(勺子 筷子 杯子)
  • 第十六届蓝桥杯大赛软件赛(编程类)知识点大纲
  • 25年1月-A组(萌新)- 云朵工厂
  • 本地部署Deepseek R1
  • S价标准价与V价移动平均价的逻辑,以SAP MM采购订单收货、发票校验过程举例
  • 【Valgrind】安装报错: 报错有未满足的依赖关系: libc6,libc6-dbg
  • 【硬件测试】基于FPGA的QPSK+帧同步系统开发与硬件片内测试,包含高斯信道,误码统计,可设置SNR
  • 网络爬虫学习:应用selenium获取Edge浏览器版本号,自动下载对应版本msedgedriver,确保Edge浏览器顺利打开。