I have a couple of comments regarding 1.9.829-alpha2:
• It looks like exceptions get completely swallowed - I see no logging at all when I deliberately throw an exception from within a go when I have virtual threads enabled in this build. I believe in standard core.async at least the exception message would get printed by default. I'm guessing that a default exception handler should be set for these threads.
• When verifying whether line numbers in these non-IOC go blocks were better, I was surprised to see that the emitted line number for a deliberately thrown exception in a go were off by 1 from the actual line number Maybe that's just due to the macro itself.
On 1, that’s good feedback, we’ll look at how the exception handler situation differs. On the second, can you provide a code example?
Hi Alex ! I'm not sure if it is always exactly off by 1, but in this code:
(ca/<!!
(ca/go
(println "1")
(println "2")
(RuntimeException. "deliberately thrown!"))) ;; <-- line 447
In my case, the first few lines look like this:
#error {
:cause "deliberately thrown!"
:via
[{:type java.lang.RuntimeException
:message "deliberately thrown!"
:at [dev$eval43350$fn__43351 invoke "dev.clj" 446]}]
:trace
[[dev$eval43350$fn__43351 invoke "dev.clj" 446]
[clojure.lang.AFn applyToHelper "AFn.java" 152]
[clojure.lang.AFn applyTo "AFn.java" 144]
[clojure.core$apply invokeStatic "core.clj" 667]
[clojure.core$with_bindings_STAR_ invokeStatic "core.clj" 1990]
So the line number there (446 in the exception) is off from the actual line number (447).Adding @fogus
@pmooser Thanks for reaching out. Do you have a small bit of code that demonstrates the Exception eating behavior?
Hi @fogus -
Actually, I apologize - I think this is some artifact of nrepl with exceptions or something. If I type it directly into a clj command line repl, I never see this problem, and I always get the exception.
So sorry to waste your time!
It’s absolutely not a waste of time. We are happy that you’re trying this out and learning more about any nREPL interactions is extremely useful information for us. Thank you.
I appreciate you guys and all of your hard work. I think I met you and Alex back at a conj in 2011 ! A lifetime ago.
Hi,
Different question regarding 1.9.829-alpha2:
I tried it out for the sake of utilizing virtual threads with pipeline-blocking which we use throughout our org.
I discovered that virtual threads are not used (on JDK21) and as I looked at the code, it seems buggy to me:
When the pipeline is of type (:blocking :compute), thread is https://github.com/clojure/core.async/blob/master/src/main/clojure/clojure/core/async.clj#L637-L640 - which in turn invokes thread-call with a :mixed workload.
Now, I am frankly a bit confused by the semantics of the different pipeline types and workloads (which seem to overlap, but are not an equal set). However, I would expect that at least a :blocking pipeline would utilize virtual threads when available, possibly via io-thread.
As a side note: as I was just preparing this message, I noticed that in the io-thread documentation it says that the code block must not do extended computation which I find confusing in a setting where virtual threads are available - why should it matter?
Would appreciate any clarification and/or practical solution.
Thanks
@didibus That is not compatible with the API and documentation:
1. There are 3 distinct pipeline variants:
a. pipeline - sets type as :compute
b. pipeline-blocking - sets type as :blocking
c. pipeline-async - sets type as :async
2. The doc string for pipeline says: "If you have multiple blocking operations to put in flight, use pipeline-blocking instead"
3. The doc string of pipeline-blocking is simply: "Like pipeline, for blocking operations."
So in a world without virtual threads, it makes sense to map both :compute and :blocking types to thread, but in a world with virtual threads, it makes less sense to me. If it's somehow a backward-compatibility issue, then adding an alternative type would make it reasonable for me to adapt my code, but the current types seem very explicit as they are IMHO.
I'm not saying it's the right thing to have done. But I suspect that could be part of the rational maybe. Because it ends up mapping to :mixed workload. So I'm guessing it thinks of it as a mix of blocking IO and transducer compute. 🤷🏻
In a world without vthread the compute workload also shouldn't just be an unbounded thread pool to be honest. So I definitely don't fully follow all the logic.
It is effectively bounded by the parallelism factor that the user passes, and it is fair to assume the user knows what they want to do IMO. It's also fine (depending on the context) to have more "compute threads" than CPUs because (unlike virtual threads) they have time sharing.
For a single pipeline yes. But if you run many concurrently the user can't easily tune it.
https://ask.clojure.org/index.php/14738/pipeline-blocking-uses-thread-internally-instead-of-thread
Regarding #4, do you mean what is the harm in using (many) CPU bound tasks on virtual threads? I assume it's the problem of them being cooperative (not pre-emptable), i.e. you could block the entire thread pool without them every yielding back to the scheduler to schedule another virtual thread.
What thread pool are you referring to?
Whichever OS thread pool (executor) is used to execute them. If the CPU and IO bound virtual threads share an OS thread pool then the CPU ones might block everything else (e.g. you have 4 OS threads and 4 virtual threads all stuck in tight loops). Because they are CPU bound they will never yield back control to the scheduler. Unlike the OS threads, which are interrupted by the OS, virtual threads are be cooperative and must yield back control.
I see, thank you.
I now found this is explained well in the section Scheduling virtual threads in the JEP above.
Interestingly, they do not explicitly warn about this, not even in Using virtual threads vs. platform threads.
What I take from this is that indeed the extreme case (as mentioned in the io-thread documentation) of "extended computation" can be harmful for scalability, but some computation may be fine. So it makes sense to avoid assigning virtual threads to :compute pipelines, but seems more than appropriate to assign virtual threads to :blocking pipelines.
I am not sure what you mean by extreme, but you only need a few CPU bound virtual threads sharing the same pool with the IO bound virtual threads to cause a problem. • Let's assume we only have a thread pool with one OS thread. • Let's say we have a scheduled task (via some library) which runs every minute and it takes 30 seconds to finish. • Let's also assume you have 1,000 virtual IO bound threads handling the web server requests. Because the single CPU bound virtual thread will block (use up) the entire pool (the only OS thread), it will have the effect that of all 1,000 web requests will have up to 30s of latency every 1 minute.
Yes, 30 seconds is definitely extreme, but I understand what you are trying to illustrate.
The general guidance is to handle I/O and light compute on the virtual thread. And if you have heavy compute, to offload it on a core-bounded real OS thread-pool.
I also find it strange the choice for pipeline-blocking, not sure if it's due to backwards compatibility.
Ah, I think maybe I understand the rational. Because the transducer xf does compute, and it could be heavy compute or anything in-between. I think pipeline-blocking is considered a :mixed use as in a combination of blocking and compute. So it decides to still allocate a real thread as to avoid doing compute (possibly heavy) from the transducer on the virtual thread pool.
Is there a way for me to submit an "official" enhancement request / bug or is this discussion the closest thing?
You can submit one here: https://ask.clojure.org/ When you ask a question it can be marked as a bug
Thank you for the response,
I would like to clarify:
1. From the documentation of pipeline and pipeline-blocking it seems to be expected that the transducer passed to pipeline-blocking performs blocking I/O operations with the provided parallelism n which is not bounded. I think this already meets the requirements mentioned above such that virtual threads should be used for blocking pipelines.
2. If not, perhaps another kind of pipeline type which is more explicit can be provided.
3. In practice though, the same application can run multiple such pipelines concurrently, accruing a significant number of threads. This is what happens in my setting (on multiple projects), which I admit is bad practice in a "platform threads setting", but in a "virtual threads setting" should be perfectly fine, from my understanding. This is actually my main motivation for using the new version.
4. Going up one meta-level though (if I may), I completely understand how virtual threads are best utilized when the workload is I/O bound and is of high concurrency but, even when these conditions do not apply (or apply weakly) - what's the harm in using virtual threads anyway?
> To put it another way, virtual threads can significantly improve application throughput when > • The number of concurrent tasks is high (more than a few thousand), and > • The workload is not CPU-bound, since having many more threads than processor cores cannot improve throughput in that case. > Virtual threads help to improve the throughput of typical server applications precisely because such applications consist of a great number of concurrent tasks that spend much of their time waiting.
Is that the proper way to do it?
I know it looks a bit weird haha, but yes that's the official way. Once I got used to it I actually like it. By framing bug reports and feature requests as "asks" Q&As, you allow others to respond with workarounds, alternatives, and so on, so it does serve as both a Q&A, and the tag and upvote can let the core team decide the priority to fix it.