Thread|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 Dispatchfunc 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 :? (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 ...]
==================
【Thread|Thread Sanitizer for Swift on Linux】可以看下 summary 这一行,它表明:
- 被检测出来的 bug 类型,此处是 "Swift access race" 。
- 源码位置,main.swift:41,表示
results.append(r)
。 - 闭合函数,它是编译器生成的闭包。
在本示例中,两者访问都是通过同一条源码语句实现的。然而,其实不总是这样的。当在大型应用中调试一些微小的交互时,了解这些 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,开发人员可以使用该工具提高多线程环境下安全性和高效性。
推荐阅读
- whlie循环和for循环的应用
- Java内存泄漏分析系列之二(jstack生成的Thread|Java内存泄漏分析系列之二:jstack生成的Thread Dump日志结构解析)
- ffmpeg源码分析01(结构体)
- 【WORKFOR】最真的自己
- 2019-08-16day20总结
- R|R for data Science(六)(readr 进行数据导入)
- performSelectorOnMainThread:withObject:waitUntilDone:参数设置为NO或YES的区别
- JavaScript|JavaScript — 初识数组、数组字面量和方法、forEach、数组的遍历
- Swift7|Swift7 - 循环、函数
- 65|65 - Tips for File Handling