Skip to content

Instantly share code, notes, and snippets.

@fbartho
Last active April 5, 2024 22:38
Show Gist options
  • Save fbartho/e8ac00e0827f3c7976da7bfce0fe51ed to your computer and use it in GitHub Desktop.
Save fbartho/e8ac00e0827f3c7976da7bfce0fe51ed to your computer and use it in GitHub Desktop.
Swift Structured Concurrency - Actors and Rescheduling
actor Scheduler {
var tasks: [@Sendable () async -> Void]
func schedule(task: @escaping @Sendable () async -> Void) {
tasks.append(task)
}
func executeAll() async {
while let t = tasks.popLast() {
// Tasks may schedule more tasks when they execute
await t()
// How do I defer this loop until any pending schedule operations hit my actor?
}
}
}
actor Scheduler {
var tasks: [@Sendable () async -> Void]
func schedule(task: @escaping @Sendable () async -> Void) {
tasks.append(task)
}
func executeAll() async {
while let t = tasks.popLast() {
// Tasks may schedule more tasks when they execute
await t()
// Slot a task on the main actor, so any pending stuff has a
// chance to get scheduled before the loop resumes.
let flush = @MainActor () async -> { await self.ready() }
await flush()
// Is this await blocking the Scheduler actor?
// Does resuming the loop "cut the line" or is this resumption
// like enqueuing at the back of the actor's mailbox?
}
}
private func ready() async {
// Does nothing
}
}
//
// main.swift
// TestCLI
//
// Created by Frederic Barthelemy on 4/5/24.
//
import Foundation
print("Hello, World!")
actor Scheduler {
var tasks: [@Sendable () async -> Void] = []
func schedule(task: @escaping @Sendable () async -> Void) {
print("Scheduling!")
tasks.append(task)
}
func executeAll() async {
print("Beginning Execute")
// do {
// try await Task.sleep(nanoseconds: 1_000_000_000)
// } catch {}
// print("Post Sleep")
while !tasks.isEmpty {
print("Loop: Start")
var batch = Array(tasks.reversed())
tasks = []
while let t = batch.popLast() {
print("Loop: Entry")
// Tasks may schedule more tasks when they execute
await t()
}
print("Loop: Post-batch")
// Slot a task on the main actor, so any pending stuff has a
// chance to get scheduled before the loop resumes.
await flush()
print("Loop: Post-flush")
}
print("Done with execute all")
}
@MainActor private func flush() async {
print("Pre-flush")
await self.ready()
print("Post flush")
}
private func ready() async {
// Does nothing
print("Ready")
}
}
let s = Scheduler()
print("1.s")
await s.schedule {
print("1.1")
await s.schedule {
print("1.2")
}
print("1.1e")
}
print("1.e")
await s.schedule {
print("2.1")
await s.schedule {
print("2.2")
}
print("2.1e")
}
await s.schedule {
print("3.1")
await s.schedule {
print("3.2")
}
print("3.1e")
}
let t = Task {
print("enqueuing execute")
await s.executeAll()
await s.schedule {
print("post-all.1")
await s.schedule {
print("post-all.2")
}
print("post-all.1e")
}
}
await s.schedule {
print("4.1")
await s.schedule {
print("4.2")
}
print("4.1e")
}
await s.schedule {
print("5.1")
await s.schedule {
print("5.2")
}
print("5.1e")
}
print("wait for the executeAll task")
await t
print("done waiting for executeAll")
print("spinloop")
var count = 0
while count < 1000 {
print("spin \(count)")
do {
try await Task.sleep(nanoseconds: 1_000_000_000)
} catch {}
count += 1
}
print("Done")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment