Thread Sanitizer for Swift on Linux



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

    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,开发人员可以使用该工具提高多线程环境下安全性和高效性。


    相关

    更多讯息,可关注微信公众号:SwiftMic

    微信公众号推广Logo_min.png


Log in to reply