Как мне заснуть в Swift Playgrounds и получить последние значения asyn c? - PullRequest
0 голосов
/ 10 июля 2020

Я хотел бы протестировать / отобразить поведение в Swift Playgrounds, связанное с функцией, которая изменяет значения после задержки. Для простоты скажем, что он мутирует строку. Я знаю, что могу отложить выполнение обновления значения через DispatchQueue.main.asyncAfter и что я могу засыпать текущий поток, используя usleep или sleep.

Однако, поскольку игровая площадка, по-видимому, работает в синхронном потоке , Я не вижу изменений после сна.

Вот пример того, что я хотел бы сделать:

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

Обратите внимание на все неудавшиеся утверждения, поскольку ничего на верхнем уровне не видит изменений в string. Это похоже на проблему синхронного или асинхронного потока.

Я знаю, что могу исправить это, заменив usleep на большее 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")
    }
  }
}

Однако это приводит к пирамиде гибели кода с отступом каждый раз, когда приложение откладывается. Это не так уж плохо с 3 уровнями, но если у меня будет большая игровая площадка, за этим будет действительно трудно следить.

Есть ли способ использовать что-то более близкое к первому стилю линейного программирования, который учитывает обновления, обновляемые после задержки?

Другое возможное решение - заключить каждую ссылку на string в 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") }

Однако это тоже не является предпочтительным, поскольку это также довольно беспорядочно и, вероятно, подвержено ошибкам, если они Для выполнения своей функции, например, выполнение полагается на предыдущее значение string. Также требуется 0.001 или аналогичная коррекция, чтобы убедиться в отсутствии состояния гонки.

Как использовать стиль линейного программирования (например, с sleep) на игровой площадке Swift, но чтобы значения, которые обновляются во время сна, правильно отображались строками после sleep?

Ответы [ 2 ]

1 голос
/ 10 июля 2020

Вы создаете состояние гонки. Забудьте о детской площадке; просто рассмотрите следующий код:

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

Мы задерживаем на 1 секунду и печатаем «отложено», а мы спим на 2 секунды и печатаем «готово». Как вы думаете, что появится первым: «отложено» или «выполнено»? Если вы думаете, что "отложено" появится первым, вы не понимаете, что делает sleep. Он блокирует основной поток. Задержка не может повторно войти в основной поток до тех пор, пока блокировка не исчезнет.

0 голосов
/ 12 июля 2020
Упрощенный пример

Мэтта проясняет настоящую проблему с этим кодом. Дело не в том, что основная очередь не видела обновления значений, а скорее в том, что порядок выполнения кода выполнялся не так, как я ожидал.

Вы можете думать о порядке выполнения как:

  1. Все строки на верхнем уровне Swift Playground (основная очередь)
  2. Все остальное, что запланировано в основной очереди

Это объясняет, почему код выполняется в следующем порядке:

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

Обратите внимание, как он выполняет его в порядке 132 вместо желаемого порядка 123. Причина в том, что код верхнего уровня в Playground выполняется в основном потоке, и что .main был передан в DispatchQueue.

Если вы попробуете его с семафором , вы также можете ясно увидеть проблему:

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

Строка semaphore.wait() вызывает тупик, поскольку semaphore.signal() может быть вызван только после печати done, но не может перейти к эта строка из-за wait.

Один из способов заставить код работать в желаемом порядке - переместить асинхронный код в другой поток (например, global()):

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

Простое изменение на использование очереди global вместо очереди main (и добавление дополнительного буфера времени) делает исходный код желаемым:

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

Другой вариант для тестирования кода с задержками в Playground - использовать XCTWaiter, , так как XCTest работает в Playgrounds . Если вам нужен еще лучший способ тестирования, вы можете использовать настраиваемую очередь отправки, предназначенную для тестирования, а не буквально ждать, пока пройдет время. Этот эпизод на PointFree объясняет, как это сделать.

...