0

I'd like to test/display behavior in Swift Playgrounds related to a function that mutates values after a delay. For simplicity, let's just say it mutates a string. I know I can delay the execution of updating the value via DispatchQueue.main.asyncAfter and that I can sleep the current thread using usleep or sleep.

However, since the playground is seemingly running in a synchronous thread, I'm not able to see the changes after sleeping.

Here's an example of what I would like to do:

var string = "original"

let delayS: TimeInterval = 0.100
let delayUS: useconds_t = useconds_t(delayS * 1_000_000)

func delayedUpdate(_ value: String) {
  DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + delayS) {
    string = value
  }
}

delayedUpdate("test2")
assert(string == "original")
usleep(delayUS)
print(string) // ❌ Prints "original"
assert(string == "test2") // ❌ Assertion failure. string is "original" here

delayedUpdate("test3")
assert(string == "test2") // ❌ Assertion failure. string is "original" here
usleep(delayUS)
print(string) // ❌ Prints "original"
assert(string == "test3") // ❌ Assertion failure. string is "original" here

delayedUpdate("test4")
assert(string == "test3") // ❌ Assertion failure. string is "original" here
usleep(delayUS)
print(string) // ❌ Prints "original"
assert(string == "test4") // ❌ Assertion failure. string is "original" here

Notice all the failed assertions since anything at the top level doesn't see the changes to string. This seems like a synchronous vs. asynchronous thread issue.

I know I can fix it by replacing usleep with more asyncAfter:

delayedUpdate("test2")
assert(string == "original")
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + delayS) {
  print(string)
  assert(string == "test2")

  delayedUpdate("test3")
  assert(string == "test2")
  DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + delayS) {
    print(string)
    assert(string == "test3")

    delayedUpdate("test4")
    assert(string == "test3")
    DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + delayS) {
      print(string)
      assert(string == "test4")
    }
  }
}

However, this leads to a pyramid of doom of indented code each time the app is delayed. This is not too bad with 3 levels, but if I have a large playground, this will become really hard to follow.

Is there a way to use something closer to the first linear programming style that respects updates updated after delays?


Another potential solution is to wrap each reference to string in a asyncAfter:

delayedUpdate("test2")
assert(string == "original")
usleep(delayUS)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.001) { print(string) }
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.001) { assert(string == "test2") }

delayedUpdate("test3")
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.001) { assert(string == "test2") }
usleep(delayUS)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.001) { print(string) }
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.001) { assert(string == "test3") }

delayedUpdate("test4")
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.001) { assert(string == "test3") }
usleep(delayUS)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.001) { print(string) }
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.001) { assert(string == "test4") }

However this is not preferred either since it is pretty messy as well, and probably error prone if one execution relies on the previous value of string to do its function, for example. It also needs a 0.001 or similar correction to ensure there is no race condition.


How do I use a linear programming style (e.g. with sleep) in a Swift playground, but have values that are updated during the sleep be reflected correctly by the lines after sleep?

Senseful
  • 86,719
  • 67
  • 308
  • 465

2 Answers2

2

You're creating a race condition. Forget the playground; just consider the following code:

    print("start")
    DispatchQueue.main.asyncAfter(deadline:.now() + 1) {
        print("delayed")
    }
    sleep(2)
    print("done")

We delay for 1 second and print "delayed", and we sleep for 2 seconds and print "done". Which do you think will appear first, "delayed" or "done"? If you think "delayed" will appear first, you don't understand what sleep does. It blocks the main thread. The delay cannot re-enter the main thread until after the blockage is gone.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Ah excellent point. Thanks for the example that makes it obvious what's going on too. I guess I was trying to run asynchronous code at the top level of the playground, which is not supported by Swift. – Senseful Jul 11 '20 at 00:54
  • Certainly you can "run asynchronous code at the top level of the playground". My point is that that's not what you asked. You showed a self-blocking race condition. If you didn't have that, you could certainly use `dispatchAfter` in a playground. – matt Jul 11 '20 at 01:37
1

matt's simplified example makes the real problem with this code very clear. It's not that the main queue was not seeing updates to the values, but rather that the order of the code was being executed in a different way than I was expecting.

You can think of the order of execution as:

  1. All lines at the top level of a Swift Playground (main queue)
  2. Anything else scheduled on the main queue

That explains why the code is executed in this order:

print("start") // 1
DispatchQueue.main.asyncAfter(deadline:.now() + 1) {
    print("delayed") // 3
}
sleep(2)
print("done") // 2
// All async code on the main thread will execute after this line

Notice how it executes it in 132 order instead of the desired 123 order. The reason is that the top level code in a Playground is running on the main thread, and that .main was supplied to DispatchQueue.

If you try it with a semaphore, you can clearly see the problem as well:

print("start") // 1
let semaphore = DispatchSemaphore(value: 0)
DispatchQueue.main.asyncAfter(deadline:.now() + 1) {
  print("delayed") // never executed
  semaphore.signal()
}
semaphore.wait() // This causes a deadlock
print("done") // never executed

The line semaphore.wait() causes a deadlock since the semaphore.signal() can only be called after the done is printed, but it can't advance to that line because of the wait.

One way to get the code running in the desired order is to move the asynchronous code to a different thread (e.g. global()):

print("start") // 1
DispatchQueue.global().asyncAfter(deadline:.now() + 1) {
    print("delayed") // 2
}
sleep(2)
print("done") // 3

Simply changing to use a global queue instead of the main queue (and adding more time buffer) makes the original code function as desired:

var string = "original"

let delayS: TimeInterval = 0.100
let sleepDelayUS: useconds_t = useconds_t(delayS * 1_000_000)
let sleepPaddingUS = useconds_t(100_000)

func delayedUpdate(_ value: String) {
  DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + delayS) {
    string = value
  }
}

delayedUpdate("test2")
assert(string == "original")
usleep(sleepDelayUS + sleepPaddingUS)
print(string) // ❌ Prints "original"
assert(string == "test2") // ❌ Assertion failure. string is "original" here

delayedUpdate("test3")
assert(string == "test2") // ❌ Assertion failure. string is "original" here
usleep(sleepDelayUS + sleepPaddingUS)
print(string) // ❌ Prints "original"
assert(string == "test3") // ❌ Assertion failure. string is "original" here

delayedUpdate("test4")
assert(string == "test3") // ❌ Assertion failure. string is "original" here
usleep(sleepDelayUS + sleepPaddingUS)
print(string) // ❌ Prints "original"
assert(string == "test4") // ❌ Assertion failure. string is "original" here

Another option for testing code with delays in Playground is using XCTWaiter, since XCTest works in Playgrounds. If you want an even better way to test, you could use a custom dispatch queue meant for testing rather than literally waiting for time to pass. This episode on PointFree explains one way of doing that.

Senseful
  • 86,719
  • 67
  • 308
  • 465