《Linux多线程服务端编程》读书笔记(一)

news2025/1/18 17:10:43

线程安全

一个线程安全的类应该满足下面三个条件

  • 多个线程同时访问,其表现出正确的行为
  • 无论操作系统如何调度这些线程,无论这些线程的执行顺序如何交织
  • 调用端代码无需额外的同步或其他协调动作

对象的线程安全

对象构造要做到线程安全,唯一的要求是在构造期间不要泄露this指针,即

  • 不要在构造函数中注册任何回调
  • 不要在构造函数中把this传给跨线程的对象:如果在构造期间将指针泄露出去,那么别的线程可能会访问到一个没有构造完成的对象,这样可能会造成不可预知的后果。
  • 即使在构造函数的最后一行也不安全:该类可能是一个基类,基类的构造函数先于派生类构造;创建派生类对象是执行完基类构造的最后一行还会去接着执行子类的构造函数,这样执行完该行后此对象还是处于构造过程,所以依旧不安全。
// 错误的做法
class Foo : public Observer
{
    public:
    	Foo(Observable* s)
        {
            s->register_(this);
        }
    	virtual void update();
};

正确的做法如下

class Foo : public Observer
{
    public:
    	Foo();
    	virtual void update();
    
    	// 定义另外一个函数用来进行注册
    	void observe(Observable* s)
        {
            s->register_(this);
        }
};

// 声明对象
Foo* foo = new Foo;
Observable*s = getSubject();
foo->observe(s);

这样使用两段式的构造:即构造器+初始化器的组合,就能避免上面的问题。

但是在多线程条件下,由于析构函数造成的竞态条件有很多:可能出现在析构一个对象的过程中,有可能另一个线程还在执行成员函数。

用互斥锁也无法解决上面这种问题:对于一般的成员函数,使用互斥锁保护临界区就能保证程序运行正确,但正确的前提是互斥锁必须是正常工作的;析构函数会把对象的mutex成员变量销毁,这就破坏了上面的条件。例如下面的代码

Foo::~Foo()
{
    std::lock_guard<std::mutex> lock(mutex_);
    // (1)
}

void Foo::update()
{
    std::lock_guard<std::mutex> lock(mutex_);  // (2)
}

假如有A、B两个线程都能看到Foo对象x,线程A即将销毁x,线程B正准备调用x->update()

// thread A
delete x;
x = NULL;

// thread B
if (x) {
    x->update();
}

此时线程A执行到析构函数的(1)处,已经持有了互斥锁;线程B通过了if(x)的判断,并且阻塞在(2)处。这时就会发生不可预料的事情了。

原始指针的问题

有两个指针A、B指向同一个Object对象,当一个线程通过指针A销毁对象的时候,B就变成了空悬指针。

一个解决空悬指针的办法是加入一层代理,让A、B指针都指向代理对象,代理对象持有一个指向Object对象的指针,当Object被销毁的时候,代理对象依旧存在,另一个线程也可以通过访问代理对象查看Object对象是否存活。但是这样竞态条件依旧存在,当B线程查看了对象存活后,将要调用对象的成员函数,但是此时A线程销毁了该对象,就会造成不可知的后果。

另一个更好的解决方法是引入引用计数(reference counting):这时我们给代理对象增加一个成员count,用于记录实际对象被引用的次数;每当对象执行析构函数时,让代理对象的count自减,当count归零时,我们就可以非常安全的销毁代理对象和Object对象了,因为此时不可能再有任何线程访问到代理对象了(因为没有引用)。

其实最后一种解决方法就是智能指针。

智能指针shared_ptr/weak_ptr

  • shared_ptr控制对象的生命周期。shared_ptr是强引用,只要有一个指向x对象的shared_ptr存在,该对象就不会被析构。当指向对象的最后一个shared_ptr析构或reset时,对象保证会被析构。
  • weak_ptr不控制对象的生命周期,但是它知道对象是否存活。如果对象还活着,那么它可以提升(promote)为有效的shared_ptr;如果对象已经死了,提升就会失败,返回一个空的shared_ptr。“提升”行为是线程安全的。
  • shared_ptr/weak_ptr的“计数”在主流平台上是原子操作,没有用锁,效率很高

