ctietze,
@ctietze@mastodon.social avatar

async/await question:

What are the patterns/conventions to expose async closures in one place, and handle getting back to the main queue in another?

Context:

The Xcode warning here makes perfect sense:

"""
Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
"""

A dummy implementation of an async throws closure modified an view model directly. (It's annotated MainActor.)

mattiem,
@mattiem@mastodon.social avatar

@ctietze are you taking about a situation where MainActor.run will not work?

ctietze,
@ctietze@mastodon.social avatar

@mattiem Maybe more like: who's responsible to call MainActor.run?

@brentsimmons has this very good heuristic that whoever dispatches on a background queue is responsible to dispatch back to main. Don't leak "being executed on the background queue", and always assume you're on main.

async/await doesn't show where I am in the same way. When I expose an async function, chances are it is being called from a Task (not MainActor), so it's all turned on it's head :)

mattiem,
@mattiem@mastodon.social avatar

@ctietze that heuristic does not match how most Apple APIs work. And I think this actually makes sense. The code that has the synchronization requirement should be the one that enforces it. Don’t allow other code to decide what actor/thread you run on.

ctietze,
@ctietze@mastodon.social avatar

@mattiem So the takeaway is: when I'm inside an async function, and need to update something on MainActor, I have to tell it to do so. Even if I don't know the current execution context.

Better safe (and explicit) than sorry, more or less?

mattiem,
@mattiem@mastodon.social avatar

@ctietze one of the neat things about Swift concurrency is the compiler always knows the context. So you’ll always get a warning if you need to change actors. The idea is you cannot ever get this wrong, no heuristic required.

The idea of hiding thread/queue from callers is still possible, just requires sprinkling around some MainActor annotations. It is, however, “pure overhead” from the compiler’s point of view. But might offer convenience in some situations.

vincefried,
@vincefried@mastodon.social avatar

@ctietze @mattiem Yes, this is also what I learned how to think about it.

ctietze,
@ctietze@mastodon.social avatar

@mattiem Hmmmm I may also be interpreting the async keyword wrong.

That tells the caller to await (on whatever thread). It doesn't say anything about if I actually perform asynchronous work in the function itself.

Xcode's lack of warnings got me thinking: again, this is for a closure parameter, async throws. I pass a function reference. The function doesn't need to be async or throwing; that's also acceptable.

The closure signature doesn't affect the content, really :/

mattiem,
@mattiem@mastodon.social avatar

@ctietze you are right: async just means needs an async context, does not have to actually use it. But context requirements like MainActor must be at definition point (where you write the closure signature). This is very different from previous systems, where callers were in charge.

ctietze,
@ctietze@mastodon.social avatar

@mattiem So if I want to do the dummy app's view model update, I should annotate the (potentially) async function that's being called as MainActor accordingly?

mattiem,
@mattiem@mastodon.social avatar

@ctietze you certainly can do it that way, but I think I’d need to see the code in front of me to be certain. It could be overly-restrictive, but that may be a non-issue, or even desirable in practice.

ctietze,
@ctietze@mastodon.social avatar

@mattiem If you want to spend a minute on the sample, here's a rough sketch: https://gist.github.com/DivineDominion/97e294cb2f469900fc7eba964f56afd5

Thinking about this more and more, I wonder if I should not leak async throws via the action. Maybe that ought to be an implementation detail of the app, talking to the server 🤔

mattiem,
@mattiem@mastodon.social avatar

@ctietze ok so in this example, I would remove the async-ness of the closure and push the Task into FooAction.

Does handleFoo mutate internal server state? If so, you also need to think about ordering

ctietze,
@ctietze@mastodon.social avatar

@mattiem So you'd keep the async-ness of the FooAction (better name examples: ReloadTweets, RefreshOrderDetail, ... i.e. something that communicates a potentially failing action with a server round-trip) -- but you'd remove it from the closure and make the async buck stop there?

That's interesting. I haven't thought about the implications of this. It looks like the async-ness is isolated in the action and not spilling out, that could be good 👍

mattiem,
@mattiem@mastodon.social avatar

@ctietze it was just the first thing that occurred to me. It does look kinda fire-and-forget, and if you do not need to await, why do it?

But always always always when moving from sync to async you need to think hard about ordering. Free, stand-alone Tasks will not run in the order they are created without explicit work on your part.

ctietze,
@ctietze@mastodon.social avatar

@mattiem Thank you for the reminder!

The contrived example might be misleading :/

I believe I'm fine here w.r.t. ordering. I block the UI before spawning the Task to use the send action (state = .pending) and unlock it with or without error when the response has arrived. So you can't send 2 messages in the UI and get results out of order. (Yet)

Re-reading @twostraws's overview on MainActor usage, the 'couldBeAnywhere()' example sounds like my case 🤔
https://www.hackingwithswift.com/quick-start/concurrency/how-to-use-mainactor-to-run-code-on-the-main-queue

ctietze,
@ctietze@mastodon.social avatar

@mattiem The recommendation by @twostraws there also is that, if you don't know, better be safe than sorry and use MainActor.run -- so we're all on the same page there, I think?

It's puzzling how I can have a non-async, non-throwing function plugged into an async throwing closure (compiler doesn't complain), and then I don't see that MainActor.run might be necessary.

mattiem,
@mattiem@mastodon.social avatar

@ctietze I believe the idea that “if you don’t know” doesn’t make sense. The compiler must know, or actor isolation cannot be enforced. It should only come up for code that should be MainActor-annotated but is not.

mattiem,
@mattiem@mastodon.social avatar

@ctietze I’m trying to parse your second paragraph but having trouble. I’m interested though!

ctietze,
@ctietze@mastodon.social avatar

@mattiem Ok. Thank you for your patience :)

It's basically declarred like this (in a view model that sends a message from SwiftUI text field + button press):

var sendChatMessage: (_ chatID: ChatID, _ message: String) async throws -> Void  

I can declare a similar-ish block like so:

let block: (ChatID, String) -> Void = { chatID, message in  
 print(chatID, message)  
}  

And it's possible to call it from inside sendChatMessage's implementation.

Replace print with UI update 💥

mattiem,
@mattiem@mastodon.social avatar

@ctietze ok yes I understand! That closure you define actually must be marked MainActor. Are you running with strict concurrency checking?

ctietze,
@ctietze@mastodon.social avatar

@mattiem Oh god I didn't know that SWIFT_STRICT_CONCURRENCY exists and now there are errors all over the place :D

That's a help and the warning I always wanted!

mattiem,
@mattiem@mastodon.social avatar

@ctietze yeah without the warnings it’s impossible to use concurrency safely

ctietze,
@ctietze@mastodon.social avatar

@mattiem If I wrap the screenshotted call in a closure it goes away :| Must be something different.

mattiem,
@mattiem@mastodon.social avatar

@ctietze I just sent you a message on slack - having trouble following everything

  • All
  • Subscribed
  • Moderated
  • Favorites
  • swift
  • DreamBathrooms
  • mdbf
  • ngwrru68w68
  • magazineikmin
  • thenastyranch
  • rosin
  • khanakhh
  • osvaldo12
  • Youngstown
  • slotface
  • Durango
  • kavyap
  • InstantRegret
  • tacticalgear
  • provamag3
  • ethstaker
  • cisconetworking
  • modclub
  • tester
  • GTA5RPClips
  • cubers
  • everett
  • normalnudes
  • megavids
  • Leos
  • anitta
  • JUstTest
  • lostlight
  • All magazines