Thread Sanitizer for Swift on Linux

Swift国内社区: SwiftMic


本篇为译文,原文可见:链接

Thread Sanitizer 已经成为 Swift 5.1 的一部分(Linux 平台)。可查阅 Swift.org,下载 Swift 5.1 Development snapshot 去尝试。

Swift 语言在单线程环境中保证了 memory safety。然而,多线程代码中的冲突访问(conflicting accesses)导致了数据竞争(data races)。Swift 中的数据竞争会引起意外的行为,甚至会导致内存崩溃(memory corruption),破坏 Swift 的内存安全性。Thread Sanitizer 是一个 Bug 发现工具,用于诊断运行时的数据竞争问题。执行时,它会在编译期间处理代码,并检测数据竞争问题。

数据竞争示例

让我们看一个简单的多线程程序。它通过 DispatchQueue.concurrentPerform 实现了一个高效的并行 for 循环(parallel for-loop)。

import Dispatch

func computePartialResult(chunk: Int) -> Result {
    var result = Result()
    // Computing the result is an expensive operation.
    return result
}

var results = [Result]()

DispatchQueue.concurrentPerform(iterations: 100) { index in
    let r = computePartialResult(chunk: index)
    results.append(r)
}

print("Result count: \(results.count)")

乍一看可能会期望这个程序输出 "Result count: 100" 。然而实际它可能输出 "91"、"94",甚至会 Crash。原因是程序包含了数据竞争:多线程未使用同步方式来改变 results 数组。

上述例子中,我们比较容易就能找到哪部分代码引入了数据竞争。然而,现实世界中应用程序的数据竞争是很难被发现的。它们的表象可能只是偶发的,而且以微妙的方式改变程序行为。最坏情况下,它们破坏内存,并且中断 Swift 的内存保护机制。但 Thread Sanitizer 被证明是一种用来检测 Swift 数据竞争的有效工具。

Using Thread Sanitizer

想要为你的程序加上 Thread Sanitizer,可以使用 -sanitize=thread 编译器符号,并确保以 Debug 模式构建你的程序。Thread Sanitizer 依赖于 debug 信息来描述它发现的问题。

Swift Compiler

通过如下命令可以让 Thread Sanitizer 在 Swift 编译器中被执行:

swiftc -g -sanitize=thread

因为当前 Thread Sanitizer 与未优化过并且包含 debug 信息的代码,一直正常运行。未优化过的代码要么忽略了用于优化的编译器符号,要么使用 -Onone 覆盖了之前已存在的优化等级。

Swift Package Manager

Thread Sanitizer 也可以直接被 Swift Package Manager 使用:

swift build -c debug --sanitize=thread

使用 test target(而不是 build)来执行你的 package 的 tests(开启 Thread Sanitizer)。注意你的 tests 需要在多线程代码中被执行。否则 Thread Sanitizer 将不会发现数据竞争。

示例

让我们编译运行这个简单示例来看看 Thread Sanitizer 是如何报告数据竞争这个问题的。在 Linux 上,Thread Sanitizer 不会输出 unmangled Swift symbol names,你可以使用 swift-demangle 来让结果更清晰:

➤ swiftc main.swift -g -sanitize=thread -o race
➤ ./race 2>&1 | swift-demangle
==================
WARNING: ThreadSanitizer: Swift access race (pid=96)
  Modifying access of Swift variable at 0x7ffef26e65d0 by thread T2:
    #0 closure #1 (Swift.Int) -> () in main main.swift:41 (swift-linux+0xb9921)
    #1 partial apply forwarder for closure #1 (Swift.Int) -> () in main <compiler-generated>:? (swift-linux+0xb9d4c)
       [... stack frames ...]

  Previous modifying access of Swift variable at 0x7ffef26e65d0 by thread T1:
    #0 closure #1 (Swift.Int) -> () in main main.swift:41 (swift-linux+0xb9921)
    #1 partial apply forwarder for closure #1 (Swift.Int) -> () in main race-b3c26c.o:? (swift-linux+0xb9d4c)
       [... stack frames ...]

  Location is stack of main thread.

  Thread T2 (tid=99, running) created by main thread at:
    #0 pthread_create /home/buildnode/jenkins/workspace/oss-swift-5.1-package-linux-ubuntu-16_04/llvm/projects/compiler-rt/lib/tsan/rtl/tsan_interceptors.cc:980 (swift-linux+0x487b5)
       [... stack frames ...]
    #3 static Dispatch.DispatchQueue.concurrentPerform(iterations: Swift.Int, execute: (Swift.Int) -> ()) -> () ??:? (libswiftDispatch.so+0x1d916)
    #4 __libc_start_main ??:? (libc.so.6+0x2082f)

  Thread T1 (tid=98, running) created by main thread at:
    #0 pthread_create /home/buildnode/jenkins/workspace/oss-swift-5.1-package-linux-ubuntu-16_04/llvm/projects/compiler-rt/lib/tsan/rtl/tsan_interceptors.cc:980 (swift-linux+0x487b5)
       [...stack frames ...]
    #3 static Dispatch.DispatchQueue.concurrentPerform(iterations: Swift.Int, execute: (Swift.Int) -> ()) -> () ??:? (libswiftDispatch.so+0x1d916)
    #4 __libc_start_main ??:? (libc.so.6+0x2082f)

SUMMARY: ThreadSanitizer: Swift access race main.swift:41 in closure #1 (Swift.Int) -> () in main
==================
[... more identical warnings ...]
==================

可以看下 summary 这一行,它表明:

  • 被检测出来的 bug 类型,此处是 "Swift access race" 。
  • 源码位置,main.swift:41,表示 results.append(r)
  • 闭合函数,它是编译器生成的闭包。

注意,数据竞争包含至少 2 个线程并行访问同一块内存(非同步方式),其中至少有一方是写操作。Thread Sanitizer 显示线程涉及到了("Modifying access/Previous modifying access … by thread …"),并且提供了 这两个冲突访问的 stack traces。

在本示例中,两者访问都是通过同一条源码语句实现的。然而,其实不总是这样的。当在大型应用中调试一些微小的交互时,了解这些 traces 是很有价值的。这个报告也阐述了 racing threads 是如何被创建的("Thread … created by …")。此例中,它们在 main thread 中通过调用 concurrentPerform 来被创建的。

一旦问题清楚了,下一步就是修复它。如何修复主要取决于特定情况和代码的目的。比如,目的是为了通过并发来防止一个长期运行的任务阻塞用户界面。另一个目的是为了通过利用多核处理器拆分工作量来加速服务。

甚至在这个简单例子中,有许多不同的方案来修复数据竞争的问题。只要环境和性能允许的情况下,比起低级别 synchronization primitives,更倾向于使用 high-level abstractions。在这个例子中,让我们使用 serial queue 来添加 synchronization。

let serialQueue = DispatchQueue(label: "Results Queue")

DispatchQueue.concurrentPerform(iterations: 100) { index in
    let r = computePartialResult(chunk: index)
    serialQueue.sync {
        results.append(r);
    }
}

上述代码通过串行方式调用 results.append 来保证 synchronization ,这将解决数据竞争问题。注意 computePartialResult 仍将并行处理。这意味着部分结果可能是不一样的。

Swift 其中一个主要目标是让编程变得更容易。编写高效的多线程程序是其中一个难点。Swift 在无数据竞争的情况下保证内存安全,同时允许开发人员按需处理复杂事情。有了 Thread Sanitizer,开发人员可以使用该工具提高多线程环境下安全性和高效性。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容