1

ThreadSanitizer detects a data race in the following Swift program run on macOS:

import Dispatch


class Foo<T> {
    var value: T?
    let queue = DispatchQueue(label: "Foo syncQueue")
    init(){}
    func complete(value: T) {
        queue.sync {
            self.value = value
        }
    }

    static func completeAfter(_ delay: Double, value: T) -> Foo<T> {
        let returnedFoo = Foo<T>()
        let queue = DispatchQueue(label: "timerEventHandler")
        let timer = DispatchSource.makeTimerSource(queue: queue)
        timer.setEventHandler {
            returnedFoo.complete(value: value)
            timer.cancel()
        }
        timer.scheduleOneshot(deadline: .now() + delay)
        timer.resume()
        return returnedFoo
    }
}



func testCompleteAfter() {

    let foo = Foo<Int>.completeAfter(0.1, value: 1)
    sleep(10)
}

testCompleteAfter()

When running on iOS Simulator, ThreadSanitizer does not detect a race.

ThreadSanitizer output:

WARNING: ThreadSanitizer: data race (pid=71596)
  Read of size 8 at 0x7d0c0000eb48 by thread T2:
    #0 block_destroy_helper.5 main.swift (DispatchTimerSourceDataRace+0x0001000040fb)
    #1 _Block_release <null>:38 (libsystem_blocks.dylib+0x000000000951)

  Previous write of size 8 at 0x7d0c0000eb48 by main thread:
    #0 block_copy_helper.4 main.swift (DispatchTimerSourceDataRace+0x0001000040b0)
    #1 _Block_copy <null>:38 (libsystem_blocks.dylib+0x0000000008b2)
    #2 testCompleteAfter() -> () main.swift:40 (DispatchTimerSourceDataRace+0x000100003981)
    #3 main main.swift:44 (DispatchTimerSourceDataRace+0x000100002250)

  Location is heap block of size 48 at 0x7d0c0000eb20 allocated by main thread:
    #0 malloc <null>:144 (libclang_rt.tsan_osx_dynamic.dylib+0x00000004188a)
    #1 _Block_copy <null>:38 (libsystem_blocks.dylib+0x000000000873)
    #2 testCompleteAfter() -> () main.swift:40 (DispatchTimerSourceDataRace+0x000100003981)
    #3 main main.swift:44 (DispatchTimerSourceDataRace+0x000100002250)

  Thread T2 (tid=3107318, running) created by thread T-1
    [failed to restore the stack]

SUMMARY: ThreadSanitizer: data race main.swift in block_destroy_helper.5

Is there anything suspicious with the code?

CouchDeveloper
  • 18,174
  • 3
  • 45
  • 67
  • You're creating things in one queue and deallocating them in another (because of the behavior of `DispatchSourceTimer` to not retain themselves like [`Timer`](https://developer.apple.com/reference/foundation/timer), so it's only getting retained because of your strong reference in the closure). Sure, you can dispatch the `timer.cancel()` back to the main queue, but that doesn't seem like the generalized solution. But I'm not sure how to advise fixing this because I'm not sure what you're trying to do here, why you're not using `asyncAfter`, why this method is `static`, etc. – Rob Jan 26 '17 at 21:37
  • Thanks @Rob. That `timer` will be imported intentionally into the event handler, since a client won't get a reference. The timer will be cancelled by other means (not shown). The purpose of importing the timer reference is to keep the timer alive up until the handler executes. So, if this issue stems from the fact that deinitializing a timer accesses variables which have been previously written on another thread without proper synchronisation, then this is a bummer. I already tried to enclose all accesses to the timer into the queue `queue`, but the issue remains. – CouchDeveloper Jan 27 '17 at 16:04
  • Note: the object `foo` will also be allocated on Thread A, then dispatched to thread B, modified on B (no data race, since dispatch inserts proper memory barriers), and then deallocated in A. The deallocation may potentially have data races as well, unless `deinit` or the ref-count mechanism inserts appropriate memory barriers (which we don't know). – CouchDeveloper Jan 27 '17 at 16:14

1 Answers1

1

The comment from @Rob made me think again about the issue. I came up with the following modification for static func completeAfter - which ThreadSanitizer is happy with *):

static func completeAfter(_ delay: Double, value: T) -> Foo<T> {
    let returnedFoo = Foo<T>()
    let queue = DispatchQueue(label: "timerEventHandler")
    queue.async {
        let timer = DispatchSource.makeTimerSource(queue: queue)
        timer.setEventHandler {
            returnedFoo.complete(value: value)
            timer.cancel()
        }
        timer.scheduleOneshot(deadline: .now() + delay)
        timer.resume()
    }
    return returnedFoo
}

This change ensures that all accesses to timer will be executed in queue queue, which tries to synchronise the timer that way. Even though, this same solution in my "real" code didn't work with this solution, it's probably due to other external factors.


*) We should never think, our code has no races, just because ThreadSanitizer doesn't detect one. There may be external factors which just happen to "erase" a potential data race (for example, dispatch lib happens to execute two blocks with a conflicting access on the same thread - and no data race can happen)

CouchDeveloper
  • 18,174
  • 3
  • 45
  • 67