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

news2024/11/26 20:43:51

文章目录

    • 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.coloradmin.cn/o/578001.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Office project 2010安装教程

哈喽&#xff0c;大家好。今天一起学习的是project 2010的安装&#xff0c;Microsoft Office project项目管理工具软件&#xff0c;凝集了许多成熟的项目管理现代理论和方法&#xff0c;可以帮助项目管理者实现时间、资源、成本计划、控制。有兴趣的小伙伴也可以来一起试试手。…

每天一道面试题之String str=“i“与 String str=new String(“i”)一样吗?

String str"i"与 String strnew String(“i”)一样吗&#xff1f; 要想知道二者是否一样&#xff0c;我们只需要通过进行比较&#xff0c;为什么不用equals的原因&#xff0c;大家可以认真阅读这篇文章 测试代码如下&#xff1a; public class Test1 {public stati…

chatgpt赋能python:Python选择器:优化你的SEO

Python 选择器&#xff1a;优化你的SEO 介绍 随着搜索引擎优化(SEO)变得越来越重要&#xff0c;Web 开发人员需要采取各种措施来提高网站的搜索排名。其中之一就是通过使用正确的 HTML 标签和优秀的内容来提高搜索引擎爬虫对网站的理解。这时候 Python 选择器就变得尤为重要。…

chatgpt赋能python:Python选择函数

Python 选择函数 Python是一种流行的编程语言&#xff0c;是许多开发人员的首选。在Python中&#xff0c;有许多函数可供开发人员使用。这些函数不仅提高了开发的速度&#xff0c;而且还可以帮助我们更快地解决许多问题。在本文中&#xff0c;我们将重点介绍Python选择函数。 …

iostat详解

iostat在centOS中默认没有 安装 yum install sysstat 使用 iostat -dxk 5 rrqm/s 每秒进入队列的合并读写请求数 wrqm/sr/s每秒发送到设备的读写请求数w/srKB/s每秒读写的吞吐量&#xff0c;单位KBwKB/savgrq-sz 以扇区为大小的请求大小 avgqu-sz设备队列中等待的请求数await在…

HNU-电路与电子学-综合设计-模型机

写在前面&#xff1a; 每一年的综合设计肯定都有改动&#xff0c;故肯定不能照搬照抄&#xff0c;大家可以借鉴思路&#xff0c;复现成果&#xff0c;但要注意不同之处&#xff0c;以免被0分处理。 模型设计的指导书我不放在这里了&#xff0c;因为那是课程组老师们的成果。 …

HNU-电路与电子学-学习建议

电路与电子学我考的并不好&#xff0c;最后总评只有85分。 &#xff08;主要是期末考太差了&#xff0c;只有70分&#xff0c;前面基本全拿满了&#xff0c;也无济于事&#xff09; 但我自认为学的还可以&#xff0c;于是写下一些感想。 我姑妄言之&#xff0c;诸位姑妄信之…

【华为OD机试】投骰子【2023 B卷|200分】

