Fork me on GitHub
#core-async
<
2023-03-01
>
stephenmhopper13:03:51

I’ve been testing out core.async with Java 19 virtual threads by swapping out the underlying thread pool executor like this:

(defonce virtual-thread-executor
  (let [executor-svc (Executors/newVirtualThreadPerTaskExecutor)]
    (reify protocols/Executor
      (protocols/exec [this r]
        (.execute executor-svc ^Runnable r)))))

(alter-var-root #'clojure.core.async.impl.dispatch/executor
  (constantly (delay virtual-thread-executor)))
So far, I’ve been pleased with the results. The most exciting piece so far is that I can just use go and go-loop everywhere without worrying about blocking operations inside of a go block. Are there any plans to support virtual threads in core.async once virtual threads are no longer in preview?

Alex Miller (Clojure team)13:03:53

Yes, we would look like to support virtual threads, but it probably implies a completely different implementation of go blocks

stephenmhopper13:03:48

Oh, really? Why’s that?

Alex Miller (Clojure team)13:03:01

virtual threads give you much of the continuation requirements the current implementation is designed to give you - tbd what is actually needed

Rupert (All Street)13:03:15

I imagine a fair bit of the core.async code is implementing its own virtual threads (by effectively rewriting the user's code with macros - see ioc_macros.clj). If core.async uses virtual threads on the JVM I would expect the code could be considerably reduced/simplified for the JVM platform. One of the usecases for core.async (of which there are many) was to benefit from having many core.async virtual threads (e.g. millions of active go blocks) - even if you didn't need the rest of core.async's capabilities. I imagine in those usecases some coders may consider using JVM virtual threads now instead of core.async.

seancorfield15:03:21

An approach I played with a bit is to implement go (and go-loop) directly as using a virtual thread instead of being a complex transpiling macro and then use blocking ops everywhere. Bear in mind that core.async still uses full-on threads, separate from that thread pool, in at least one place. I think a JDK 19 reimagining of core.async could be dramatically simpler in terms of its API as well as its implementation.

seancorfield16:03:02

Also worth noting that 19.0.2 has a known memory leak with --enable-preview even without using vthreads (fixed in JDK 20 but not yet backported).

Rupert (All Street)16:03:47

The implementation of core.async's go/go-loop are amazing especially since they work with no extension to the language itself and work in JavaScript where there are no threads at all. They effectively escape callback hell in ClojureScript which is very useful. It used to be a killer selling point for Clojure that only golang and a few others could rival.

mpenet16:03:43

I think promesa has some kind of implem of core.async go with support for vthreads, but you have to buy into the whole thing I think

mpenet16:03:28

it was just on my radar, I didn't dig into the details yet

hiredman17:03:06

the problem with naively adding virtual threads to core.async is the channel implementation is built around callbacks, and it unconditionally throws the callback onto an executor (another thread, virtual or otherwise) to run

hiredman17:03:51

with virtual threads, you don't want that executor at all

hiredman17:03:05

(the promesa library also does the naive thing despite claiming to be written for virtual threads)

seancorfield17:03:26

I don't even know if the API of core.async makes sense for virtual threads? I would think it could be a much simpler idiom: without the blocking/non-blocking difference in the API.

hiredman17:03:07

with virtual threads you would want to use the just the blocking api, but because core.async's channels are written for callbacks, it implements the blocking api by using the non-blocking api, and passing a callback that delivers to a promise that the blocking api blocks on

hiredman17:03:11

but then the channel executes the callback by throwing it onto an executor, when means instead of delivering to the promise directly, you pass it off to another thread to do the delivering

hiredman17:03:06

if core.async's channel implementation was changed to not execute callbacks on the threadpool, but just invoke them immediately, and let whoever passes in the callback decide if it needs to move to threadpool, it would keep largely the same api, but then using the blocking api directly with virtual threads would make more sense

Alex Miller (Clojure team)17:03:54

the question is whether you can still have alt'ing

hiredman18:03:46

like, the fnhandler stuff would all stay the same, just go macro ops fn handlers would have to add code to move execution to the executor instead of relying on the channel to do it, and the blocking ops would stay the same, but their callback (that just delivers to the promise) would execute on whatever thread the channel take/put is running on and not get moved to the queue of the executor

seancorfield18:03:53

Or they could use vthreads for that too?

hiredman18:03:06

using another thread just to deliver to a promise is a waste vthread or not

1