并发、可重入性与信号安全

并发、可重入性与信号安全

这篇博客主要记录的是关于可重入性的相关定义,以及关于并发安全的思考。

可重入性

在不同语言中,由于语言标准以及运行期环境规定的不同,可重入性的具体定义可能有所不同。这里聊的是C++语言中的可重入性。

所谓可重入性(reetrant),指的是同时具备并发安全中断安全的特征,这是目前为止我对可重入性的认识,也是这篇博客在写下时给可重入性下的定义。

这个认知可能并不准确,因为在wiki上的定义是这样的。

若一个程序或子程序可以「在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错」,则称其为可重入(reentrant或re-entrant)的。即当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合設計時預期的结果。与多线程并发执行的线程安全不同,可重入强调对单个线程执行时重新进入同一个子程序仍然是安全的。

但是在很多中文博客里,聊到可重入性的时候往往也会把并发安全混为一谈。实际上来说的话......一个可重入的函数,常常也是并发安全的。

那么先从并发安全讲起吧。

并发安全性和可重入性

所谓并发安全已经是老生常谈了。

以一段非常简单的代码为例,我们打算初始化一个对象,这个对象被两个线程共享。

void initialize(Something** someshit) {
  if(!*someshit) {
    *someshit = createSomeShit();
  }
}

显而易见,如果线程在执行到特定环节时发生了切换

void initialize(Something** someshit) {
  if(!*someshit) {
    // <-------- 线程切换
    // 线程2() {
    // initialize(something);
    // }
    // 线程切换 --------->
    *someshit = createSomeShit();
  }
}

那么 createSomeShit这段代码就会被执行两次。

显然这和我们预期的行为不符。

这里要聊的不是并发,而是......可重入性。所以我们再看看这个函数能否被重入。

按照 wiki 提供的定义,函数可重入指的是

在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错。

符合吗?不。为什么?因为同样在那个线程切换的位置上中断,然后再另一段代码里再次执行这个函数,也会触发同样的问题,导致createSomeShit被执行两次。

void initialize(Something** someshit) {
  if(!*someshit) {
    // <-------- 被中断
    // 中断处理函数() {
    //   initialize(something);
    // }
    // 中断结束 --------
    *someshit = createSomeShit();
  }
}

可以看出,那些线程不安全的代码,都是不可重入的。

那么,线程安全的代码,就一定是可重入的吗?

中断安全性,或者叫信号安全性

中断这个东西对其他编程语言的用户来说可能会少见一些,在C/C++语言里,中断并不是什么新鲜话题。

在C标准库中,规定了一系列的信号和信号处理方法。关于信号的定义可以参考这个

当进程接收到信号的时候,当前正在执行的代码就会被中断——注意了,这回,锁救不了你。

在C/C++中,中断处理是由一个函数进行。在函数里可能会调用到中断时正在执行的函数。那么问题来了——一个线程安全的函数,是中断安全的函数吗?

void initialize(Something** someshit, std::mutex& realshit) {
  std::lock_guard<std::mutex>(realshit);
  if(!*someshit) {
    *someshit = createSomeShit();
  }
}

看上去岁月静好~一切线程切换的问题,都被那句std::lock_guard<std::mutex>(realshit)给挡在了墙的另一边。

但是......

void initialize(Something** someshit, std::mutex& realshit) {
  std::lock_guard<std::mutex>(realshit);
  if(!*someshit) {
    // <----- 调皮的用户按下了 Ctrl-C
    // 中断处理函数() {
    //   initialize(someshit, realshit);
    //   // inside initialize {
    //   //   std::lock_guard<std::mutex>(realshit); // DEAD LOCK
    //   // }
    // }
    *someshit = createSomeShit();
  }
}

看这里~

std::lock_guard<std::mutex>(realshit);
// 进入信号处理
std::lock_guard<std::mutex>(realshit);

好了,GG。死锁在这个时候发生了。

经验丰富的大佬可能注意到了,咱还可以用std::recursive_mutex啊!

这里就要提到一个很遗憾的问题了:C/C++的语言标准给了哪些保证。

