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

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

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

可重入性

在不一样语言中,因为语言标准以及运行期环境规定的不一样,可重入性的具体定义可能有所不一样。这里聊的是C++语言中的可重入性。程序员

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

这个认知可能并不许确,由于在wiki上的定义是这样的。设计模式

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

可是在不少中文博客里,聊到可重入性的时候每每也会把并发安全混为一谈。实际上来讲的话......一个可重入的函数,经常也是并发安全的。多线程

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

并发安全性和可重入性

所谓并发安全已是老生常谈了。编程语言

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

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”,引得众人都哄笑起来:店内外充满了快活的空气。