【华为OD机试】-真题 !!点这里!! 【华为OD机试】真题考点分类 !!点这里 !! 题目描述 骰子是一个立方体,每个面一个数字,初始为左1,右2,前3(观察者方向),后4,上5,下6, 用123456表示这个状态,放置在平面上, 可以向左翻转(用L表示向左翻转1次), 可以向右翻转(用…

C Primer Plus第九章编程练习答案

学完C语言之后&#xff0c;我就去阅读《C Primer Plus》这本经典的C语言书籍&#xff0c;对每一章的编程练习题都做了相关的解答&#xff0c;仅仅代表着我个人的解答思路&#xff0c;如有错误&#xff0c;请各位大佬帮忙点出&#xff01; 1.设计一个函数min(x, y)&#xff0c;…

【利用AI让知识体系化】5种创建型模式

文章目录 创建型模式简介工厂模式抽象工厂模式单例模式建造者模式原型模式 创建型模式 简介 创建型模式&#xff0c;顾名思义&#xff0c;是用来创建对象的模式。在软件开发中&#xff0c;对象的创建往往比一般的编程任务更为复杂&#xff0c;可能涉及到一些琐碎、复杂的过程…

Boosting以及代表算法(Adaboost、GBDT)介绍

一、Boosting介绍 1.什么是Boosting Boosting&#xff08;提升&#xff09;是一种集成学习方法&#xff0c;通过组合多个弱学习器来构建一个强学习器。与Bagging不同&#xff0c;Boosting采用了序列化的方式构建模型&#xff0c;每个弱学习器都是在前一个弱学习器的基础上进行…

Linux-0.11 文件系统exec.c详解

Linux-0.11 文件系统exec.c详解 模块简介 该模块实现了二进制可执行文件和shell脚本文件的加载和执行。 函数详解 create_tables static unsigned long * create_tables(char * p,int argc,int envc)该函数的作用是建立参数和环境变量指针表。 create_table的作用就是建立…

玄子Share - IDEA 2023.1 自定义 代码模板(Servlet)

玄子Share - IDEA 2023.1 自定义 代码模板&#xff08;Servlet&#xff09; 23版 IDEA 内取消了自动生成 Servlet 模板类&#xff0c;不过我们可以自己定义一个 Servlet 模板 步骤 第一步打开 IDEA 设置界面&#xff0c;编辑器 -> 文件和代码模板 -> 点击加号新建模板…

chatgpt赋能python:Python补齐0,让你在编程中更得心应手

Python 补齐0&#xff0c;让你在编程中更得心应手 在Python编程中&#xff0c;我们经常会遇到数字需要补齐0的情况。比如说我们要制作一个日期格式的字符串&#xff0c;例如“2022-02-22”&#xff0c;但是当天数只有一位数时&#xff0c;需要在前面补0&#xff0c;即“2022-0…

chatgpt赋能python:Python转换器——将数据转换为所需格式的工具

Python 转换器——将数据转换为所需格式的工具 Python 是一种功能强大的编程语言&#xff0c;因为它具有处理和分析数据的能力&#xff0c;因此被广泛使用。然而&#xff0c;在现代互联网时代&#xff0c;数据的格式和大小通常不相同&#xff0c;因此经常需要将数据从一种格式…

chatgpt赋能python:Python词形还原:一种优化搜索引擎排名的方法

Python词形还原&#xff1a;一种优化搜索引擎排名的方法 什么是词形还原&#xff1f; 词形还原是自然语言处理&#xff08;NLP&#xff09;中的一种重要技术。它是将单词转化为其最基本的形式的过程&#xff0c;例如动词的原形或名词的单数形式。这个过程旨在帮助计算机更好地…

openGauss 3.1企业版升级至5.0

&#x1f4e2;&#x1f4e2;&#x1f4e2;&#x1f4e3;&#x1f4e3;&#x1f4e3; 哈喽&#xff01;大家好&#xff0c;我是【IT邦德】&#xff0c;江湖人称jeames007&#xff0c;10余年DBA及大数据工作经验 一位上进心十足的【大数据领域博主】&#xff01;&#x1f61c;&am…

什么是SOME/IP?

本文是SOME/IP 官方文档的翻译。原文地址&#xff1a;https://www.autosar.org/fileadmin/standards/R22-11/FO/AUTOSAR_RS_SOMEIPProtocol.pdf 1.引言和概览 2. 协议要求 3. 缩略语和术语 术语/缩略语描述Byte Order Mark字节顺序标记(byte order mark, BOM)是一个Unicode字…

Spring学习记录

目录 工厂模式的三种形态 简单工厂模式 代码&#xff1a; 运行结果&#xff1a; 总结&#xff1a; 工厂模式 代码&#xff1a; 运行结果&#xff1a; 总结&#xff1a; 抽象工厂模式 代码&#xff1a; 运行结果&#xff1a; 总结&#xff1a; bean的单例与多例 设置 …

chatgpt赋能python:Python父类调用子类:多态性的核心思想

Python父类调用子类&#xff1a;多态性的核心思想 多态性是面向对象编程(OOP)的核心思想之一&#xff0c;它允许不同的对象在调用同一个方法时产生不同的行为。Python是一门支持多态性的编程语言。在Python中&#xff0c;使用父类调用子类的方法是实现多态性的关键之一。 什么…