C对信号处理函数的定义很粗暴,除了abort_Exitquick_exitsignalstdatomic.h的免锁原子函数atomic_is_lock_free与任何类型的原子参数这些函数以外,任何标准库函数的调用,行为都是未定义的。

C++对信号处理函数的定义则更加复杂,限制比之C更加严格。毕竟标准库要庞大得多......也不是不能理解。

标准中有个一个地方的描述很微妙:......免锁的

换言之,谁又保证了信号处理函数必然和你希望的那个线程是同一个线程呢?

std::recursive_mutex的实现依赖于平台提供的系统API,反正我没有找到语言标准中相关的规定要求信号处理函数必须和main函数在同一个线程,所以我认为这是平台相关的问题:这样的代码是不可移植的

按照设计模式原则,我们是面向接口——也就是标准文档编程,而不是面对实现——Visual C++、GCC、MinGW或者哪个中东土豪在未来某天突发奇想送我一台MIPS的超算的话。

到业务层面的话会更灵活一些——反正我只在某环境下跑,等公司什么时候全面换平台了,咱再能改则改,改不了就跑路。

递归函数和可重入

递归和重入有一定的相似性,但又有所不同。

一个递归函数,直觉上来讲,好像应该是可重入的:因为它要调用自己。

那么......事实上呢?

写个比较骚的递归删除链表节点的例子。

void removeNode(Node* node, int length) {
  if(length > 0) {
    Node* tmp = node.prev;
    node.next.prev = tmp;
    // <------ 出现了!中断兽!
    // 不用看了,Node之间的联结已经被破坏了
    // 离开了!中断兽!-------->
    tmp.next = node.next;
    freeNode(node);
    removeNode(tmp.next, length-1);
  }
}

轻易地否定了递归函数=可重入函数的直觉想法。

深究下去,又到了线程安全——然后是死锁——然后提出了std::recursive_mutex或者其他类似的操作——最后走到平台相关的API和保证——失去可移植性。

为什么我一直在提可移植性?

emmmm,大概是装逼如风,常伴吾身吧。

标准库好烦人啊

C/C++语言的标准库是出了名的——但不是好的方面,而是他们总在修修补补又一年。

C标准库还好说——毕竟语言本身没啥特性,全靠各种平台提供API撑着。标准库改来改去也只是割个双眼皮的程度。

C++要更骚气一些,每隔几年就整个容,简直不给人活路。

就中断安全来说,虽然不知道内部怎么实现的,但是......printf这样的函数在信号处理函数里调用的话,也算是未定义行为。

认输吧,你是斗不过标准的。该依赖平台行为的时候,就去依赖平台行为吧。

文档引用

懒得找原文,直接看cppreference对signal的说法就好。有兴趣的话可以找又臭又长的WG14 - N1570 - C11,还有WG21 - N4659 - C++17这两本标准文档。

尾声

于是这会儿就到了其他各种语言的用户惯例吐槽的时候:

...大佬是公司里唯一用C++写代码的人。他对人说话,总是满口“目标平台”、“标准”、“可移植性”之类的话,叫人半懂不懂的。因为他总是说“C++天下第一!”,别人便从他说的那些半懂不懂的话里,替他取下个绰号,叫C++大神。

C++大神一到公司里,程序员们便看着他笑,有的叫道:“C++大神,你的代码又编译出错了!”

他不回答,对前台说:“倒上特浓的咖啡,今天也要加班到夜里。”便拿出员工卡。程序员们又高声叫嚷道:“你一定又用上新标准了吧?”

C++大神睁大眼睛说,“你怎么凭空污人清白!”

“什么清白?我前天亲眼看见你的代码编译报了错,整整十几MB的日志!”

C++大神便涨红了脸,额上的青筋条条绽出,争辩道,“编译器报错怎么能叫错......C++......编译器不支持,那能算错么?”

接连便是难懂的话,什么“CONCEPT还不加入标准”、“未定义行为就该是编译错误”、“SFINAE就是给编译器开洞”、“boost大法好,天灭std::experimental”,引得众人都哄笑起来:店内外充满了快活的空气。

转载于:https://my.oschina.net/u/3888259/blog/1834856

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值