Task example works as designed?

I tried this example from the friendly manual:

struct Work: Sendable {}

actor Worker {
  var work: Task<Void, Never>?
  var result: Work?

  deinit {
    print("deinit actor")
  }

  func start() {
    work = Task {
      print("start task work")
      try? await Task.sleep(for: .seconds(1))
      self.result = Work()
      print("completed task work")
    }
  }
} 

await Worker().start()

If I compile and run it “as is” the line print("completed task work") is never reached. If I add await try Task.sleep(for: .seconds(1)) it prints the message. The message from the deinitializer is not printed either. Should it be like this? Or do I miss something essential?

1 Like

With a tiny adjustment to the cited example, this works:

@main
enum Temp {
    static func main () async {
        await Worker().start()
    }
}

struct Work: Sendable {}

actor Worker {
  var work: Task <Void, Never>?
  var result: Work?

  deinit {
    print ("deinit actor")
  }

  func start() async {
    work = Task {
      print ("start task work")
      try? await Task.sleep (for: .seconds(1))
      self.result = Work()
      print ("completed task work")
    }
     
    // wait for the task to complete 
    _ = await work?.value
  }
}

Output

start task work
completed task work
deinit actor
Program ended with exit code: 0
1 Like

Ah, OK I see. Nice, thanks! One more thing: How do I have to do this (asynchronous context) without an explicit main? With the added code it doesn’t compile without …main() async…Or is that really the only way in this case?

Use a Task to create an asynchronous context from a synchronous one.

I am not an expert, but something like these would work.

T 1
// Use a global var to synchronise

struct Work: Sendable {}

actor Worker {
  var work: Task<Void, Never>?
  var result: Work?

  deinit {
    print("deinit actor")
  }

  func start() async {
    work = Task {
      print("start task work")
      try? await Task.sleep(for: .seconds(1))
      self.result = Work()
      print("completed task work")
    }
      
    _ = await work?.value
  }
}

// for synchronisation
nonisolated(unsafe) var finished = false

// gateway-to-async-world task
Task {
    print ("task starting...")
    await Worker().start()
    print ("task finished...")
    finished = true
}

// wait for gateway-to-async-world task to signal finished.
import Foundation
while !finished {
    Thread.sleep (forTimeInterval: 1.0)
}
print ("finished")

Output

task starting...
start task work
completed task work
deinit actor
task finished...
finished
Program ended with exit code: 0
T 2
// Use a global semaphore to synchronise

struct Work: Sendable {}

actor Worker {
  var work: Task<Void, Never>?
  var result: Work?

  deinit {
    print("deinit actor")
  }

  func start() async {
    work = Task {
      print("start task work")
      try? await Task.sleep(for: .seconds(1))
      self.result = Work()
      print("completed task work")
    }
      
    _ = await work?.value
  }
}

// for synchronisation
import Dispatch
let finished = DispatchSemaphore (value:0)

Task {
    print ("task starting...")
    await Worker().start()
    print ("task finished...")
    finished.signal()
}

let u = finished.wait (timeout: .now() + 30)
if case .timedOut = u {
    print ("took too long - bailing out")
}
else {
    print ("finished")
}

Output

task starting...
start task work
completed task work
deinit actor
task finished...
finished
Program ended with exit code: 0

Pretty sure, they will raise some eyebrows, though. :slight_smile:
Let's wait for an expert to chime in.

@robert.ryan, do you have some advice on this?

1 Like

@ibex10 – I’m not following you. What is eyebrow raising here? I’m not sure what you expected.

Obviously, it is inadvisable to use either semaphores or the unsafe nonisolated, but I assume that is not the question here.

That having been said, I am unclear what the question is.

1 Like

Sorry, I should have made it clear. @karlgoethebier is wondering why some print messages are missing.

And I tried to show how they can be printed without losing them, but I used some controversial techniques.

1 Like

@ibex10 – Ah, I see.

The problem here is simply that the OP’s start method launches a task and immediately returns, so there is a simple race between the task that start launched and the termination of the program. As you noted, one would simply await that task.