image-20230823152019421

这样还是会有一定问题产生

  • 侵入性:强制要求Observer必须以shared_ptr来管理
  • 不完全线程安全:Observer的析构函数会调用subject_->unregister(this),为了得知subject_是否存活,又要在Observer中使用智能指针来管理Observable
  • 锁争用:Observable的三个成员函数都使用了互斥锁来进行同步,这就会导致register_(), unregister() 会无休止的等待notifyObservers(),而因为它同步调用了用户的update()函数,造成notifyObservers()的执行时间过长,而我们希望register_(), unregister()的执行时间不会超过某个上限
  • 死锁:如果update()中调用了(un)register(),如果mutex_是不可重入的,那么就会造成死锁;如果是可重入的,那么就有可能造成迭代器失效,因为vector在遍历期间被修改了。

shared_ptr的线程安全

shared_ptr本身不是线程安全的,它的引用计数本身是安全无锁的,但是它本身作为一个对象不是线程安全的,因为shared_ptr有两个数据成员(一个指向对象的指针和一个ref_count对象,在复制一个shared_ptr的过程中就可能会产生问题),读写操作不能原子化。shared_ptr的线程安全级别和内建类型、标准库容器、std::string一样:

  • 一个shared_ptr对象实体可以被多个线程同时读取
  • 两个shared_ptr对象可以被两个线程分别同时写入
  • 如果多个线程要同时读写一个shared_ptr对象,那么就需要加锁

多个线程要同时访问一个shared_ptr,则我们需要使用mutex保护:

std::mutex mutex;  // 不需要使用读写锁,因为临界区很小
shared_ptr<Foo> globalPtr;

// 任务是把globalPtr安全地传递给doit()
void doit(const shared_ptr<Foo>& pFoo);
void read()
{
    shared_ptr<Foo> localPtr;
    {
        std::lock_guard<std::mutex> lock(mutex);
        localPtr = globalPtr; // 读操作
    }
    // 这里读写本地变量localPtr无需加锁了
    doit(localPtr);
}
void write()
{
    shared_ptr<Foo> newPtr(new Foo);  // 对象创建写在临界区外,减小临界区,效率更好
    {
        std::lock_guard<std::mutex> lock(mutex);
        globalPtr = newPtr;  // 把内容写入到globalPtr中
    }
    doit(newPtr);
}

shared_ptr技术与陷阱

  • 意外延长对象的生命期:由于shared_ptr是允许拷贝构造和赋值的,所以如果不小心遗漏了一个拷贝,那么这个对象将永远存活,这也是Java内存泄漏的常见原因。
    另外一个可能出错的地方是boost::bind ,因为boost::bind会把实参拷贝一分,如果参数是一个shared_ptr,那么对象的生命期就不会短于boost::function对象

    class Foo
    {
        void doit();
    };
    shared_ptr<Foo> pFoo(new Foo);
    boost::function<void()> func = boost::bind(&Foo::doit, pFoo);
    
  • 函数参数:因为要修改引用计数(拷贝的时候通常也要加锁),shared_ptr的拷贝开销比原始指针要高。但是多数情况下可以以const reference方式传递,一个线程只需要在最外层函数有一个实体对象,之后都可以使用const reference来使用这个shared_ptr

    void save(const shared_ptr<Foo>& pFoo);
    void validateAccount(const Foo& foo);
    
    bool validate(const shared_ptr<Foo>& pFoo);
    {
        validateAccount(*pFoo);
    }
    
    // 通过传常引用提高效率
    void onMessage(const string& msg)
    {
        shared_ptr<Foo> pFoo(new Foo(msg));
        if (validate(pFoo)) {  // 没有拷贝,但由于指针在栈上,所以不会产生竞态条件
            save(pFoo);  // 与上同理
        }
    }
    
  • 析构动作在创建时被捕获:特性,这个特性使得

    • 虚析构函数不再是必须
    • shared_ptr<void>可以持有任何对象,并且可以安全释放
    • 析构动作可以定制
  • 析构所在的线程:对象的析构是同步的。当指向x的最后一个shared_ptr离开作用域时,x就会在同一个线程被销毁。这个线程是不固定的,因此如果对象的析构十分耗时,那么就有可能拖慢关键进程。因此我们可以使用一个单独的线程用来做析构,通过某种手段将对象的析构转移到专用的线程。

  • 线程的 RAII(资源获取即初始化) handle:每一个明确的资源配置动作(例如new)都应该在单一语句中执行,并在该语句中立刻将配置获得的资源交给handle对象(如shared_ptr),程序中一般不出现delete。使用shared_ptr的时候需要注意避免循环引用,通常做法是owner持有childshared_ptrchild持有ownerweak_ptr

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

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

