C++多线程常见的数据竞争模式及示例分析

在这里插入图片描述

一、简单竞争
最简单的数据竞争是最常见的一种:两个线程在没有任何同步的情况下访问一个内置类型的变量。很多时候,这种竞争是良性的(代码统计一些允许不精确的统计信息)。

int var;void Thread1() {  // 在一个线程中运行。var++;
}
void Thread2() {  // 在另一个线程中运行。var++;
}

但有时这种竞争是极其有害的(例如,如果var是在计算你的钱的数量时)。

二、线程不友好的引用计数
这是对整数的一种竞争变体。非常有害。可能导致偶尔的内存泄漏或重复释放。

// Ref()和Unref()可能从多个线程被调用。
// 最后一次Unref()会销毁对象。
class RefCountedObject {
...public:void Ref() {ref_++;  // 错误!}void Unref() {if (--ref_ == 0)  // 错误!需要使用原子递减!delete this;}private:int ref_;
};

三、对复杂对象的竞争
另一种常见的竞争发生在两个线程在没有同步的情况下访问一个非线程安全的复杂对象(例如一个 STL 容器)时。这些几乎总是危险的。

std::map<int,int> m;void Thread1() {m[123] = 1;
}void Thread2() {m[345] = 0;
}

四、通知
考虑以下代码:

bool done = false;void Thread1() {while (!done) {do_something_useful_in_a_loop_1();} do_thread1_cleanup();
}void Thread2() {do_something_useful_2();done = true;do_thread2_cleanup();
}

这两个线程之间的同步是通过一个布尔变量done完成的。这是一种错误的同步两个线程的方式。

在 x86 架构上,最大的问题是编译时的优化。

do_something_useful_2()的部分代码可能被编译器移动到“done = true”下面。
do_thread2_cleanup()的部分代码可能被编译器移动到“done = true”上面。
如果do_something_useful_in_a_loop_1()不修改“done”,编译器可能会将Thread1重写为以下方式:

  if (!done) {while(true) {do_something_useful_in_a_loop_1();} }do_thread1_cleanup();

这样Thread1将永远不会退出。

在除 x86 以外的架构上,缓存效应或乱序指令执行可能会导致其他微妙的问题。

大多数竞争检测器会检测到这种竞争。
此外,大多数动态竞争检测器会报告在本应与此布尔值同步的内存访问上的数据竞争(即在do_something_useful_2()do_thread1_cleanup()之间)。

要修复这种竞争,你需要使用编译器和/或内存屏障(如果你不是专家——只需使用锁)。强烈建议将这种同步包装到一个单独的类中(即ExitNotification)。

五、不同步地发布对象
一个线程用一个新值初始化一个对象指针(最初为NULL),另一个线程循环直到该对象指针变为非NULL。如果没有适当的同步,编译器可能会对这样的代码进行非常令人惊讶的转换,这将导致(偶尔的)失败。除此之外,在某些架构上,这种竞争可能会由于缓存相关的效应而导致失败。

MyType* obj = NULL;void Thread1() {obj = new MyType();
}void Thread2() {while(obj == NULL)yield();obj->DoSomething();
}

六、不同步地初始化对象
这可能会导致例如内存泄漏(对象被构造了两次)。

static MyObj *obj = NULL;void InitObj() { if (!obj) obj = new MyObj(); 
}void Thread1() {InitObj();
}void Thread2() {InitObj();
}

七、在写操作期间持有读锁
在持有读锁的情况下进行更新。

void Thread1() {mu.ReaderLock();var++;mu.ReaderUnlock();
}void Thread2() {mu.ReaderLock();var++;mu.ReaderUnlock();
}

八、对位域的竞争
下面的代码乍一看似乎是正确的。但是如果xstruct { int a:4, b:4; },我们就有一个错误。

void Thread1() {x.a++;
}void Thread2() {x.b++;
}

九、双重检查锁定
所谓的双重检查锁定是众所周知的反模式,但我们仍然偶尔会发现它。

bool inited = false;
void Init() {// 可能被多个线程调用。if (!inited) {mu.Lock();if (!inited) {//.. 初始化一些东西}inited = true;mu.Unlock();}
}

十、销毁期间的竞争
有时对象在栈上创建,传递给另一个线程,然后在不等待第二个线程完成其工作的情况下被销毁。

void Thread1() {SomeType object;ExecuteCallbackInThread2(SomeCallback, &object);
...// “object”在离开其作用域时被销毁。
}

十一、对虚函数表指针的数据竞争
一些背景知识:假设你有:

struct A {virtual ~A() {F();}virtual void F() {printf("In A");}
};
struct B : public A {virtual void F() {printf("In B");}
};

C++规定当一个B类型的对象被销毁时,你会看到“In A”被打印出来。更具体地说,在A::~A()期间,所有的虚函数都解析为在A(或其基类)中定义的方法,而不是B中的重写方法。为了实现这一点,gcc和其他编译器在B::~B()的开头将虚函数表指针(指向虚函数表的指针)重新赋值为指向B::vtable,然后在A::~A()的开头再次将其重新赋值为指向A::vtable

现在来看竞争:类A有一个函数Done()、一个虚函数F()和一个虚析构函数。析构函数等待由Done()生成的事件。还有一个类B,它继承自A并覆盖了A::F()

#include <thread>
#include <mutex>class A {public:A() : done_(false) {}virtual void F() { printf("A::F\n"); }void Done() {std::unique_lock<std::mutex> lk(m_);done_ = true;cv_.notify_one();}virtual ~A() {std::unique_lock<std::mutex> lk(m_);cv_.wait(lk, [this] {return done_;});}private:std::mutex m_;std::condition_variable cv_;bool done_;
};class B : public A {public:virtual void F() { printf("B::F\n"); }virtual ~B() {}
};int main() {A *a = new B;std::thread t1([a] {a->F(); a->Done();});std::thread t2([a] {delete a;});t1.join(); t2.join();
} 

