Fork me on GitHub
#missionary
<
2024-03-30
>
Dallas Surewood07:03:22

What is the idiomatic way to ensure I'm using as little threads as possible for IO bound tasks? It sounds like I'm supposed to use m/via with :blk for IO tasks, but it will put the task on its own OS thread. Is there any equivalent to using core.async lightweight threads? For instance, if these Thread/sleeps were IO bound tasks, is there a way to rewrite this where they park on the current thread instead of spawning two exclusive threads?

(m/? (m/join vector
       (m/via m/blk (Thread/sleep 1000) 1)
       (m/via m/blk (Thread/sleep 1000) 2)))

Andrew Wilcox08:03:05

Well, Thread/sleep or a blocking IO operation is going to block the thread, so you'll need a separate thread for each blocking operation that you're doing at the same time. Maybe use java.nio for non-blocking IO, or perhaps the new Java 21 lightweight virtual threads might be useful (I haven't looked at them yet, so I don't know).

leonoel08:03:46

core.async doesn't solve this either, blocking calls must be wrapped with a/thread

Dallas Surewood08:03:02

I see. It looks like all the Missionary waiting functions all run on the same thread: The missionary scheduler. What is going on under the hood that lets something like this all run on the same thread?

(m/? (m/join vector
             (m/via m/blk (Thread/sleep 3000))
             (m/sleep 3000 3)
             (m/sleep 3000 3)
             (m/sleep 3000 3)
             (m/sleep 3000 3)))

Dallas Surewood08:03:16

And is there no way to do that for non missionary functions?

Andrew Wilcox08:03:09

It looks like all the Missionary waiting functions all run on the same threadNo, Missionary doesn't switch to a Missionary scheduler thread (if that's what you meant). All Missionary functions run in the current thread, aside from when you use m/via to specifically request a different thread. If you start two different threads and run Missionary functions in each thread, each will continue to run in the thread that you started them in. > What is going on under the hood that lets something like this all run on the same thread? Missionary constructs such as ap, sp, etc. are macros that rewrite their body to implement coroutines. This is why you have to call ? etc. syntactically within the sp etc. form. See "Coroutines' in https://gorgeous-sorbet-a5a2bf.netlify.app/synchronizers

kawas10:03:03

Doing blocking function calls in go blocks or coroutines is a big "don't" either with core.async or missionary. If you want a deep integration of your "users" IO with the runtime/event loop IO there must exist an API which core.async and missionary don't provide. Examples of those API in the wild are : select, epoll, reactive framework, etc. An other solution can be provided by a runtime with deep knowledge of your code, and that, I think :thinking_face: , is what the JVM is doing with the new Virtual Thread feature. The JVM can "catch" your calls to blocking functions of its JDK IO classes and turn them to non blocking calls.

Dallas Surewood15:03:52

> No, Missionary doesn't switch to a Missionary scheduler thread (if that's what you meant). I'm just basing this off VisualVM where the repl thread doesn't seem to be blocked. Maybe that's just a quirk of how the thread is reported when using m/? at the top level. > If you start two different threads and run Missionary functions in each thread, each will continue to run in the thread that you started them in. When I run this, it looks like the Thread is only blocked for one second instead of 4.

(m/via m/blk (Thread/sleep 1000)
         (m/? (m/join vector
                      (m/sleep 3000 3)
                      (m/sleep 3000 3)
                      (m/sleep 3000 3)
                      (m/sleep 3000 3)
                      (m/sleep 3000 3))))

Dallas Surewood15:03:58

So from Clojure, what would be the appropriate way to make a heavy IO call non blocking? It sounds like coroutines with m/sp aren't the way to do that. Do I have to drop down into Java?

leonoel18:03:25

Currently there is no way to make a blocking call non-blocking, neither in clojure nor in java. A blocking call, by definition, blocks the calling thread until the call returns. What coroutines do - both m/sp and a/go - is sequential composition of asynchronous tasks in a special context that looks like threads but doesn't rely on actual threads. The special context is just syntactic sugar, it doesn't change the JVM capabilities, especially it is unable to detect if a call is blocking and make it asynchronous - that's why m/via and a/thread exist. Project loom addresses this issue but doesn't fundamentally change the evaluation model - a blocking call is still blocking when called from a virtual thread, it is the same abstraction with different performance implications

leonoel18:03:31

> When I run this, it looks like the Thread is only blocked for one second instead of 4. That doesn't seem right

Dallas Surewood19:03:29

I'm sorry, I was wrong about the timing of that function.

Dallas Surewood19:03:00

What I mean is I don't understand a function becomes non-blocking in the first place. In Clojure or Java. What primitives are used to let us wait on something? I don't care so much about making a blocking function non-blocking, I'm talking more if I were to write my own library that needed async capabilities

Dallas Surewood20:03:06

For instance, why can we wait on m/sleep but not Thread/sleep

leonoel20:03:08

All asynchronous constructs use callbacks in one way or another. An action is requested by a function call taking a callback, the function returns immediately, things get done somehow, eventually the callback is invoked to release control to the origin of the request.

Dallas Surewood20:03:32

But where do different thread implementations come into that? Why does a future need a whole new thread but a go block can park on channels? Why can missionary wait on m/sleep and make 1000 of those but it can't wait on other async libraries like clj-http

leonoel20:03:31

if a library has an async API exposing callbacks, missionary can consume it (modulo plumbing).

leonoel20:03:02

missionary tasks are just callbacks, core.async channels are also callbacks under the hood

Dallas Surewood21:03:01

Hmm. Let me get this straight 1. Async functions are passed in a success and failure function and then they run some IO interaction. 2. Blocking functions that are IO bound are async functions that are pre-written to "wait" for this result and don't expose their callbacks. 3. I went on a reading tangent with Java NIO and Completeable futures, but it sounds like you don't need these two write async capability in your libraries?

leonoel07:03:16

yes that's right