├── trash.txt ├── .gitignore ├── CHANGELOG.md ├── Makefile ├── dev └── src │ └── bench.clj ├── project.clj ├── LICENSE ├── test └── virtuoso │ ├── core_test.clj │ ├── v2_test.clj │ └── v3_test.clj ├── src └── virtuoso │ ├── v2.clj │ ├── core.clj │ └── v3.clj ├── trash.clj ├── doc ├── v2_api.md └── v1_api.md └── README.md /trash.txt: -------------------------------------------------------------------------------- 1 | server { 2 | listen 8080; 3 | server_name localhost; 4 | 5 | location /hugefile.bin { 6 | root html; 7 | limit_rate 500k; 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | profiles.clj 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | /.lein-* 10 | /.nrepl-port 11 | /.prepl-port 12 | .hgignore 13 | .hg/ 14 | 15 | node_modules/ 16 | package-lock.json 17 | package.json 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.3-SNAPSHOT 2 | 3 | - ? 4 | - ? 5 | - ? 6 | 7 | ## 0.1.2-SNAPSHOT 8 | 9 | - add v3 namespace 10 | - add benchmarks 11 | 12 | ## 0.1.1 13 | 14 | - add v2 namespace 15 | - chage license 16 | - misc changes 17 | 18 | ## 0.1.0 19 | 20 | - initial release 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | jdk21 ?= $(error Please specify the jdk21=... home path) 3 | jdk21bin = ${jdk21}/bin/java 4 | 5 | repl: 6 | JDK21=${jdk21bin} lein repl 7 | 8 | .PHONY: test 9 | test: 10 | JDK21=${jdk21bin} lein test 11 | 12 | release: 13 | JDK21=${jdk21bin} lein release 14 | 15 | toc-install: 16 | npm install --save markdown-toc 17 | 18 | toc-build: 19 | node_modules/.bin/markdown-toc -i readme.md 20 | -------------------------------------------------------------------------------- /dev/src/bench.clj: -------------------------------------------------------------------------------- 1 | (ns bench 2 | (:require 3 | [virtuoso.v3 :as v3] 4 | [clj-http.client :as client] 5 | [cheshire.core :as json])) 6 | 7 | ;; /opt/homebrew/var/www 8 | (def URL "http://127.0.0.1:8080/hugefile.bin") 9 | 10 | (defn download [i] 11 | (with-open [in ^java.io.InputStream 12 | (:body (client/get URL {:as :stream})) 13 | out 14 | (java.io.OutputStream/nullOutputStream)] 15 | (.transferTo in out))) 16 | 17 | 18 | (def SEQ 19 | (vec (range 100))) 20 | 21 | 22 | (comment 23 | 24 | "Elapsed time: 1102802.057709 msecs" 25 | (time 26 | (count 27 | (map download SEQ))) 28 | 29 | "Elapsed time: 44213.30375 msecs" 30 | (time 31 | (count 32 | (pmap download SEQ))) 33 | 34 | "Elapsed time: 11124.417959 msecs" 35 | (time 36 | (count 37 | (v3/map download SEQ))) 38 | 39 | "Elapsed time: 11090.514792 msecs" 40 | (time 41 | (count 42 | (v3/pmap 512 download SEQ))) 43 | 44 | ) 45 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject com.github.igrishaev/virtuoso "0.1.3-SNAPSHOT" 2 | 3 | :java-cmd 4 | ~(System/getenv "JDK21") 5 | 6 | :description 7 | "A number of trivial wrappers on top of virtual threads" 8 | 9 | :url 10 | "https://github.com/igrishaev/virtuoso" 11 | 12 | :deploy-repositories 13 | {"releases" {:url "https://repo.clojars.org" :creds :gpg}} 14 | 15 | :license 16 | {:name "The Unlicense" 17 | :url "https://unlicense.org/"} 18 | 19 | :release-tasks 20 | [["vcs" "assert-committed"] 21 | ["test"] 22 | ["change" "version" "leiningen.release/bump-version" "release"] 23 | ["vcs" "commit"] 24 | ["vcs" "tag" "--no-sign"] 25 | ["deploy"] 26 | ["change" "version" "leiningen.release/bump-version"] 27 | ["vcs" "commit"] 28 | ["vcs" "push"]] 29 | 30 | :dependencies 31 | [] 32 | 33 | :profiles 34 | {:dev 35 | {:source-paths ["dev/src"] 36 | :dependencies [[org.clojure/clojure "1.11.1"] 37 | [clj-http "3.12.0"] 38 | [cheshire "5.10.0"]]} 39 | :uberjar 40 | {:aot :all 41 | :jvm-opts ["-Dclojure.compiler.direct-linking=true"]}}) 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /test/virtuoso/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns virtuoso.core-test 2 | (:import 3 | java.util.concurrent.RejectedExecutionException 4 | java.util.concurrent.Callable 5 | java.util.concurrent.ExecutorService) 6 | (:require 7 | [clojure.test :refer [deftest is]] 8 | [virtuoso.core :as v])) 9 | 10 | 11 | (deftest test-executor 12 | 13 | (let [capture! (atom nil)] 14 | 15 | (v/with-executor [exe] 16 | (reset! capture! exe)) 17 | 18 | (let [^ExecutorService exe @capture!] 19 | (is (instance? ExecutorService exe)) 20 | 21 | (try 22 | (.submit exe (reify Callable 23 | (call [_] 24 | (+ 1 2)))) 25 | (is false) 26 | (catch RejectedExecutionException e 27 | (is true)))))) 28 | 29 | 30 | (deftest test-thread 31 | (let [capture! (atom {:foo 1}) 32 | t (v/thread 33 | (swap! capture! update :foo inc))] 34 | (.join t) 35 | (is (= {:foo 2} @capture!)))) 36 | 37 | 38 | (deftest test-futures 39 | 40 | (let [fut (v/future (+ 1 2 3))] 41 | (is (future? fut)) 42 | (is (= 6 @fut))) 43 | 44 | (v/with-executor [exe] 45 | (let [a 3 46 | b 4 47 | 48 | f1 (v/future-via exe 49 | (+ a b)) 50 | f2 (v/future-via exe 51 | (* a b)) 52 | 53 | res 54 | [@f1 @f2]] 55 | 56 | (is (= [7 12] res)))) 57 | 58 | (let [futs 59 | (v/futures 60 | (+ 1 2 3) 61 | (* 1 2 3) 62 | (assoc {:foo 42} :bar 1))] 63 | 64 | (is (= 3 (count futs))) 65 | (is (= [6 6 {:foo 42, :bar 1}] (mapv deref futs)))) 66 | 67 | (let [values 68 | (v/futures! 69 | (+ 1 2 3) 70 | (* 1 2 3) 71 | (assoc {:foo 42} :bar 1))] 72 | 73 | (is (= [6 6 {:foo 42, :bar 1}] values)))) 74 | 75 | 76 | (deftest test-pmap 77 | 78 | (let [futs 79 | (v/pmap inc [1 2 3])] 80 | (is (= [2 3 4] (mapv deref futs)))) 81 | 82 | (let [futs 83 | (v/pmap + [1 2 3] [2 3 4])] 84 | (is (= [3 5 7] (mapv deref futs)))) 85 | 86 | (let [values 87 | (v/pmap! inc [1 2 3])] 88 | (is (= [2 3 4] values))) 89 | 90 | (let [values 91 | (v/pmap! + [1 2 3] [2 3 4])] 92 | (is (= [3 5 7] values)))) 93 | 94 | 95 | (deftest test-each 96 | 97 | (let [futs 98 | (v/each [x [1 2 3]] 99 | (inc x))] 100 | (is (= [2 3 4] (mapv deref futs)))) 101 | 102 | (let [values 103 | (v/each! [x [1 2 3]] 104 | (inc x))] 105 | (is (= [2 3 4] values)))) 106 | -------------------------------------------------------------------------------- /test/virtuoso/v2_test.clj: -------------------------------------------------------------------------------- 1 | (ns virtuoso.v2-test 2 | (:require 3 | [clojure.test :refer [deftest is]] 4 | [virtuoso.v2 :as v])) 5 | 6 | (deftest test-future-ok 7 | (let [fut 8 | (v/future (println 42) 9 | (do 1 2 3))] 10 | (is (future? fut)) 11 | (is (= 3 @fut))) 12 | (let [fut 13 | (v/future (/ 0 0))] 14 | (is (future? fut)) 15 | (try 16 | @fut 17 | (is false) 18 | (catch java.util.concurrent.ExecutionException e 19 | (is (= "Divide by zero" 20 | (-> e ex-cause ex-message))))))) 21 | 22 | 23 | (deftest test-pvalues-ok 24 | (let [items 25 | (v/pvalues 1 26 | 2 27 | 3)] 28 | (is (= [1 2 3] items))) 29 | (let [items 30 | (v/pvalues 1 31 | (/ 0 0) 32 | 3)] 33 | (is (seq? items)) 34 | (is (= 1 35 | (first items))) 36 | (try 37 | (second items) 38 | (is false) 39 | (catch java.util.concurrent.ExecutionException e 40 | (is true))) 41 | (try 42 | (last items) 43 | (is false) 44 | (catch java.util.concurrent.ExecutionException e 45 | (is true))))) 46 | 47 | (deftest test-map-ok 48 | (let [items 49 | (v/map / [8 10 3] [4 5 0])] 50 | (is (= 2 (first items))) 51 | (is (= 2 (second items))) 52 | (try 53 | (last items) 54 | (is false) 55 | (catch java.util.concurrent.ExecutionException e 56 | (is true)))) 57 | (let [items 58 | (v/map (fn [x] 59 | (/ x x)) [1 2 3 0])] 60 | (is (= 1 (first items))) 61 | (is (= 1 (second items))) 62 | (try 63 | (last items) 64 | (is false) 65 | (catch java.util.concurrent.ExecutionException e 66 | (is true))))) 67 | 68 | (deftest test-for-ok 69 | (let [items 70 | (v/for [x [1 2 3 0]] 71 | (/ x x))] 72 | (is (= 1 (first items))) 73 | (try 74 | (last items) 75 | (is false) 76 | (catch java.util.concurrent.ExecutionException e 77 | (is true)))) 78 | 79 | (let [t1 80 | (System/currentTimeMillis) 81 | 82 | items 83 | (v/for [x (range 99999)] 84 | (do (Thread/sleep 1000) 85 | x)) 86 | 87 | _ 88 | (doall items) 89 | 90 | t2 91 | (System/currentTimeMillis)] 92 | 93 | (is (< (- t2 t1) 2000)))) 94 | 95 | 96 | (deftest test-thread-ok 97 | (let [time1 98 | (System/currentTimeMillis) 99 | 100 | t1 (v/thread 101 | (do (Thread/sleep 1000) 102 | 1)) 103 | t2 (v/thread 104 | (do (Thread/sleep 2000) 105 | 2)) 106 | 107 | _ (.join t1) 108 | _ (.join t2) 109 | 110 | time2 111 | (System/currentTimeMillis)] 112 | 113 | (is (< (- time2 time1) 2200)))) 114 | -------------------------------------------------------------------------------- /src/virtuoso/v2.clj: -------------------------------------------------------------------------------- 1 | (ns virtuoso.v2 2 | " 3 | A set of Clojure-like functions and macros 4 | that act using a global virtual executor. 5 | " 6 | (:refer-clojure :exclude [future 7 | pmap 8 | map 9 | for 10 | pvalues]) 11 | (:import 12 | (java.lang VirtualThread) 13 | (java.util.concurrent Callable 14 | Future 15 | ExecutorService 16 | Executors))) 17 | 18 | 19 | (set! *warn-on-reflection* true) 20 | 21 | 22 | (alias 'cc 'clojure.core) 23 | 24 | 25 | (def ^ExecutorService -EXECUTOR 26 | (Executors/newVirtualThreadPerTaskExecutor)) 27 | 28 | 29 | (defn underef 30 | " 31 | A helper function that accepts a lazy sequence 32 | of futures and derefs items lazily one by one. 33 | " 34 | [coll] 35 | (lazy-seq 36 | (when-let [f (first coll)] 37 | (cons (deref f) (underef (next coll)))))) 38 | 39 | 40 | (defmacro future 41 | " 42 | Wraps an arbitrary block of code into a future 43 | bound to global virtual executor service. 44 | " 45 | ^Future [& body] 46 | `(.submit -EXECUTOR 47 | ^Callable 48 | (^{:once true} fn* [] ~@body))) 49 | 50 | 51 | (defmacro pvalues 52 | " 53 | Wrap each form into a virtual future and return 54 | a lazy sequence that, while iterating, derefs 55 | them. 56 | " 57 | [& forms] 58 | `(underef 59 | (list ~@(cc/for [form forms] 60 | `(future ~form))))) 61 | 62 | 63 | (defn map 64 | " 65 | Like `map` but run each function in a virtual future. 66 | Return a lazy sequence that derefs futures when 67 | iterating. 68 | " 69 | ([f coll] 70 | (underef 71 | (doall 72 | (cc/map (fn [item] 73 | (future (f item))) 74 | coll)))) 75 | 76 | ([f coll & colls] 77 | (underef 78 | (doall 79 | (apply cc/map 80 | (fn [& items] 81 | (future (apply f items))) 82 | coll 83 | colls))))) 84 | 85 | 86 | (defmacro for 87 | " 88 | Like `for` but wraps each body expression into a virtual 89 | future. Return a lazy sequence that derefs them when 90 | iterating. 91 | " 92 | [seq-exprs body-expr] 93 | `(underef 94 | (doall 95 | (cc/for ~seq-exprs 96 | (future ~body-expr))))) 97 | 98 | 99 | (defmacro thread 100 | " 101 | Spawn and run a new virtual thread. 102 | " 103 | ^VirtualThread [& body] 104 | `(-> (Thread/ofVirtual) 105 | (.name "virtuoso.v2") 106 | (.start 107 | (reify Runnable 108 | (run [this#] 109 | ~@body))))) 110 | 111 | 112 | (defonce ^Thread -shutdown-hook 113 | (new Thread (fn [] 114 | (.close -EXECUTOR)))) 115 | 116 | 117 | (defonce ___ 118 | (-> (Runtime/getRuntime) 119 | (.addShutdownHook -shutdown-hook))) 120 | -------------------------------------------------------------------------------- /trash.clj: -------------------------------------------------------------------------------- 1 | 2 | (time 3 | (do 4 | (mapv deref 5 | (map 6 | (fn [x] 7 | (future 8 | (cheshire/get "https://github.com/seductiveapps/largeJSON/raw/master/100mb.json"))) 9 | (range 0 10))) 10 | :end)) 11 | 12 | ;; "https://microsoftedge.github.io/Demos/json-dummy-data/1MB.json 13 | 14 | ;; "https://raw.githubusercontent.com/json-iterator/test-data/refs/heads/master/large-file.json" 15 | 16 | (time (->> (clojure.core/range 0 100) 17 | (clojure.core/map 18 | (fn [x] 19 | (clojure.core/future 20 | (try 21 | (clj-http.client/get "https://microsoftedge.github.io/Demos/json-dummy-data/1MB.json") 22 | x 23 | (catch Throwable e 24 | :error) 25 | (finally 26 | (println x "DONE")))))) 27 | (clojure.core/doall) 28 | (clojure.core/mapv deref))) 29 | 30 | 31 | (time (->> (clojure.core/range 0 100) 32 | (map 33 | (fn [x] 34 | (try 35 | (clj-http.client/get "https://microsoftedge.github.io/Demos/json-dummy-data/1MB.json") 36 | x 37 | (catch Throwable e 38 | :error) 39 | (finally 40 | (println x "DONE"))))) 41 | (doall))) 42 | 43 | (defmacro future [& body] 44 | `(let [array# 45 | (object-array 2) 46 | 47 | thread# 48 | (Thread/startVirtualThread 49 | (binding-conveyor-fn 50 | (^{:once true} fn* [] 51 | (let [[result# error#] 52 | (try 53 | [(do ~@body) nil] 54 | (catch Throwable e# 55 | [nil e#]))] 56 | (aset array# 0 result#) 57 | (aset array# 1 error#)))))] 58 | 59 | (reify 60 | 61 | clojure.lang.IDeref 62 | (deref [_#] 63 | (.join thread#) 64 | (let [[result# error#] array#] 65 | (if error# 66 | (throw error#) 67 | result#))) 68 | 69 | #_ 70 | clojure.lang.IBlockingDeref 71 | #_ 72 | (deref [_ timeout-ms# timeout-val#] 73 | (.join thread# timeout-val#) 74 | (let [[result# error#] array#] 75 | (if error# 76 | (throw error#) 77 | result#))) 78 | 79 | clojure.lang.IPending 80 | (isRealized [_] 81 | (.isAlive thread#)) 82 | 83 | java.util.concurrent.Future 84 | (get [this#] 85 | (deref this#)) 86 | 87 | #_ 88 | (get [_ timeout unit] 89 | (.join thread# timeout-val#) 90 | ;; convert 91 | #_ 92 | (.get fut timeout unit)) 93 | 94 | (isCancelled [_] 95 | (.isInterrupted thread#)) 96 | 97 | (isDone [_] 98 | (not (.isAlive thread#))) 99 | 100 | (cancel [_ interrupt?] 101 | (.interrupt thread#)))) 102 | ) 103 | 104 | 105 | (defmacro future 106 | " 107 | TODO 108 | " 109 | ^CompletableFuture [& body] 110 | `(let [future# (new CompletableFuture)] 111 | (Thread/startVirtualThread 112 | (binding-conveyor-fn 113 | (^{:once true} fn* [] 114 | (let [[result# e#] 115 | (try 116 | [(do ~@body) nil] 117 | (catch Throwable e# 118 | [nil e#]))] 119 | (if e# 120 | (.completeExceptionally future# e#) 121 | (.complete future# result#)))))) 122 | future#)) 123 | -------------------------------------------------------------------------------- /doc/v2_api.md: -------------------------------------------------------------------------------- 1 | The new namespace called `virtuoso.v2` brings some functions and macros that 2 | already present in the `virtuoso.core` namespace. To prevent things from 3 | breaking, I decided to put them into a new namespace. Personally I find `v2` 4 | more convenient for usage but this is up to you. 5 | 6 | The `v2` API provides utilities named after their Clojure counterparts, 7 | e.g. `map`, `pvalues` and so on. But under the hood, they use a global virtual 8 | executor. This executor gets closed on JVM shutdown. 9 | 10 | Import the namespace: 11 | 12 | ~~~clojure 13 | (ns some.project 14 | (:require 15 | [virtuoso.v2 :as v])) 16 | ~~~ 17 | 18 | **The `future` macro** acts like a regular future but is served using a global 19 | virtual executor service: 20 | 21 | ~~~clojure 22 | (def -fut 23 | (v/future 24 | (let [a 1 b 2] (+ a b)))) 25 | 26 | -fut 27 | #object[java.util... 0x1f3aa45f "java.util.concurrent...@1f3aa45f[Completed normally]"] 28 | 29 | @-fut 30 | 3 31 | ~~~ 32 | 33 | The macro accepts an arbitrary block of code that gets executed into a future. 34 | 35 | **The `pvalues` macro** accepts a number of forms and runs each into a 36 | future. The result is a lazy sequence of dereferenced values: 37 | 38 | ~~~clojure 39 | (def -items 40 | (v/pvalues (+ 3 4) 41 | (Thread/sleep 1000) 42 | (let [a 3] 43 | (* a a)))) 44 | 45 | -items 46 | (7 nil 9) 47 | ~~~ 48 | 49 | Pay attention all the futures get run immediately but the process of 50 | dereferencing is lazy. They get `deref`-ed one by one as you iterate the 51 | result. Thus, you can easily spot an exception should it pop up: 52 | 53 | ~~~clojure 54 | (def -items 55 | (v/pvalues (/ 3 2) 56 | (/ 3 1) 57 | (/ 3 0))) 58 | 59 | (first -items) 60 | 3/2 61 | 62 | (second -items) 63 | 3 64 | 65 | (last -items) ;; only now throws 66 | ;; Execution error (ArithmeticException) at virtuoso.v2/fn... 67 | ;; Divide by zero 68 | ~~~ 69 | 70 | **The `map` function** is similar to the standard `map` but performs each 71 | function call in a virtual future. All the steps are fired without chunking. The 72 | result is a lazy sequence of dereferenced values. 73 | 74 | ~~~clojure 75 | (def -items 76 | (v/map (fn [x] (/ 10 x)) [5 4 3 2 1 0])) 77 | 78 | ;; don't touch the last item 79 | (take 5 -items) 80 | ;; (2 5/2 10/3 5 10) 81 | 82 | ;; touch it 83 | (last -items) 84 | ;; Execution error (ArithmeticException) at virtuoso.v2/fn... 85 | ;; Divide by zero 86 | ~~~ 87 | 88 | **The `for`** macro acts like `for` but wraps each body expression into a 89 | future. All the futures are fired at once with no chunking. The result is a lazy 90 | sequence of dereferenced values. You can use `:let`, `:when`, and other nested 91 | forms: 92 | 93 | ~~~clojure 94 | (def -items 95 | (v/for [a [:a :b :c] 96 | b [1 2 3 4 5] 97 | :when (and (not= a :b) (not= b 3))] 98 | {:a a :b b})) 99 | 100 | -items 101 | ({:a :a, :b 1} 102 | {:a :a, :b 2} 103 | {:a :a, :b 4} 104 | {:a :a, :b 5} 105 | {:a :c, :b 1} 106 | {:a :c, :b 2} 107 | {:a :c, :b 4} 108 | {:a :c, :b 5}) 109 | ~~~ 110 | 111 | The `thread` macro just creates and starts a new virtual thread out from a block 112 | of code. Useful if you'd like to deal with `Thead` instances: 113 | 114 | ~~~clojure 115 | (let [t1 (v/thread (Thread/sleep 1000) (+ 1 2)) 116 | t2 (v/thread (Thread/sleep 2000) (* 3 2))] 117 | (.join t1) 118 | (.join t2) 119 | (println "both are done")) 120 | ~~~ 121 | 122 | The `v2` namespace, when loaded, adds its own JVM shutdown hook as follows: 123 | 124 | ~~~clojure 125 | (defonce ^Thread -shutdown-hook 126 | (new Thread (fn [] 127 | (.close -EXECUTOR)))) 128 | 129 | (defonce ___ 130 | (-> (Runtime/getRuntime) 131 | (.addShutdownHook -shutdown-hook))) 132 | ~~~ 133 | 134 | The global executor will be closed on JVM shutdown. 135 | -------------------------------------------------------------------------------- /test/virtuoso/v3_test.clj: -------------------------------------------------------------------------------- 1 | (ns virtuoso.v3-test 2 | (:import 3 | (java.lang VirtualThread)) 4 | (:require 5 | [clojure.test :refer [deftest is]] 6 | [virtuoso.v3 :as v])) 7 | 8 | (set! *warn-on-reflection* true) 9 | 10 | (def ^:dynamic *var* nil) 11 | 12 | (deftest test-future-1 13 | (let [f (v/future (+ 1 2))] 14 | (future? f) 15 | (is (= 3 @f))) 16 | 17 | (let [f (v/future (/ 1 0))] 18 | (future? f) 19 | (try 20 | @f 21 | (is false) 22 | (catch Exception e 23 | (is (instance? java.util.concurrent.ExecutionException 24 | e)) 25 | (is (= "java.lang.ArithmeticException: Divide by zero" 26 | (ex-message e))) 27 | (is (instance? ArithmeticException 28 | (ex-cause e)))))) 29 | 30 | (let [f 31 | (v/future 32 | (Thread/sleep 3000) 33 | ::done) 34 | v 35 | (deref f 1000 ::timeout)] 36 | 37 | (is (= v ::timeout))) 38 | 39 | (let [f 40 | (binding [*var* 42] 41 | (let [f (v/future 42 | (+ *var* 10))] 43 | (Thread/sleep 100) 44 | f))] 45 | 46 | (is (= 52 @f))) 47 | 48 | (with-redefs [double? (constantly ::abc)] 49 | (let [f (binding [*var* 42] 50 | (let [f (future 51 | (double? 42))] 52 | (Thread/sleep 100) 53 | f))] 54 | (is (= ::abc @f))))) 55 | 56 | 57 | (deftest test-pvalues 58 | (let [capture! 59 | (atom #{}) 60 | 61 | result 62 | (v/pvalues 63 | (let [result (+ 1 1)] 64 | (swap! capture! conj :a) 65 | result) 66 | (let [result (/ 0 0)] 67 | (swap! capture! conj :b) 68 | result) 69 | (let [result (+ 2 2)] 70 | (swap! capture! conj :c) 71 | result))] 72 | 73 | (is (= #{:a :c} 74 | @capture!)) 75 | 76 | (is (= 2 77 | (first result))) 78 | 79 | (try 80 | (nth result 1) 81 | (is false) 82 | (catch Exception e 83 | (is true))))) 84 | 85 | (deftest test-thread 86 | (let [t (v/thread 87 | (Thread/sleep 1000) 88 | (+ 1 2))] 89 | (is (instance? VirtualThread t)) 90 | (is (.isAlive t)) 91 | (.join t) 92 | (is (not (.isAlive t))))) 93 | 94 | (deftest test-future-2 95 | (let [f (v/future 1)] 96 | (is (future? f)) 97 | (is (= 1 @f))) 98 | 99 | (let [f (v/future 100 | (/ 0 0))] 101 | (is (future? f)) 102 | (try 103 | @f 104 | (is false) 105 | (catch Exception e 106 | (is e))))) 107 | 108 | (deftest test-map 109 | (let [result (v/map inc [1 2 3])] 110 | (is (= [2 3 4] result))) 111 | 112 | (let [result (v/map / [4 6 3] [2 2 0])] 113 | (is (= 2 (nth result 0))) 114 | (is (= 3 (nth result 1))) 115 | (try 116 | (nth result 2) 117 | (is false) 118 | (catch Exception e 119 | (is e)))) 120 | 121 | (let [result (v/map / [4 6 3] [2 2 3 0])] 122 | (is (= [2 3 1] result)))) 123 | 124 | (deftest test-pmap 125 | (let [capture! 126 | (atom #{}) 127 | 128 | result 129 | (v/pmap 3 130 | (fn [a b] 131 | (swap! capture! conj [a b]) 132 | (/ a b)) 133 | [3 2 1 0] 134 | [3 2 1 0])] 135 | 136 | (is (= #{} @capture!)) 137 | (is (= 1 (first result))) 138 | (is (= #{[2 2] [3 3] [1 1]} @capture!)))) 139 | 140 | (deftest test-future-via 141 | (let [f 142 | (v/with-executor [exe] 143 | (v/future-via [exe] 144 | (Thread/sleep 1500)))] 145 | (is (false? (future-cancel f))) 146 | (is (not (future-cancelled? f))) 147 | (is (future-done? f))) 148 | 149 | (let [f 150 | (v/with-executor [exe] 151 | (v/future-via [exe] 152 | (Thread/sleep 100) 153 | (/ 0 0)))] 154 | (is (false? (future-cancel f))) 155 | (is (not (future-cancelled? f))) 156 | (is (future-done? f)))) 157 | -------------------------------------------------------------------------------- /src/virtuoso/core.clj: -------------------------------------------------------------------------------- 1 | (ns virtuoso.core 2 | (:refer-clojure :exclude [future pmap]) 3 | (:import 4 | clojure.lang.RT 5 | java.util.Iterator 6 | java.util.concurrent.Callable 7 | java.util.concurrent.Executors)) 8 | 9 | 10 | (defn deref-all 11 | " 12 | Dereference all the futures. Return a vector of values. 13 | " 14 | [futs] 15 | (mapv deref futs)) 16 | 17 | 18 | (defmacro with-executor 19 | " 20 | Run a block of code with a new instance of a virtual task executor 21 | bound to the `bind` symbol. The executor gets closed when exiting 22 | the macro. Guarantees that all the submitted tasks will be completed 23 | before closing the executor. 24 | " 25 | [[bind] & body] 26 | `(with-open [~bind (Executors/newVirtualThreadPerTaskExecutor)] 27 | ~@body)) 28 | 29 | 30 | (defmacro future-via 31 | " 32 | Spawn a new future using a previously open executor. 33 | Do not wait for the task to be completed. 34 | " 35 | {:style/indent 1} 36 | [executor & body] 37 | `(.submit ~executor 38 | (reify Callable 39 | (call [this#] 40 | ~@body)))) 41 | 42 | 43 | (defmacro future 44 | " 45 | Spawn a new future using a temporal virtual executor. 46 | Close the pool afterwards. Block until the task is completed. 47 | " 48 | [& body] 49 | `(with-executor [executor#] 50 | (future-via executor# 51 | ~@body))) 52 | 53 | 54 | (defmacro futures 55 | " 56 | Wrap each form into a future bound to a temporal virtual 57 | executor. Return a vector of futures. Close the pool afterwards 58 | blocking until all the tasks are complete. 59 | " 60 | [& forms] 61 | (let [exe-sym (gensym "executor")] 62 | `(with-executor [~exe-sym] 63 | [~@(for [form forms] 64 | `(future-via ~exe-sym 65 | ~form))]))) 66 | 67 | 68 | (defmacro futures! 69 | " 70 | Like `futures` but dereference all the futures. Return 71 | a vector of dereferenced values. Should any task fail, 72 | trigger an exception." 73 | [& forms] 74 | `(deref-all (futures ~@forms))) 75 | 76 | 77 | (defmacro thread 78 | " 79 | Spawn and run a new virtual thread. 80 | " 81 | [& body] 82 | `(.start (Thread/ofVirtual) 83 | (reify Runnable 84 | (run [this#] 85 | ~@body)))) 86 | 87 | 88 | (defn ->iter ^Iterator [coll] 89 | (RT/iter coll)) 90 | 91 | 92 | (defn has-next? [^Iterator iter] 93 | (.hasNext iter)) 94 | 95 | 96 | (defn get-next [^Iterator iter] 97 | (.next iter)) 98 | 99 | 100 | (defn- pmap-multi [func colls] 101 | (let [iters (mapv ->iter colls)] 102 | (with-executor [executor] 103 | (loop [acc! (transient [])] 104 | (if (every? has-next? iters) 105 | (let [xs (mapv get-next iters) 106 | f (future-via executor 107 | (apply func xs))] 108 | (recur (conj! acc! f))) 109 | (persistent! acc!)))))) 110 | 111 | 112 | (defn pmap 113 | " 114 | Like `clojure.core/pmap` but wrap each step into a future 115 | bound to a temporal virtual executor. Return a vector of 116 | futures. Close the pool afterwards which leads to blocking 117 | until all the tasks are completed. 118 | " 119 | 120 | ([func coll] 121 | (with-executor [executor] 122 | (let [iter (->iter coll)] 123 | (loop [acc! (transient [])] 124 | (if (has-next? iter) 125 | (let [x (get-next iter) 126 | f (future-via executor 127 | (func x))] 128 | (recur (conj! acc! f))) 129 | (persistent! acc!)))))) 130 | 131 | ([func coll & colls] 132 | (pmap-multi func (cons coll colls)))) 133 | 134 | 135 | (defn pmap! 136 | 137 | " 138 | Like `pmap` but dereference all the futures. Return a vector 139 | of values. Should any task fail, throw an exception. 140 | " 141 | 142 | ([func coll] 143 | (deref-all (pmap func coll))) 144 | 145 | ([func coll & colls] 146 | (deref-all (pmap-multi func (cons coll colls))))) 147 | 148 | 149 | (defmacro each 150 | " 151 | Run a block of code for each collection's item. The item 152 | is bound to the `item` symbol. Return a vector of futures 153 | each bound to a temporal virtual executor. 154 | " 155 | {:style/indent 1} 156 | [[item coll] & body] 157 | `(pmap (fn [~item] ~@body) ~coll)) 158 | 159 | 160 | (defmacro each! 161 | " 162 | Like `each` but dereference all the futures. Return a vector 163 | of values. Should any task fail, throw an exception. 164 | " 165 | {:style/indent 1} 166 | [[item coll] & body] 167 | `(deref-all (each [~item ~coll] ~@body))) 168 | -------------------------------------------------------------------------------- /doc/v1_api.md: -------------------------------------------------------------------------------- 1 | First, import the library: 2 | 3 | ~~~clojure 4 | (require '[virtuoso.core :as v]) 5 | ~~~ 6 | 7 | **with-executor** 8 | 9 | The `with-executor` wraps a block of code binding a new instance of 10 | `VirtualThreadPerTaskExecutor` to the passed symbol: 11 | 12 | ~~~clojure 13 | (v/with-executor [exe] 14 | (do-this ...) 15 | (do-that ...)) 16 | ~~~ 17 | 18 | Above, the executor is bound to the `exe` symbol. Exiting from the macro will 19 | trigger closing the executor, which, in turn, leads to blocking until all the 20 | tasks sent to it are complete. The `with-executor` macro, although it might be 21 | used on your code, is instead a building material for other macros. 22 | 23 | 24 | **future-via** 25 | 26 | The `future-via` macro spawns a new virtual future through a previously open 27 | executor. You can generate as many futures as you want due to the nature of 28 | virtual threads: there might be millions of them. 29 | 30 | ~~~clojure 31 | (v/with-executor [exe] 32 | (let [f1 (v/future-via exe 33 | (do-this ...)) 34 | f2 (v/future-via exe 35 | (do-that ...))] 36 | [@f1 @f2])) 37 | ~~~ 38 | 39 | Virtual futures give performance gain only when the code they wrap makes 40 | IO. Instead, if you run CPU-based computations in virtual threads, the 41 | performance suffers due to continuations and moving the stack trace from the 42 | stack to the heap and back. 43 | 44 | **futures(!)** 45 | 46 | The `futures` macro takes a series of forms. It spawns a new virtual thread 47 | executor and wraps each form into a future bound to that executor. The result is 48 | a vector of `Future` objects. To obtain values, pass the result through 49 | `(map/mapv deref ...)`: 50 | 51 | ~~~clojure 52 | (let [futs 53 | (v/futures 54 | (io-heavy-task-1 ...) 55 | (io-heavy-task-2 ...) 56 | (io-heavy-task-3 ...))] 57 | (mapv deref futs)) 58 | ~~~ 59 | 60 | Right before you exit the macro, it closes the executor, which leads to blicking 61 | until all the tasks are complete. 62 | 63 | Pay attention that `deref`-ing a failed future leads to throwing an 64 | exception. That's why the macro doesn't dereference the futures for you, as it 65 | doesn't know how to handle errors. But if you don't care about exception 66 | handling, there is a `futures!` macro that does it for you: 67 | 68 | ~~~clojure 69 | (v/futures! 70 | (io-heavy-task-1 ...) 71 | (io-heavy-task-2 ...) 72 | (io-heavy-task-3 ...)) 73 | ~~~ 74 | 75 | The result will be vector of dereferenced values. 76 | 77 | **thread** 78 | 79 | The `thread` macro spawns and starts a new virtual thread using the 80 | `(Thread/ofVirtual)` call. Threads in Java do not return values; they can only 81 | be `join`-ed or interrupted. Use this macro when interested in a `Thread` object 82 | but not the result. 83 | 84 | ~~~clojure 85 | (let [thread1 86 | (v/thread 87 | (some-long-task ...)) 88 | 89 | thread2 90 | (v/thread 91 | (some-long-task ...))] 92 | 93 | (.join thread1) 94 | (.join thread2)) 95 | ~~~ 96 | 97 | **pmap(!)** 98 | 99 | The `pmap` function acts like the standard `clojure.core/pmap`: it takes a 100 | function and a collection (or more collections). It opens a new virtual executor 101 | and submits each calculation step to the executor. The result is a vector of 102 | futures. The function closes the executor afterwards, blocking until all the 103 | tasks are complete. 104 | 105 | ~~~clojure 106 | (let [futs 107 | (v/pmap get-user-from-api [1 2 3])] 108 | (mapv deref futs)) 109 | ~~~ 110 | 111 | Or: 112 | 113 | ~~~clojure 114 | (let [futs 115 | (v/pmap get-some-entity ;; assuming it accepts id and status 116 | [1 2 3] ;; ids 117 | ["active" "pending" "deleted"] ;; statuses 118 | )] 119 | (mapv deref futs)) 120 | ~~~ 121 | 122 | The `pmap!` version of this function dereferences all the results for you with 123 | no exception handling: 124 | 125 | ~~~clojure 126 | (v/pmap! get-user-from-api [1 2 3]) 127 | ;; [{:id 1...}, {:id 2...}, {:id 3...}] 128 | ~~~ 129 | 130 | **each(!)** 131 | 132 | The `each` macro is a wrapper on top of `pmap`. It binds each item from a 133 | collection to a given symbol and submits a code block into a virtual 134 | executor. The result is a vector of futures; exiting the macro closes the 135 | executor. 136 | 137 | ~~~clojure 138 | (let [futs 139 | (v/each [id [1 2 3]] 140 | (log/info...) 141 | (try 142 | (get-entity-by-id id) 143 | (catch Throwable e 144 | (log/error e ...))))] 145 | (is (= [{...}, {...}, {...}] (mapv deref futs)))) 146 | ~~~ 147 | 148 | The `each!` macro acts the same but dereferences all the futures with no error 149 | handling. 150 | -------------------------------------------------------------------------------- /src/virtuoso/v3.clj: -------------------------------------------------------------------------------- 1 | (ns virtuoso.v3 2 | " 3 | A set of functions and macros named after 4 | their clojure.core counterparts but acting 5 | using virtual threads. 6 | " 7 | (:refer-clojure :exclude [future 8 | pmap 9 | map 10 | for 11 | pvalues]) 12 | (:import 13 | (java.lang VirtualThread) 14 | (java.util.concurrent Callable 15 | CompletableFuture 16 | Future 17 | ExecutorService 18 | Executors))) 19 | 20 | 21 | (set! *warn-on-reflection* true) 22 | 23 | 24 | (alias 'cc 'clojure.core) 25 | 26 | 27 | (defmacro with-executor 28 | " 29 | Run a block of code with a new instance of a virtual task executor 30 | bound to the `bind` symbol. The executor gets closed when exiting 31 | the macro. Guarantees that all the submitted tasks are completed 32 | before the executor is closed. 33 | " 34 | [[bind] & body] 35 | `(with-open [~bind (Executors/newVirtualThreadPerTaskExecutor)] 36 | ~@body)) 37 | 38 | 39 | (def binding-conveyor-fn 40 | @(var cc/binding-conveyor-fn)) 41 | 42 | 43 | (defmacro future-via 44 | " 45 | Submit a block of code to the given executor service. 46 | Return a future. 47 | " 48 | {:style/indent 1} 49 | ^Future [[^ExecutorService exe] & body] 50 | `(.submit ~exe 51 | ^Callable 52 | (binding-conveyor-fn 53 | (^{:once true} fn* [] ~@body)))) 54 | 55 | 56 | (defmacro thread 57 | " 58 | Run a block of code in a virtual thread. Return 59 | a running VirtualThread instance. 60 | " 61 | ^VirtualThread [& body] 62 | `(-> (Thread/ofVirtual) 63 | (.name "virtuoso.v3") 64 | (.start 65 | (binding-conveyor-fn 66 | (^{:once true} fn* [] ~@body))))) 67 | 68 | 69 | (defmacro future 70 | " 71 | Run a block of code in a virtual thread. Return 72 | a CompletableFuture that gets completed either 73 | successfully or exceptionally depending on how 74 | the code behaves. 75 | " 76 | ^CompletableFuture [& body] 77 | `(let [future# (new CompletableFuture)] 78 | (thread 79 | (let [[result# e#] 80 | (try 81 | [(do ~@body) nil] 82 | (catch Throwable e# 83 | [nil e#]))] 84 | (if e# 85 | (.completeExceptionally future# e#) 86 | (.complete future# result#)))) 87 | future#)) 88 | 89 | 90 | (defn process-by-one 91 | " 92 | A helper function that accepts a sequence of items 93 | and applies a function lazily one by one. 94 | " 95 | [f coll] 96 | (lazy-seq 97 | (when-let [e (first coll)] 98 | (cons (f e) (process-by-one f (next coll)))))) 99 | 100 | 101 | (defn deref-by-one 102 | " 103 | Deref all items from a collection lazily one by one. 104 | " 105 | [coll] 106 | (process-by-one deref coll)) 107 | 108 | 109 | (defn map 110 | " 111 | Like `map` but each function is running in a virtual executor 112 | producing a future. Return a lazy sequence that derefs futures 113 | when iterating. 114 | " 115 | ([f coll] 116 | (deref-by-one 117 | (with-executor [exe] 118 | (cc/mapv (fn [item] 119 | (future-via [exe] 120 | (f item))) 121 | coll)))) 122 | 123 | ([f coll & colls] 124 | (deref-by-one 125 | (with-executor [exe] 126 | (apply cc/mapv 127 | (fn [& items] 128 | (future-via [exe] 129 | (apply f items))) 130 | coll 131 | colls))))) 132 | 133 | (defn fmap 134 | " 135 | Like `pmap` where each chunk of items is executed in 136 | a dedicated virtual executor. The `n` parameter specifies 137 | the chunk size. Each chunk gets completely finished and 138 | the executor is closed before proceeding to the next chunk. 139 | Return a lazy sequence of futures. 140 | " 141 | ([n f coll] 142 | (lazy-seq 143 | (when-let [chunk (->> coll (take n) seq)] 144 | (concat (with-executor [exe] 145 | (cc/vec 146 | (cc/for [item chunk] 147 | (future-via [exe] 148 | (f item))))) 149 | (fmap n f (drop n coll)))))) 150 | 151 | ([n f coll & colls] 152 | (lazy-seq 153 | (let [chunks 154 | (cons (->> coll (take n) seq) 155 | (cc/for [coll colls] 156 | (->> coll (take n) seq)))] 157 | (when (every? some? chunks) 158 | (concat (with-executor [exe] 159 | (apply cc/mapv 160 | (fn [& args] 161 | (future-via [exe] 162 | (apply f args))) 163 | chunks)) 164 | (apply fmap 165 | n 166 | f 167 | (drop n coll) 168 | (cc/for [coll colls] 169 | (drop n coll))))))))) 170 | 171 | 172 | (defn pmap 173 | " 174 | Like `pmap` but each chunk of items is run in a dedicated 175 | virtual executor. Next chunk is only calculated after the 176 | previous one was done. The `n` parameter specifies the 177 | chunk size. Return a lazy sequence of items deref'fed one 178 | by one. Based on `fmap` (see above). 179 | " 180 | ([n f coll] 181 | (deref-by-one 182 | (fmap n f coll))) 183 | ([n f coll & colls] 184 | (deref-by-one 185 | (apply fmap n f coll colls)))) 186 | 187 | 188 | (defmacro pvalues 189 | " 190 | Run forms in a dedicated virtual executor and close it 191 | afterwards. Return a lazy sequence of deref'ed futures 192 | (by one). 193 | " 194 | [& forms] 195 | (let [exe (gensym "exe")] 196 | `(deref-by-one 197 | (with-executor [~exe] 198 | [~@(cc/for [form forms] 199 | `(future-via [~exe] 200 | ~form))])))) 201 | 202 | 203 | (defmacro for 204 | " 205 | Like `for` but performs all body expressions 206 | in a virtual executor. The executor gets closed 207 | afterwards. Return a lazy sequence of deref'ed 208 | items. 209 | " 210 | [bindings & body] 211 | `(deref-by-one 212 | (with-executor [exe#] 213 | (doall 214 | (cc/for [~@bindings] 215 | (future-via [exe#] 216 | ~@body)))))) 217 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Virtuoso 2 | 3 | [virtual-threads]: https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html 4 | 5 | A small wrapper on top of [virtual threads][virtual-threads] introduced in Java 6 | 21. 7 | 8 | 9 | 10 | - [About](#about) 11 | - [Installation](#installation) 12 | - [V3 API (current)](#v3-api-current) 13 | - [V2 API (deprecated)](#v2-api-deprecated) 14 | - [V1 API (deprecated)](#v1-api-deprecated) 15 | - [Measurements](#measurements) 16 | - [Links and Resources](#links-and-resources) 17 | - [License](#license) 18 | 19 | 20 | 21 | ## About 22 | 23 | The recent release of Java 21 introduced virtual threads to the scene. It's a 24 | nice feature that allows you to run imperative code, such as it was written in 25 | an asynchronous way. This library is a naive attempt to gain something from the 26 | virtual threads. 27 | 28 | ## Installation 29 | 30 | Lein 31 | 32 | ~~~clojure 33 | [com.github.igrishaev/virtuoso "0.1.2"] 34 | ~~~ 35 | 36 | Deps/CLI 37 | 38 | ~~~clojure 39 | com.github.igrishaev/virtuoso {:mvn/version "0.1.2"} 40 | ~~~ 41 | 42 | ## V3 API (current) 43 | 44 | *TL;DR: why making the new API? Because unlike v1 and v2, the third version was 45 | heavily tested in production. It has spotted some weaknesses in v2 but I don't 46 | want to introduce breaking changes. Thus, it's safer to ship a new module (done 47 | right this time, I hope).* 48 | 49 | The `virtuoso.v3` namespace provides a number of functions and macros. Most of 50 | them mimic their counterparts from the `clojure.core` namespace, but are 51 | enforced by virtual threads. 52 | 53 | ~~~clojure 54 | (ns demo 55 | (:require 56 | [virtuoso.v3 :as v])) 57 | ~~~ 58 | 59 | **The `thread` macro** runs a block if code in a virtual thread. You'll get an 60 | instance of `java.lang.VirtualThread`. The thread is started immediately. You 61 | can `.join` it if you want. 62 | 63 | ~~~clojure 64 | (def -t (v/thread 65 | (Thread/sleep 1000) 66 | (+ 1 2))) 67 | 68 | (.join -t) 69 | nil 70 | ~~~ 71 | 72 | Usually, you cannot return a value from a thread (in a normal way). Use this 73 | macro only when you're not interested in a result. 74 | 75 | **The `future` macro** runs a virtual thread similar to the `thread` macro. The 76 | diffenrece is, you'll get an instance of `CompletableFuture` which gets 77 | completed either normally or exceptionally should an exception pops up. The 78 | result can be processed with `deref` or any `future-...` function: 79 | 80 | ~~~clojure 81 | (def -f (v/future 82 | (Thread/sleep 1000) 83 | (+ 1 2))) 84 | 85 | (future? -f) ;; true 86 | (future-done? -f) ;; true 87 | @-f ;; 3 88 | 89 | (def -failed 90 | (v/future (/ 0 0))) 91 | 92 | (deref -failed) 93 | ;; Execution error (ArithmeticException) at ... 94 | ;; Divide by zero 95 | ~~~ 96 | 97 | There are **two macros called `with-executor` and `future-via`** working in 98 | pair. The first macro temporary opens a new virtual `ExecutorService` and binds 99 | it to a certain variable. Pass this executor into the `future-via` macro so the 100 | task is bound to this specific executor. The `with-executor` macro closes the 101 | executor when exiting which guarantees all pending tasks are completed (normally 102 | or exceptionally). 103 | 104 | ~~~clojure 105 | (v/with-executor [exe] 106 | (let [a (v/future-via [exe] 107 | (Thread/sleep 1000) 108 | (+ 1 2)) 109 | b (v/future-via [exe] 110 | (Thread/sleep 1000) 111 | (+ 4 5))] 112 | (+ @a @b))) 113 | 114 | ;; 12 115 | ~~~ 116 | 117 | **The `map` function** acts like `clojure.core/map` does but: 118 | 119 | - for every item, the target function is called in a virtual thread; 120 | - all items are processed immediately without chunking; 121 | - therefore, the amount of virtual threads is unlimited; 122 | - the result is a lazy seq of `deref`-ed items processed one by one. 123 | 124 | ~~~clojure 125 | (def -result 126 | (v/map (fn [a b] 127 | (Thread/sleep 100) 128 | (+ a b)) 129 | (range 10) 130 | (range 10))) 131 | 132 | (0 2 4 6 8 10 12 14 16 18) 133 | ~~~ 134 | 135 | Pay attention: if you perform HTTP calls or file IO for each item, apparently 136 | you might hit the global 1024 `ulimit` constraint. Java will throw an exception 137 | saying "too many open connections" or something. For such cases, it's better to 138 | use the `pmap` function that acts through chunks (see below). 139 | 140 | **The `pmap` function** is similar to `clojure.core/pmap` as it splits incoming 141 | data on chunks. Each chunk of items is served within a dedicated virtual 142 | executor. The next chunk won't start until the current one is complete. The 143 | leading `n` parameter determines the chunk size. While working with HTTP calls, 144 | that's ok to pass 512 or something similar (unless you have a custom `ulimit` 145 | alue set). The function returns a lazy sequence of `deref`-ed values. 146 | 147 | ~~~clojure 148 | (def -result 149 | (v/pmap 512 150 | (fn [a b] 151 | (Thread/sleep 1000) 152 | (+ a b)) 153 | (range 1000) 154 | (range 1000))) 155 | 156 | (count -result) ;; takes ~2 seconds 157 | 1000 158 | ~~~ 159 | 160 | Better example: download 50k files from S3 by chunks of 1000: 161 | 162 | ~~~clojure 163 | (def -result 164 | (v/pmap 1000 165 | (fn [url] 166 | (-> url 167 | (client/get {:as :stream}) 168 | (:body) 169 | (process-input-stream))) 170 | (get-urls-to-fetch...))) 171 | ~~~ 172 | 173 | **The `fmap` function** is a low-level function which `pmap` is based on. It 174 | returns a chunked sequence of futures. It's up to you how to handle them: 175 | 176 | ~~~clojure 177 | (def -futs 178 | (v/fmap 512 179 | (fn [a b] 180 | (Thread/sleep 100) 181 | (+ a b)) 182 | (range 1000) 183 | (range 1000))) 184 | 185 | (take 5 -futs) 186 | 187 | (#object[ThreadBoundFuture ...[Completed normally]"] 188 | #object[ThreadBoundFuture ...[Completed normally]"] 189 | #object[ThreadBoundFuture ...[Completed normally]"] 190 | #object[ThreadBoundFuture ...[Completed normally]"] 191 | #object[ThreadBoundFuture ...[Completed normally]"]) 192 | ~~~ 193 | 194 | **The `pvalues` macro** acts like `clojure.core/pvalues` forms are executed 195 | within a virtual executor which gets closed afterwards. The result a lazy 196 | sequence which iterating, `deref`s futures. 197 | 198 | ~~~clojure 199 | (v/pvalues 200 | (+ 1 2) 201 | (let [a 3 b 4] 202 | (Thread/sleep 100) 203 | (+ a b)) 204 | (* 5 6)) 205 | 206 | ;; (3 7 30) 207 | ~~~ 208 | 209 | **The `for` macro** mimics the standard `clojure.core/for` but each body is run 210 | in a virtual future. These futures are global meaning they are not bound to a 211 | dedicated virtual executor. The result is a sequence of `deref`-ed values: 212 | 213 | ~~~clojure 214 | (v/for [a [1 2 3] 215 | b [:a :b :c] 216 | :when (not= [a b] [2 :b]) 217 | :let [c (* a a)]] 218 | {:c c :b b}) 219 | 220 | ({:c 1, :b :a} 221 | {:c 1, :b :b} 222 | {:c 1, :b :c} 223 | {:c 4, :b :a} 224 | {:c 4, :b :c} 225 | {:c 9, :b :a} 226 | {:c 9, :b :b} 227 | {:c 9, :b :c}) 228 | ~~~ 229 | 230 | ## V2 API (deprecated) 231 | 232 | Moved to a [legacy V2 doc file](doc/v2_api.md). 233 | 234 | ## V1 API (deprecated) 235 | 236 | Moved to a [legacy V1 doc file](doc/v1_api.md). 237 | 238 | ## Measurements 239 | 240 | There is a development `dev/src/bench.clj` module with some benchmarks. Imagine 241 | we want to download 100 large files using `map`, `pmap` and virtual 242 | threads. Before we do this, let's mimic real environment as follows: 243 | 244 | - install and run nginx; 245 | - put a large binary file into the static folder; 246 | - for that file, limit the throughput: 247 | 248 | ~~~text 249 | server { 250 | listen 8080; 251 | server_name localhost; 252 | ... 253 | location /hugefile.bin { 254 | root html; 255 | limit_rate 500k; 256 | } 257 | } 258 | ~~~ 259 | 260 | Now when you `curl` that file, it will be v-v-very slow. 261 | 262 | The idea behind this trick is to mimic **real** IO expectation. Without limiting 263 | throughput, the standard `map` outperforms both `pmap` and virtual threads just 264 | because networking is too fast. 265 | 266 | Now that the file is served in a slow manner, prepare a function that downloads 267 | it into nowhere: 268 | 269 | ~~~clojure 270 | (def URL "http://127.0.0.1:8080/hugefile.bin") 271 | 272 | (defn download [i] 273 | (with-open [in ^java.io.InputStream 274 | (:body (client/get URL {:as :stream})) 275 | out 276 | (java.io.OutputStream/nullOutputStream)] 277 | (.transferTo in out))) 278 | ~~~ 279 | 280 | Let's download it 100 times in different ways: 281 | 282 | ~~~clojure 283 | (time 284 | (count 285 | (map download SEQ))) 286 | ;; Elapsed time: 1102802.057709 msecs 287 | 288 | (time 289 | (count 290 | (pmap download SEQ))) 291 | ;; Elapsed time: 44213.30375 msecs 292 | 293 | (time 294 | (count 295 | (v3/map download SEQ))) 296 | ;; Elapsed time: 11124.417959 msecs 297 | 298 | (time 299 | (count 300 | (v3/pmap 512 download SEQ))) 301 | ;; Elapsed time: 11090.514792 msecs 302 | ~~~ 303 | 304 | The standard `map` function lasts forever because it downloads files one by 305 | one. If the file size is 6 megabyes and the rate limit is 500 kbs, it will take 306 | 12 secods to fetch it. Therefore, downloading 100 files takes 12 sec * 100 = 307 | 1200 seconds = 20 minutes. 308 | 309 | The `pmap` function behaves better of course as it parallels jobs. My laptop has 310 | got 12 CPUs meaning that, theoretically, it can download 14 files simultaneously 311 | (pmap window size = CPU + 2). Above, downloading 100 files takes 44 seconds. 312 | 313 | Now, the two `map` functions powered with virtual threads. One soon as one 314 | virtual thread emits a blocking IO call, its stack trace replaced with a stack 315 | trace of another thread that has just woken up from blocking IO. In our case, 316 | all 100 files get downloaded in parallel, and the final time is 12 seconds. It 317 | took as longs as to download a single file -- but we got 100 files. 318 | 319 | A quick example of breaking `ulimit` constraint. 100 (files) is less than 1024 320 | (default ulimit) so we're fine. Now imagine we'd like to download 2000 files 321 | using virtual thread. This is what will happen: 322 | 323 | ~~~clojure 324 | (time 325 | (count 326 | (v3/map download (range 2000)))) 327 | 328 | INFO: I/O exception (java.net.SocketException) caught 329 | when processing request to {}->http://127.0.0.1:8080: Connection reset 330 | ... org.apache.http.impl.execchain.RetryExec execute 331 | INFO: Retrying request to {}->http://127.0.0.1:8080 332 | ~~~ 333 | 334 | Handling 2000 parallel connections is too many. Other HTTP clients may fail with 335 | "too many open connections" error. The right approach would be to use `v/pmap` 336 | with a smaller window size: 337 | 338 | ~~~clojure 339 | (time 340 | (count 341 | (v3/pmap 512 download (range 2000)))) 342 | ;; Elapsed time: 44684.907583 msecs 343 | ~~~ 344 | 345 | 2000 files in 45 seconds! It means, every second were downloading about 44 346 | files. 347 | 348 | ## Links and Resources 349 | 350 | The following links helped me a lot to dive into virtual threads, and I highly 351 | recommend reading and watching them: 352 | 353 | - [Virtual Threads | Oracle Help Center](https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html) 354 | - [Java 21 new feature: Virtual Threads #RoadTo21](https://www.youtube.com/watch?v=5E0LU85EnTI) 355 | 356 | ## License 357 | 358 | ~~~ 359 | ©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©© 360 | Ivan Grishaev, 2023. © UNLICENSE © 361 | ©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©© 362 | ~~~ 363 | --------------------------------------------------------------------------------