创建了一个静态类型为A、动态类型为B的对象a。一个线程执行a->F(),然后向第二个线程发出信号。第二个线程调用delete a(即B::~B),然后调用A::~A,而A::~A反过来又等待来自第一个线程的信号。析构函数A::~A覆盖了虚函数表指针(指向虚函数表的指针)为A::vptr。因此,如果第一个线程在第二个线程开始执行A::~A之后执行a->F(),那么将调用A::F而不是B::F

考虑如果类在构造函数/析构函数中有虚方法,则不要在构造函数/析构函数中使用任何同步。而是使用Start()Join()方法。

如果不继承在构造函数/析构函数中进行同步的类,这种数据竞争是良性的。
ThreadSanitizer可以区分虚函数表指针上的良性和有害竞争。
此外,考虑密封你的类以避免未来出现问题。

十二、构造期间对虚函数表指针的数据竞争
这是虚函数表指针竞争的另一种变体。考虑以下代码:

class Base {Base() {global_mutex.Lock();global_list.push_back(this);global_mutex.Unlock();// 点(A),见下文}virtual void Execute() = 0;
...
};class Derived : Base {Derived() {name_ =...;}virtual void Execute() {// 使用 name_;}string name_;
...
};Mutex global_mutex;
vector<Base*> global_list;// 由后台线程执行。
void ForEach() {global_mutex.Lock();for (size_t i = 0; i < global_list.size(); i++)global_list[i]->Execute()global_mutex.Unlock();
}

乍一看,似乎global_listglobal_mutex正确同步。但是添加到global_list中的对象是部分构造的,但却可供其他线程使用。例如,如果线程在点(A)被抢占,那么ForEach()将触发纯虚函数调用。

十三、对free的竞争
有时程序的一个线程可能会访问堆内存,而另一个线程正在释放相同的内存。

int *array;
void Thread1() {array[10]++;
}void Thread2() {free(array);
}

AddressSanitizer也可以检测到这种错误。

十四、退出期间的竞争
如果一个程序在其他线程仍在运行时调用exit(),静态对象可能会被一个线程销毁,同时被另一个线程使用。(结论:除非你知道自己在做什么,否则不要使用静态非 POD 对象)

#include "pthread.h"
#include <map>static std::map<int, int> my_map;void *MyThread(void *) {  // 在一个单独的线程中运行。int i = 0;while(1) {my_map[(i++) % 1000]++;}return NULL;
}int main() {pthread_t t;pthread_create(&t, 0, MyThread, 0);return 0;// 调用 exit(),my_map 被销毁
}

十五、对互斥锁的数据竞争
对互斥锁的加锁和解锁操作是同步的,但互斥锁仍然可能受到不安全的发布或竞争销毁的影响。考虑以下示例:

class Foo {Mutex m_;
...public:// 异步完成通知。void OnDone(Callback *c) {MutexLock l(&m_);// 处理完成//...// 调度用户完成回调ThreadPool::Schedule(c);}
...
};void UserCompletionCallback(Foo *f) {delete f;  // 不再需要它了// 通知另一个线程完成//...
}

十六、对文件描述符的数据竞争
文件描述符在内部是完全同步的,但用户代码中的竞争可能导致在错误的文件描述符上发出读/写操作。考虑以下示例:

int fd = open(...);// 线程 1。
write(fd,...);// 线程 2。
close(fd);

如果线程 1 和线程 2 相互竞争,并且文件描述符编号在关闭后立即被重用,那么写入操作可能会针对错误的文件或套接字进行。这可能会导致例如将敏感数据泄露到不可信的网络连接中。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/3631.html

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

相关文章

Jest进阶知识:测试快照 - 确保组件渲染输出正确

在 React 应用开发中&#xff0c;确保组件的渲染输出正确是一项重要的测试任务。快照测试是一种有效的方法&#xff0c;可以帮助开发者捕捉并验证组件的渲染输出&#xff0c;确保其在不同的情况下保持一致。 什么是快照测试&#xff1f; 快照测试的基本思想是&#xff1a; 首…

【AI落地应用实战】HivisionIDPhotos AI证件照制作实践指南

最近在网上发现了一款轻量级的AI证件照制作的项目&#xff0c;名为HivisionIDPhotos。它利用AI模型实现对多种拍照场景的识别、抠图与证件照生成&#xff0c;支持轻量级抠图、多种标准证件照和排版照生成、纯离线或端云推理、美颜等功能。此外&#xff0c;项目还提供了Gradio D…

基于SSM的在线作业管理系统 -octopus-master(源码+调试)

项目描述 临近学期结束&#xff0c;还是毕业设计&#xff0c;你还在做java程序网络编程&#xff0c;期末作业&#xff0c;老师的作业要求觉得大了吗?不知道毕业设计该怎么办?网页功能的数量是否太多?没有合适的类型或系统?等等。你想解决的问题&#xff0c;今天给大家介绍…

【SPIE单独出版审核,见刊检索稳定!】2024年遥感技术与图像处理国际学术会议(RSTIP 2024,11月29-12月1日)

2024年遥感技术与图像处理国际学术会议&#xff08;RSTIP 2024&#xff09; 2024 International Conference on Remote Sensing Technology and Image Processing 官方信息 会议官网&#xff1a;www.rstip.org 时间地点&#xff1a;2024年11月29-12月1日 | 中国大理 三轮截…

青少年编程能力等级测评CPA Python编程(一级)

青少年编程能力等级测评CPA Python编程(一级) &#xff08;考试时间90分钟&#xff0c;满分100分&#xff09; 一、单项选择题&#xff08;共20题&#xff0c;每题3.5分&#xff0c;共70分&#xff09; 下列语句的输出结果是&#xff08; &#xff09;。 print(35*2) A&a…

数学篇 - 微分(求导)的基本法则与行列式

一、常数及基本函数的求导规则 常数的导数&#xff1a; ( C ) ′ 0 (C)0 (C)′0 幂函数的导数&#xff1a; ( x μ ) ′ μ x μ − 1 (x^\mu)\mu x^{\mu-1} (xμ)′μxμ−1 三角函数正弦、余弦函数的导数&#xff1a; ( s i n x ) ′ c o s x (sin\ x)cos\ x (sin x)′…

玄机-应急响应- Linux入侵排查

一、web目录存在木马&#xff0c;请找到木马的密码提交 到web目录进行搜索 find ./ type f -name "*.php" | xargs grep "eval(" 发现有三个可疑文件 1.php看到密码 1 flag{1} 二、服务器疑似存在不死马&#xff0c;请找到不死马的密码提交 被md5加密的…

H.266与H.265、AV1、H.264对比

好多开发者希望搞清楚H.266&#xff08;Versatile Video Coding&#xff0c;VVC&#xff09;、H.265&#xff08;High Efficiency Video Coding&#xff0c;HEVC&#xff09;、AV1、H.264&#xff08;Advanced Video Coding&#xff09;四者区别&#xff0c;本文从压缩效率、画…

【征程 6 工具链性能分析与优化-1】编译器预估 perf 解读与性能分析

01 引言 本篇文章中&#xff0c;我们将首先介绍 layerdetails 中的参数信息&#xff0c;然后将结合实例分析如何利用 layerdetails 来分析模型的性能瓶颈&#xff0c;进而对模型的性能进行优化。 02 layerdetails 中信息解读 征程 6 工具链目前提供了两种方式生成性能评估报…

有线电视 1.27.5 | 完全免费的电视直播应用,频道丰富,画质清晰

有线电视是一款针对智能电视和电视盒子开发的在线观看电视应用软件。该软件最大的特色是完全免费&#xff0c;并且支持几乎国内所有的电视台&#xff0c;无论是央视频道还是省卫视频道应有尽有。为了更好地服务用户&#xff0c;有线电视还对电视频道进行了分类&#xff0c;包含…

ML2001-1 机器学习/深度学习 Introduction of Machine / Deep Learning

图片说明来自李宏毅老师视频的学习笔记&#xff0c;如有侵权&#xff0c;请通知下架 影片参考 【李宏毅】3.第一节 - (上) - 机器学习基本概念简介_哔哩哔哩_bilibili 1. 机器学习的概念与任务类型 概念&#xff1a;机器学习近似于寻找函数&#xff0c;用于处理不同类型的任…

Java项目实战II基于Java+Spring Boot+MySQL的植物健康系统(开发文档+数据库+源码)

目录 一、前言 二、技术介绍 三、系统实现 四、文档参考 五、核心代码 六、源码获取 全栈码农以及毕业设计实战开发&#xff0c;CSDN平台Java领域新星创作者&#xff0c;专注于大学生项目实战开发、讲解和毕业答疑辅导。获取源码联系方式请查看文末 一、前言 基于Java、…

推荐一款面向增材制造的高效设计平台:nTopology

nTopology是一款面向增材制造的高效设计平台&#xff0c;平台预置了大量增材制造常用的设计工具包&#xff0c;工程师通过调用若干个预置工具包、或自主开发定制的工具包&#xff0c;建立一个工作流&#xff0c;实现复杂几何结构的参数化设计。nTopology集合了的强大几何建模和…

CreateEvent使用笔记

一、前言 开发中上位机获取或设置下位机参数的接口&#xff0c;有阻塞、非阻塞两种&#xff1a; 1、API非阻塞&#xff0c;异步回调返回结果 2、API阻塞&#xff0c;超时或直接返回结果 对于应用层调用者来说&#xff0c;阻塞API更方便&#xff0c;而要实现阻塞API在windows可使…

从“点”到“面”,热成像防爆手机如何为安全织就“透视网”?

市场上测温产品让人眼花缭乱&#xff0c;通过调研分析&#xff0c;小编发现测温枪占很高比重。但是&#xff0c;测温枪局限于显示单一数值信息&#xff0c;无法直观地展示物体的整体温度分布情况&#xff0c;而且几乎没有功能拓展能力。以AORO A23为代表的热成像防爆手机改变了…

代码随想录一刷——454.四数相加II

我们现在前2个数组中&#xff0c;统计元素之和以及出现的次数&#xff08;用map&#xff09;&#xff0c;随后再另外2个数组中遍历看上面元素之和的相反数是否存在于map中即可。 C&#xff1a; class Solution { public: int fourSumCount(vector<int>& nums1, ve…

本篇万字,博客最细,oled多级菜单代码解析,与实现教程,指针实现(含源码)!!!

目录 教程前言 多级菜单基本知识 驱动文件创建 ​编辑 ​编辑 ​编辑 定义菜单数据类型代码解析 按键代码解析 菜单数据赋值代码解析 菜单按键切换显示代码解析 项目工程移植地址 教程前言 前言&#xff1a;编写不易&#xf…

C++中STL的list类常用接口及其源码解析

1. list是可以在常数范围内在任意位置进行插入和删除的序列式容器&#xff0c;并且该容器可以前后双向迭代。 2. list的底层是双向链表结构&#xff0c;双向链表中每个元素存储在互不相关的独立节点中&#xff0c;在节点中通过指针指向 其前一个元素和后一个元素。 3. list与…

csp2024T3

题目大意&#xff1a;对于每个数而言&#xff0c;可以将其染成红或蓝&#xff0c;对于每一个数&#xff0c;定义其贡献为&#xff0c;当且仅当这个数最近的同色数与其相等&#xff0c;否则其贡献为0&#xff0c;求最大贡献和。 思路&#xff1a;考虑dp 1.考场20多分钟想的奇怪…

Leetcode 198. 打家劫舍 动态规划

原题链接&#xff1a;Leetcode 198. 打家劫舍 class Solution { public:int rob(vector<int>& nums) {int n nums.size();if (n 1)return nums[0];int dp[n];dp[0] nums[0];dp[1] max(nums[1], nums[0]);for (int i 2; i < n; i) {dp[i] max(dp[i - 2] num…