相关文章

弹幕引擎使用教学

欢迎阅读 弹幕引擎 / 弹幕会模板 使用教学&#xff01; 不知道您是否听说过“弹幕游戏”&#xff0c;Scratch 闯关游戏作品&#xff1a;东方虹魔馆 如果听说过&#xff0c;那就太好啦&#xff01;您很有可能已经对“弹幕”甚至“符卡”有了认识基础。没有也不要紧&#xff0c;您…

智能化新十年,“全栈智能”定义行业“Copilot智能助手”

“智能化转型是未来十年中国企业穿越经济周期的利器”&#xff0c;这是联想集团执行副总裁兼中国区总裁刘军在去年联想创新科技大会上做出的判断&#xff0c;而2023年正值第四次工业革命第二个十年的开端&#xff0c;智能化是第四次工业革命的主题。2023年初&#xff0c;基于谷…

红盟云卡系统v1.1.17虚拟商品在线售卖平台源码

红盟云卡系统是一款基于PHPMySQL开发的虚拟商品在线售卖平台 v1.1.17.20230627 增加强制登录插件 增加QQ微信防红插件 增加首页弹窗插件 增加鱼儿游背景特效插件 官方微信支付插件增加jsapi类型 后台订单列表增加下单必填项字段显示 修复分站个人中心开通分站报错的问题 修复提…

FAST协议详解3 可null(空)类型

一、概述 所谓可null、可空&#xff0c;其实是一个特性的两个方面&#xff0c;某些情况下&#xff0c;我们不需要传递某个字段的值&#xff0c;则可以将该字段“空”起来&#xff0c;不赋值&#xff0c;则接收方在收到该字段时会自动解析为null值。所以空是对于发送方而言&…

控制各种开环伺服阀放大器

控制不带电气位置反馈模块式伺服阀开环控制&#xff0c;最大电流10mA至1000mA范围可选&#xff1b; 常规可选电流档位&#xff1a;10mA、15mA、20mA、40mA、100mA、300mA等&#xff1b; 前面板电位器或者上位机精细调整零位及增益。 颤振频率以及颤振幅度可选。 快速电流驱…

初入职场六个注意

点击下方关注我&#xff0c;然后右上角点击...“设为星标”&#xff0c;就能第一时间收到更新推送啦~~~ 作为一个职场新人&#xff0c;最重要的变化是从学校的学习生活转变进入职场的工作生活&#xff0c;一切都是新鲜的&#xff0c;步入职场就是进入了社会。 其实学校也是一个…

高忆管理:成交量突然放大股价下跌?

在出资股票时&#xff0c;咱们常常看到股票价格急剧跌落&#xff0c;而此时成交量正在暴增。许多出资者进入股市的初期或许会产生困惑&#xff0c;将“成交量忽然扩大股价跌落”视为出资时的一般改变和常态&#xff0c;但其实并不总是如此。这种现象或许暗示着不同的问题和情况…

Qt:界面实时响应鼠标拖动绘制

采用双缓冲实现界面实时响应鼠标的拖动绘制。 思想如下&#xff1a;首先需要两张画布pix和tempPix&#xff0c;他们都是QPixmap实例&#xff1b;pix用来保存初始界面或上一阶段以完成的绘制&#xff1b;tempPix用来作为鼠标拖动时的实时界面绘制&#xff1b;当鼠标左键按下后拖…

C# 生成唯一ID

1.首先通过nuget安装yitter.idgenerator 下面的三行代码搞定

文心一言 VS 讯飞星火 VS chatgpt (83)-- 算法导论8.1 4题