actor Worker {
    var work: Task<Void, Never>?
    var result: Work?

    deinit {
        print("deinit actor")
    }

    func start() async {
        let task = Task {
            print("start task work")
            try? await Task.sleep(for: .seconds(1))
            self.result = Work()
            print("completed task work")
        }

        await withTaskCancellationHandler {
            await task.value
        } onCancel: {
            task.cancel()
        }
    }
}

await Worker().start()

Perhaps needless to say, if you use unstructured concurrency, you need to implement your own cancellation handling, like above.

Or, better, abandon this unnecessary unstructured concurrency entirely:

struct Work: Sendable {}

actor Worker {
    var work: Task<Void, Never>?
    var result: Work?

    deinit {
        print("deinit actor")
    }

    func start() async {
        print("start task work")
        try? await Task.sleep(for: .seconds(1))
        self.result = Work()
        print("completed task work")
    }
}

await Worker().start()

Now, @karlgoethebier said:

When in a source file called main.swift one doesn’t need the …main() async… stuff. But when in a different source file, you need it:

@main
struct MyApp {
    static func main() async throws {
        await Worker().start()
    }
}

Maybe I'm not understanding the question…

4 Likes

Thank you! :+1:

1 Like

You seem to be under the impression that "the OP" is the poster of this thread.

But the OP is whoever wrote the documentation. The documentation not producing the results it says it does, is a bug.

2 Likes

The documentation clearly denotes that this is a "snippet of code", not a complete program. It explains the Task type, but that does not mean it has to be written without any presupposed understanding of concurrency in general.

Right at the end of the first paragraph it says (emphasis mine)

However, if you discard the reference to a task, you give up the ability to wait for that task’s result or cancel the task.

And if one starts a program, then starts the task, but then immediately lets the program finish, this is exactly what happens.
Of course I agree it's difficult to bring every aspect into each part of any documentation, but considering this is explaining how Task specifically works, imo it's fine that provided code is not necessarily a self-contained program.

5 Likes

The "self-contained program" involves one more line.

await Worker().start()
try await Task.sleep(for: .seconds(3))

Somebody came to the Swift forum to ask about this, and then we all wasted time with it. That's what a documentation bug does.

1 Like

It might be a bit strong to call it a “bug” in the documentation.

The whole point of this section is to talk about the “Task closure lifetime”. So, sure, if you allow the app to terminate before the Task closure reaches the end of its lifetime (e.g., in a command line app or a Playground without needsIndefiniteExecution), any discussions about Task lifetimes are obviously moot.

But if you put this in an app in an AppKit/UIKit/SwiftUI app (or playground with indefinite execution; or add a Task.sleep; etc.), it works just as outlined in the documentation. I might even argue that anything talking about the lifespan of an asynchronous task closure naturally implies that you wouldn’t do this in an app that is allowed to terminate before the closure finishes.

So, I would not be inclined to call it a “bug”. At worst, perhaps it is a little unclear.

3 Likes

Hi, it's me the real OP. It's going a long way. Hard to decide who to reply to first and I can't update my initial post. So I'm answering myself in the hope that everyone feels addressed.

It's like this: The issue of concurrency is still relatively new and not entirely trivial or intuitive to understand.

Unfortunately, many attempts to explain this are somewhat vague. In my opinion, this is actually the case in all languages that have such features.

That's why I think it's a bit unfortunate when examples in official documentation are not immediately executable. In books, there is the famous "...left as an exercise for the reader." Even that is sometimes borderline.

Then there's the following phenomenon: out there in the wild, there are countless examples of Swift code that don't run - either because they are outdated or simply wrong. It already looks like everyone has a Swift blog.

All this together makes life more difficult for interested learners.

Anyway, I am of the opinion that examples in official documentation aka "The Book" etc. should be executable. Just as I am of the opinion that there are far too few examples. But that's another topic. And problematic because it involves a lot of work.

Thanks to all for their kind help.

4 Likes