pthread多线程:传入参数并检查 data race

news/2025/3/19 7:54:46/

文章目录

    • 1. 目的
    • 2. 给子线程传入参数:万能类型 `void*`
    • 3. data race
      • 3.1 什么是 data race
      • 3.2 怎样检测 data race
    • 4. data race 的例子
      • 4.1 子线程传入同一个 data
      • 4.2 使用栈内存
    • 5. 解决 data race 问题
      • 5.1 忽视问题?
      • 5.2 避开同一个变量的使用
      • 5.3 使用互斥锁(mutex)
      • 5.4 使用条件变量 (cond var)

在这里插入图片描述

1. 目的

使用 pthread 创建多线程时,子线程和主线程之间、子线程和子线程之间, 很可能需要数据交互, 例如读取相同的输入, 汇总每个线程的输出, 不同线程的结果可能需要以累加方式汇总,等等。传入数据时需要了解void* 类型转换。

数据交互的类型包括读取(read)和写入(write)两种类型, 如果没有处理好, 可能导致 data race 的情况, 而 data race不一定表现出结果不正确, 或者结果正确但不会crash, 或者只有在运行了很长时间后才 crash, 甚至是极低概率的偶现crash 的问题。需要确保数据交互是安全的,也就是避免 data race, 这需要了解 ThreadSanitizer 等工具的使用。

2. 给子线程传入参数:万能类型 void*

线程函数的参数必须是 void* 类型, 因此创建线程(也就是执行 pthread_create)时,传入的最后一个参数, 会被自动转化为void*类型。任何类型都可以转为 void* 型:

    int data = 123;pthread_create(&t, NULL, hello, &data);

也可以手动显式转换:

    int data = 123;pthread_create(&t, NULL, hello, (void*)(&data));

而在子线程函数中, void* 可以转为任意的类型。对于 C 语言, 支持隐式转换,也就是说等号左侧需要写具体的(指针)类型、等号右侧不必写出具体类型;而对于 C++, 则需要在等号右侧显式给出类型。统一起见,我们写出既能用于C也能用于C++的类型转换写法:

void* hello(void* param)
{int* data = (int*)param;...
}

以下是完整能运行的代码:

//
// 创建1个线程, 创建时传入一个参数。 在线程函数中读取这个参数。
//#include <stdio.h>
#include <pthread.h>void* hello(void* param)
{int* data = (int*)param;printf("data is %d\n", *data);return NULL;
}int main()
{pthread_t t;int data = 123;pthread_create(&t, NULL, hello, &data);pthread_join(t, NULL);return 0;
}

3. data race

3.1 什么是 data race

data race 中文含义是数据竞争,所谓竞争就需要至少两个对手,两个对手之间有排斥关系,以及至少一个被竞争的物品。严谨一些的定义如下:

  • 存在至少两个线程(threads),它们访问同一个数据(data)
  • 这些线程当中,至少有一个线程是对这个数据执行写入(write)操作

3.2 怎样检测 data race

使用神器 Thread Sanitizer(简称TSan) 可以检查 data race 问题。

需要当前编译器支持 TSan, 目前(2023-05-28 00:16:12)Windows 的 Visual Studio 2022 还不支持 TSan, 不过 Linux, MacOSX 平台的 GCC, CLang 是支持 TSan 的。

还需要构建时传入编译链接选项 -fsanitize=thread .

对于 TSAN_OPTION 环境变量, 不需要额外设置。

编译出可执行程序后, 执行程序, 如果存在 data race, 会报告打印到控制台。

4. data race 的例子

4.1 子线程传入同一个 data

让每个子线程函数传入的参数, 都是同一个指针,

虽然用到的线程函数 print_message 没有写入操作, 但是其实可以写入, 仍然是危险的。

代码如下:

