{"id":19293950,"url":"https://github.com/igrishaev/virtuoso","last_synced_at":"2025-10-30T16:21:29.609Z","repository":{"id":200009500,"uuid":"704431279","full_name":"igrishaev/virtuoso","owner":"igrishaev","description":"A number of trivial wrappers on top of virtual threads","archived":false,"fork":false,"pushed_at":"2025-04-27T16:46:19.000Z","size":75,"stargazers_count":28,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-07-05T07:05:49.545Z","etag":null,"topics":["clojure","java","threads","virtual"],"latest_commit_sha":null,"homepage":"https://github.com/igrishaev/virtuoso","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"unlicense","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/igrishaev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2023-10-13T08:42:15.000Z","updated_at":"2025-06-19T17:17:53.000Z","dependencies_parsed_at":null,"dependency_job_id":"f82301d0-1127-43e0-a9ea-e67c2e1137e3","html_url":"https://github.com/igrishaev/virtuoso","commit_stats":null,"previous_names":["igrishaev/virtuoso"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/igrishaev/virtuoso","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igrishaev%2Fvirtuoso","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igrishaev%2Fvirtuoso/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igrishaev%2Fvirtuoso/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igrishaev%2Fvirtuoso/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/igrishaev","download_url":"https://codeload.github.com/igrishaev/virtuoso/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igrishaev%2Fvirtuoso/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":267911428,"owners_count":24164460,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-07-30T02:00:09.044Z","response_time":70,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clojure","java","threads","virtual"],"created_at":"2024-11-09T22:36:39.977Z","updated_at":"2025-10-30T16:21:24.556Z","avatar_url":"https://github.com/igrishaev.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Virtuoso\n\n[virtual-threads]: https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html\n\nA small wrapper on top of [virtual threads][virtual-threads] introduced in Java\n21.\n\n\u003c!-- toc --\u003e\n\n- [About](#about)\n- [Installation](#installation)\n- [V2 API (new)](#v2-api-new)\n- [V1 API (old)](#v1-api-old)\n- [Measurements](#measurements)\n- [Links and Resources](#links-and-resources)\n- [License](#license)\n\n\u003c!-- tocstop --\u003e\n\n## About\n\nThe recent release of Java 21 introduced virtual threads to the scene. It's a\nnice feature that allows you to run imperative code, such as it was written in\nan asynchronous way. This library is a naive attempt to gain something from the\nvirtual threads.\n\n## Installation\n\nLein\n\n~~~clojure\n[com.github.igrishaev/virtuoso \"0.1.1\"]\n~~~\n\nDeps/CLI\n\n~~~clojure\n{com.github.igrishaev/virtuoso {:mvn/version \"0.1.1\"}}\n~~~\n\n## V2 API (new)\n\nThe new namespace called `virtuoso.v2` brings some functions and macros that\nalready present in the `virtuoso.core` namespace. To prevent things from\nbreaking, I decided to put them into a new namespace. Personally I find `v2`\nmore convenient for usage but this is up to you.\n\nThe `v2` API provides utilities named after their Clojure counterparts,\ne.g. `map`, `pvalues` and so on. But under the hood, they use a global virtual\nexecutor. This executor gets closed on JVM shutdown.\n\nImport the namespace:\n\n~~~clojure\n(ns some.project\n  (:require\n   [virtuoso.v2 :as v]))\n~~~\n\n**The `future` macro** acts like a regular future but is served using a global\nvirtual executor service:\n\n~~~clojure\n(def -fut\n  (v/future\n    (let [a 1 b 2] (+ a b))))\n\n-fut\n#object[java.util... 0x1f3aa45f \"java.util.concurrent...@1f3aa45f[Completed normally]\"]\n\n@-fut\n3\n~~~\n\nThe macro accepts an arbitrary block of code that gets executed into a future.\n\n**The `pvalues` macro** accepts a number of forms and runs each into a\nfuture. The result is a lazy sequence of dereferenced values:\n\n~~~clojure\n(def -items\n    (v/pvalues (+ 3 4)\n               (Thread/sleep 1000)\n               (let [a 3]\n                 (* a a))))\n\n-items\n(7 nil 9)\n~~~\n\nPay attention all the futures get run immediately but the process of\ndereferencing is lazy. They get `deref`-ed one by one as you iterate the\nresult. Thus, you can easily spot an exception should it pop up:\n\n~~~clojure\n(def -items\n    (v/pvalues (/ 3 2)\n               (/ 3 1)\n               (/ 3 0)))\n\n(first -items)\n3/2\n\n(second -items)\n3\n\n(last -items) ;; only now throws\n;; Execution error (ArithmeticException) at virtuoso.v2/fn...\n;; Divide by zero\n~~~\n\n**The `map` function** is similar to the standard `map` but performs each\nfunction call in a virtual future. All the steps are fired without chunking. The\nresult is a lazy sequence of dereferenced values.\n\n~~~clojure\n(def -items\n    (v/map (fn [x] (/ 10 x)) [5 4 3 2 1 0]))\n\n;; don't touch the last item\n(take 5 -items)\n;; (2 5/2 10/3 5 10)\n\n;; touch it\n(last -items)\n;; Execution error (ArithmeticException) at virtuoso.v2/fn...\n;; Divide by zero\n~~~\n\n**The `for`** macro acts like `for` but wraps each body expression into a\nfuture. All the futures are fired at once with no chunking. The result is a lazy\nsequence of dereferenced values. You can use `:let`, `:when`, and other nested\nforms:\n\n~~~clojure\n(def -items\n    (v/for [a [:a :b :c]\n        b [1 2 3 4 5]\n        :when (and (not= a :b) (not= b 3))]\n    {:a a :b b}))\n\n-items\n({:a :a, :b 1}\n {:a :a, :b 2}\n {:a :a, :b 4}\n {:a :a, :b 5}\n {:a :c, :b 1}\n {:a :c, :b 2}\n {:a :c, :b 4}\n {:a :c, :b 5})\n~~~\n\nThe `thread` macro just creates and starts a new virtual thread out from a block\nof code. Useful if you'd like to deal with `Thead` instances:\n\n~~~clojure\n(let [t1 (v/thread (Thread/sleep 1000) (+ 1 2))\n      t2 (v/thread (Thread/sleep 2000) (* 3 2))]\n    (.join t1)\n    (.join t2)\n    (println \"both are done\"))\n~~~\n\nThe `v2` namespace, when loaded, adds its own JVM shutdown hook as follows:\n\n~~~clojure\n(defonce ^Thread -shutdown-hook\n  (new Thread (fn []\n                (.close -EXECUTOR))))\n\n(defonce ___\n  (-\u003e (Runtime/getRuntime)\n      (.addShutdownHook -shutdown-hook)))\n~~~\n\nThe global executor will be closed on JVM shutdown.\n\n## V1 API (old)\n\nFirst, import the library:\n\n~~~clojure\n(require '[virtuoso.core :as v])\n~~~\n\n**with-executor**\n\nThe `with-executor` wraps a block of code binding a new instance of\n`VirtualThreadPerTaskExecutor` to the passed symbol:\n\n~~~clojure\n(v/with-executor [exe]\n  (do-this ...)\n  (do-that ...))\n~~~\n\nAbove, the executor is bound to the `exe` symbol. Exiting from the macro will\ntrigger closing the executor, which, in turn, leads to blocking until all the\ntasks sent to it are complete. The `with-executor` macro, although it might be\nused on your code, is instead a building material for other macros.\n\n\n**future-via**\n\nThe `future-via` macro spawns a new virtual future through a previously open\nexecutor. You can generate as many futures as you want due to the nature of\nvirtual threads: there might be millions of them.\n\n~~~clojure\n(v/with-executor [exe]\n  (let [f1 (v/future-via exe\n             (do-this ...))\n        f2 (v/future-via exe\n             (do-that ...))]\n    [@f1 @f2]))\n~~~\n\nVirtual futures give performance gain only when the code they wrap makes\nIO. Instead, if you run CPU-based computations in virtual threads, the\nperformance suffers due to continuations and moving the stack trace from the\nstack to the heap and back.\n\n**futures(!)**\n\nThe `futures` macro takes a series of forms. It spawns a new virtual thread\nexecutor and wraps each form into a future bound to that executor. The result is\na vector of `Future` objects. To obtain values, pass the result through\n`(map/mapv deref ...)`:\n\n~~~clojure\n(let [futs\n      (v/futures\n       (io-heavy-task-1 ...)\n       (io-heavy-task-2 ...)\n       (io-heavy-task-3 ...))]\n  (mapv deref futs))\n~~~\n\nRight before you exit the macro, it closes the executor, which leads to blicking\nuntil all the tasks are complete.\n\nPay attention that `deref`-ing a failed future leads to throwing an\nexception. That's why the macro doesn't dereference the futures for you, as it\ndoesn't know how to handle errors. But if you don't care about exception\nhandling, there is a `futures!` macro that does it for you:\n\n~~~clojure\n(v/futures!\n  (io-heavy-task-1 ...)\n  (io-heavy-task-2 ...)\n  (io-heavy-task-3 ...))\n~~~\n\nThe result will be vector of dereferenced values.\n\n**thread**\n\nThe `thread` macro spawns and starts a new virtual thread using the\n`(Thread/ofVirtual)` call. Threads in Java do not return values; they can only\nbe `join`-ed or interrupted. Use this macro when interested in a `Thread` object\nbut not the result.\n\n~~~clojure\n(let [thread1\n      (v/thread\n        (some-long-task ...))\n\n      thread2\n      (v/thread\n        (some-long-task ...))]\n\n  (.join thread1)\n  (.join thread2))\n~~~\n\n**pmap(!)**\n\nThe `pmap` function acts like the standard `clojure.core/pmap`: it takes a\nfunction and a collection (or more collections). It opens a new virtual executor\nand submits each calculation step to the executor. The result is a vector of\nfutures. The function closes the executor afterwards, blocking until all the\ntasks are complete.\n\n~~~clojure\n(let [futs\n      (v/pmap get-user-from-api [1 2 3])]\n  (mapv deref futs))\n~~~\n\nOr:\n\n~~~clojure\n(let [futs\n      (v/pmap get-some-entity                ;; assuming it accepts id and status\n              [1 2 3]                        ;; ids\n              [\"active\" \"pending\" \"deleted\"] ;; statuses\n              )]\n  (mapv deref futs))\n~~~\n\nThe `pmap!` version of this function dereferences all the results for you with\nno exception handling:\n\n~~~clojure\n(v/pmap! get-user-from-api [1 2 3])\n;; [{:id 1...}, {:id 2...}, {:id 3...}]\n~~~\n\n**each(!)**\n\nThe `each` macro is a wrapper on top of `pmap`. It binds each item from a\ncollection to a given symbol and submits a code block into a virtual\nexecutor. The result is a vector of futures; exiting the macro closes the\nexecutor.\n\n~~~clojure\n(let [futs\n      (v/each [id [1 2 3]]\n        (log/info...)\n        (try\n          (get-entity-by-id id)\n          (catch Throwable e\n            (log/error e ...))))]\n  (is (= [{...}, {...}, {...}] (mapv deref futs))))\n~~~\n\nThe `each!` macro acts the same but dereferences all the futures with no error handling.\n\n## Measurements\n\nThere is a development `dev/src/bench.clj` file with some trivial\nmeasurements. Imagine you want to download 100 of URLs. You can do it\nsequentially with `mapv`, semi-parallel with `pmap`, and fully parallel with\n`pmap` from this library. Here are the timings made on my machine:\n\n~~~clojure\n(time\n (count\n  (map download URLS)))\n\"Elapsed time: 45846.601717 msecs\"\n\n(time\n (count\n  (pmap download URLS)))\n\"Elapsed time: 3343.254302 msecs\"\n\n(time\n (count\n  (v/pmap! download URLS)))\n\"Elapsed time: 1452.514165 msecs\"\n~~~\n\n45, 3.3, and 1.4 seconds favour the virtual threads approach.\n\n## Links and Resources\n\nThe following links helped me a lot to dive into virtual threads, and I highly\nrecommend reading and watching them:\n\n- [Virtual Threads | Oracle Help Center](https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html)\n- [Java 21 new feature: Virtual Threads #RoadTo21](https://www.youtube.com/watch?v=5E0LU85EnTI)\n\n## License\n\n~~~\n©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©\nIvan Grishaev, 2023. © UNLICENSE ©\n©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©\n~~~\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Figrishaev%2Fvirtuoso","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Figrishaev%2Fvirtuoso","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Figrishaev%2Fvirtuoso/lists"}