四、用go语言&#xff0c;假设现有一个包含n个元素的待排序序列。该序列由 n/k 个子序列组成&#xff0c;每个子序列包含k个元素。一个给定子序列中的每个元素都小于其后继子序列中的所有元素&#xff0c;且大于其前驱子序列中的每个元素。因此&#xff0c;对于这个长度为 n 的…

C++ | 源码分析 Why double free?

源码分析 Why double free? 文章目录 源码分析 Why double free?WhatWhy1.浅拷贝 VS 深拷贝浅拷贝深拷贝 2.push_back 和 emplace_backpush_back 源码emplace_back 源码 Example HowReference>>>>> 欢迎关注公众号【三戒纪元】 <<<<< What 前…

【爬虫小知识】如何利用爬虫爬网页——python爬虫

前言 网络时代的到来&#xff0c;给我们提供了海量的信息资源&#xff0c;但是&#xff0c;想要获取这些信息&#xff0c;手动一个一个网页进行查找&#xff0c;无疑是一项繁琐且效率低下的工作。这时&#xff0c;爬虫技术的出现&#xff0c;为我们提供了一种高效的方式去获取…

el-form的表单验证,有值却报红!

正确的写法是 el-form中的form用 :model绑定&#xff0c;各个输入项用 v-model绑定值 原因 显然&#xff0c;区别就是 v-model 和 :model的区别 V-mode v-model是一个语法糖&#xff0c;用于 “表单元素上” 实现双向数据绑定。包括数据绑定和事件监听。 <input v-model&q…

Docker技术--Docker简介和架构

1.Docker简介 (1).引入 我们之前学习了EXSI,对于虚拟化技术有所了解,但是我们发现类似于EXSI这样比较传统的虚拟化技术是存在着一定的缺陷:所占用的资源比较多,简单的说,就是你需要给每一个用户提供一个操作平台,这一个操作平台就会占用你的资源。这样资源的浪费是比较多的…

Ansys Zemax | 大功率激光系统的 STOP 分析(五)

大功率激光器广泛用于各种领域当中&#xff0c;例如激光切割、焊接、钻孔等应用中。由于镜头材料的体吸收或表面膜层带来的吸收效应&#xff0c;将导致在光学系统中由于激光能量吸收所产生的影响也显而易见&#xff0c;大功率激光器系统带来的激光能量加热会降低此类光学系统的…

问道管理:逾4600股飘红!汽车板块爆了,多股冲击涨停!

A股商场今天上午全体低开&#xff0c;但其后逐级上行&#xff0c;创业板指数上午收盘大涨超越3%&#xff0c;北向资金也完成净买入38亿元。 别的&#xff0c;A股商场半年报成绩发表如火如荼进行中&#xff0c;多家公司发表半年报后股价应声大涨&#xff0c;部分公司股价冲击涨停…

Docker从认识到实践再到底层原理(一)|技术架构

前言 那么这里博主先安利一些干货满满的专栏了&#xff01; 首先是博主的高质量博客的汇总&#xff0c;这个专栏里面的博客&#xff0c;都是博主最最用心写的一部分&#xff0c;干货满满&#xff0c;希望对大家有帮助。 高质量博客汇总 然后就是博主最近最花时间的一个专栏…

数字孪生:重塑政府决策与公共服务

在之前的文章中为大家分享了数字孪生在很多行业的应用场景&#xff0c;本文和大家一起探讨一下数字孪生在政务管理方面能有哪些应用&#xff0c;以及其对公共服务提供的积极影响。 1&#xff09;城市规划方面 数字孪生技术可用于模拟城市的发展和规划。政府可以建立城市的虚拟…

Promise构造函数,属性以及方法应用

&#xff08;一&#xff09;promise构造函数 <script type"text/javascript">const myPromise new Promise((resolve, reject) > {setTimeout(() > {resolve(foo)},300)})myPromise.then((value) > {console.log(value,value)}).catch((err) > {…

2023蓝帽杯初赛ctf部分题目

Web LovePHP 打开网站环境&#xff0c;发现显示出源码 来可以看到php版本是7.4.33 简单分析了下&#xff0c;主要是道反序列化的题其中发现get传入的参数里有_号是非法字符&#xff0c;如果直接传值传入my_secret.flag&#xff0c;会被php处理掉 绕过 _ 的方法 对于__可以…