//
// 这是一个反面例子。
// 
// 创建了多个线程, 每个线程的参数是同一个。 存在的风险: data race.
// 虽然用到的线程函数 print_message 没有写入操作, 但是其实可以写入。#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>void* print_message(void* param)
{int id = *(int*)param;printf("Hello from thread %d\n", id);return NULL;
}int main()
{const int thread_num = 2;pthread_t t[thread_num];int* id = (int*)malloc(sizeof(int));for (int i = 0; i < thread_num; i++){// 将变量 i 赋值给 *id// id 变量是堆内存申请的, 能否规避掉 stack-use-after-scope 和 data race?// 答案是不能。*id = i;pthread_create(&t[i], NULL, print_message, id);}for (int i = 0; i < thread_num; i++){pthread_join(t[i], NULL);}free(id);return 0;
}

执行编译并运行:

zz@Legion-R7000P% clang++ multiple_thread_data_race_by_same_param.cpp -fsanitize=thread
zz@Legion-R7000P% ./a.out 

TSan 用红色标出存在 data race,用蓝色标出具体的线程, 用绿色标出 race 的 data 有多大:
在这里插入图片描述

4.2 使用栈内存

需要首先了解栈内存(stack)和堆内存(heap)的区别,heap 内存是 malloc / new 方式申请的, 栈内存则是普通变量, 并且有显著的生命周期。

如下例子使用 for 循环中的循环变量 i, 由于 i 是 for 循环起始时定义的, 每次循环时都“活着”, 而如果每次循环把 i 的地址作为线程函数参数传入, 会导致子线程都可以修改变量 i, 导致了潜在的 data race。 代码如下:

//
// 这是一个反面例子。
//
// 创建2个线程.
// 传入线程函数的参数, 使用的是主线程的单次 for 循环的变量 i 的地址, scope 上有问题.
// 导致了 data race。 应当避免。
// #include <stdio.h>
#include <pthread.h>void* print_message(void* param)
{int* data = (int*)param;printf("data is %d\n", *data);return NULL;
}// 这个函数里就是错误的用法
int main()
{const int thread_num = 2;pthread_t t[thread_num];for (int i = 0; i < thread_num; i++){// 将变量 i 作为传给 print_message() 的变量// 由于 i 使用的是栈内存, 不能给子线程用// asan 会产生报告  "stack-use-after-scope"// tsan 则会产生报告 "data race"pthread_create(&t[i], NULL, print_message, &i);}for (int i = 0; i < thread_num; i++){pthread_join(t[i], NULL);}return 0;
}

执行编译和运行, TSan 这次也报告了 data race 问题

zz@Legion-R7000P% clang++ multiple_thread_data_race_by_stack_memory.cpp -fsanitize=thread 
zz@Legion-R7000P% ./a.out 

在这里插入图片描述

5. 解决 data race 问题

5.1 忽视问题?

如果假装不知道 ThreadSanitizer 这一神器, 又或者代码是在 Windows Visual Studio、Android NDK 平台这样的不支持 Thread Sanitizer 的编译器环境下, 好像可以“自我欺骗”, 觉得“代码和人有一个可以跑就行了”。但这样无法保证程序的正确性, 风险较大。

换言之, 如果可能, 尽量写跨平台的程序, 并在 CI/CD 阶段配置不同的操作系统、编译器执行构建, 然后到支持 TSan 的平台上执行检查。

5.2 避开同一个变量的使用

结合具体的场景, 看能否使用不同的变量来作为线程的函数, 如果确实可以用不同的参数, 那就不存在 data race。

例如如下代码, 创建两个线程, 并让每个线程使用独立的参数, 从而规避 data race 问题

//
// 创建两个线程, 并让每个线程使用独立的参数, 从而规避 data race 问题
//#include <pthread.h>
#include <stdio.h>void* print_message(void* param)
{int* p = (int*)param;*p = *p + 1;int id = *p;printf("id is %d\n", id);return NULL;
}int main()
{const int thread_num = 2;pthread_t t[2];int id[2];for (int i = 0; i < thread_num; i++){id[i] = i;pthread_create(&t[i], NULL, print_message, &id[i]);}for (int i = 0; i < thread_num; i++){pthread_join(t[i], NULL);}return 0;
}

5.3 使用互斥锁(mutex)

mutex 可以作为避免 data race 的一种基础的、部分有效的手段。本篇不做具体展开,后续会介绍。

5.4 使用条件变量 (cond var)

条件变量需要和 mutex 搭配使用, 相当于 mutex 的补充。本篇不做具体展开,后续会介绍。


http://www.ppmy.cn/news/96942.html

相关文章

jQuery样式操作和效果操作

1. css方法 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content"widthdevice-width,…

一图看懂 pycodestyle 模块:一个检查Python代码是否符合PEP8风格约定的工具,资料整理+笔记(大全)

本文由 大侠(AhcaoZhu)原创&#xff0c;转载请声明。 链接: https://blog.csdn.net/Ahcao2008 一图看懂 pycodestyle 模块&#xff1a;一个检查Python代码是否符合PEP8风格约定的工具&#xff0c;资料整理笔记&#xff08;大全&#xff09; &#x1f9ca;摘要&#x1f9ca;模块…

Microsoft Application Control部署方案

目录 前言 第一章:Microsoft Application Control概述 1.1 Microsoft Application Control的定义 1.2 Microsoft Application Control的优势

学习c语言中的几道习题(小有难度)!

有兴趣的朋友可以看着题目自己做做&#xff0c;最后在和答案对比&#xff01;相信能力会有所提升的。我现在只是刚刚开始学习c语言&#xff0c;如果有什么说的不对的地方&#xff0c;网路过的大佬&#xff0c;及时予以指正。多谢&#xff01; 1、函数判断闰年 实现函数判断yea…

使用Docker部署Jenkins

Jenkins是一款开源的持续集成&#xff08;DI&#xff09;工具&#xff0c;广泛用于项目开发&#xff0c;能提供自动构建&#xff0c;测试&#xff0c;部署等功能。 文章目录 1、安装2、配置镜像加速3、登录初始化Jenkins4、配置Jenkins 1、安装 接下来使用Docker部署Jenkins&a…

了解list

list 1. list的介绍及使用1.1 list的介绍1.2 list的使用1.2.1 list的构造1.2.2 list iterator的使用1.2.3 list capacity1.2.4 list element access1.2.5 list modifiers1. resize2. push_back/pop_back/push_front/pop_front3. insert /erase4. swap/clear 1.2.6 list operati…

设置参考文献编号与文中插入引用的具体步骤

目录 一、前言 二、操作步骤 &#xff08;一&#xff09;参考文献设置编号 &#xff08;二&#xff09;文章中引用参考文献方式 一、前言 本教程使用的软件是WPS 二、操作步骤 &#xff08;一&#xff09;参考文献设置编号 1.把引用文献的这个编号全部删掉 2.右键点击段…

关于“烫烫烫烫烫烫烫”的程序员笑话

环境 Microsoft Visual Studio Community 2022Windows 11 家庭中文版 笑话 小明在超市买了3瓶汽水&#xff0c;他先打开第0瓶汽水&#xff0c;咕咚咕咚喝光了&#xff0c;接着打开第1瓶汽水&#xff0c;又咕咚咕咚喝光了&#xff0c;然后又打开第2瓶汽水&#xff0c;咕咚咕咚…