Wrapping long-running calls as async

In considering how to update my code to use async/await, one particular question is how to deal with functions that block for a significant length of time. Specifically, I'm working with libgit2, where all network operations (push, fetch, etc) are blocking, and I want to wrap them in async functions.

The first thought I had was to use withCheckedContinuation() to run the blocking call on some background thread/queue and then resume the continuation (back on the original thread/queue) when it finishes.

Is there a more appropriate/idiomatic way to approach this?

I’ve also been doing some work with libgit2, and I’ve found that placing all the work inside an actor, in synchronous methods, isolates it well from the main actor and still requires await to access. I don’t think there’s any advantage to wrapping with continuations, because libgit2 ultimately doesn’t have support for async networking today, so the async continuations would always be synchronous in practice.

I’m not sure yet whether libgit2 is thread safe, but it may be possible to add concurrency between, say, multiple repositories, by each one getting its own actor instance.

I am planning on having an actor per repository. I'm also pondering what to do with operations like clone, where there isn't a repository until it's done. I suppose I could just create the repo actor and then clone, or maybe have a global actor for such operations.

I think part of my problem is I still don't fully understand how async concurrency works (and I've been following most of the async/await related threads). I'm used to threads and dispatched queues where the interactions are very explicit and fairly easy to visualize, but I don't have as good a grasp on how async tasks are executed.

Actors run on the "cooperative thread pool", which only contains as many threads as there are hardware threads on the system. It's imperative that these don't block (await is fine, it frees other actors to run on that thread, blocking is not, it hogs the thread but doesn't actually use it to do work).

I wonder if there's a define a separate executor for your actor to run on, so your git library can do its blocking on its own independent thread, where blocking would be acceptable

1 Like

That cooperative thread pool is exactly the kind of thing I need to understand better. The Swift language guide explains actor isolation pretty thoroughly, but does't mention the thread pool or the importance of not blocking.

2 Likes

That cooperative thread pool is exactly the kind of thing I need to
understand better.

WWDC 2021 Session 10254 Swift concurrency: Behind the scenes is a great discussion of this stuff.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

1 Like

I've watched that, but I don't think it has answered this question: What do I do if I have no choice but to call a blocking API (from an actor)?

Are there await-able Dispatch APIs that we can use to offload our blocking work onto an isolated shit-thread?

1 Like

What do I do if I have no choice but to call a blocking API (from an
actor)?

I don’t think there’s a good answer to this right now [1]. Then again, there isn’t a good answer to this in the Dispatch world either. I’ve seen a lot of code that does this by dispatching to a concurrent queue and then blocking there, ignoring the wider impact of that choice.

As to what you can do right now, that depends on the nature of this API and your use pattern. For example, if there’s some heavyweight API that’s going to block for a long period of time and that I call infrequently, I may well do this using a thread; that avoids consuming a worker thread (either Dispatch or Swift concurrency) and gives me full control over any thread explosion. In contrast, if I expect this API to block for a short period of time and I call it frequently, I might use a Dispatch serial queue. Regardless, withCheckedContinuation(…) would get me out and then back in to Actor Space™.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

[1] I suspect that the answer will come from the custom executor space but, speaking for myself, I can only discuss what is, not what might be.

5 Likes