-
Notifications
You must be signed in to change notification settings - Fork 164
Runtime async
Starting in .NET 11, the CoreCLR runtime now has direct support for asynchronous programming.
In C#, a method (named, anonymous, lambda expression) can be marked with the async keyword; it should return a task type (Task, ValueTask, Task<TResult>, or ValueTask<TResult>, though are exceptions to this). A method so marked can use the await operator on an expression. At that point, the method will yield control of its thread back to its caller, to be resumed (likely on a different thread) after the awaited expression has completed.
Prior to .NET 11, this mechanism was implemented by special code generation in the C# compiler. An async method would be compiled into a finite-state machine that was set up to suspend the computation at an await and store the local context, then restore the local context and resume computation. (For the curious, this is very similar to the rewriting done in Clojure in the core.async library.)
Starting in .NET 11, the CoreCLR has direct runtime support. Minimal effort by the compiler -- setting a metadata flag on a method, translating awaits into calls to specially designated methods -- identifies suspension points. The runtime knows how to suspend and restore state on behalf of the method.
There are multiple benefits to this approach, including improved efficiency, more readable stack traces on exceptions, and less work for the compiler (and the compiler developer).
Starting in version 1.12.3-alpha6, ClojureCLR offers direct support for asynchronous programming. The compiler has been modified to provide CLR runtime with the needed information. A new library, clojure.clr.async.task.alpha, provides utility functions for this capability.
In the code samples below, we assume an require of [clojure.clr.async.task.alpha :as t] has been done.
Any function can be designated asynchronous by attaching the flag :async. This can be done on an anonymous function. It is supported by defn. Thus:
(defn ^:async whoopee-i-am-async [ ...args... ]
... body ...)or
`:async (fn [...args...] ...body...)A function marked as :async will be given the type hint System.Threading.Tasks.Task[Object]. It will be compiled not to run directly but to return a task object that will run the body when started.
There is a utility macro to define an async context for piece of code.
(t/async ...body... ) ;; expands to ^:async (fn [] ... body... )This will allow t/await calls in the body without the entire containing function needing to be async.
The t/await macro signals a suspension point to run a subordinate task. Consider:
(defn ^:async shout-it-out [infile outfile]
(let [content (t/await (File/ReadAllTextAsync infile CancellationToken/None))
capitalized (.ToUpper content)]
(t/await (File/WriteAllTextAsync ^String outfile capitalized CancellationToken/None))
"I'm done yelling."))This function awaits on calls to the asynchronous I/O methods (in System.IO.File) ReadAllTextAsync and WriteAllTextAsync. The use of t/await requires:
- it occurs in an
:asynccontext - it is called on an expression that has a known task type:
Task,ValueTask,Task<TResult>, orValueTask<TResult>
t/await suspends the current computation, returning control of the thread to the caller, and starts the subcomputation (if not already started or completed). Upon completion of the subcomputation, it retrieves the result of the computation. (In the case of a non-generic Task or ValueTask, it returns nil).
You can use the t/result function to retrieve the result of a task. It will start the task if it not yet started or completed and wait for the result.
Note that if it has to start the task, it does not yield its thread, i.e., it blocks. Better to use t/await in most circumstances.
There are several utility functions providing access to some simple Task methods.
(t/->task 42) ;; creates a Task<Object> that returns 42 when run
(t/completed-task) ;; creates a Task that is already completed
(t/delay-task 3000) ;; creates a Task that delays for 3000 millisecondsYou can create a task from any no-arg method using t/run:
(t/run (fn [] (+ 1 2 3)))) ;; returns a task
(defn now [] DateTime/Now)
(t/run now) ;; returns a taskAnd there is a test for being a task:
(t/task? (t/run now)) ;; => trueYou can do wait-for-one and wait-for-all operations on a group of tasks. You can either just run the tasks or ask for their value(s).
| Function | Description |
|---|---|
(t/wait-all tasks) |
wait for all the tasks to complete; return nil
|
(t/wait-any tasks) |
start all tasks, return the first task to complete |
(t/wait-all-results tasks) |
wait for all the tasks to complete, return a lazy sequence of their results |
(t/wait-any-result tasks) |
return the result of the first task to complete |
Each of these has variants that accept a timeout (any number , converted to an integer representing milliseconds, or a TimeSpan)
and a timeout plus a cancellation token.
;; A little dummy function to delay and then return a value.
(defn ^:async delayed-value [msecs val]
(t/await (t/delay-task msecs))
val)
;; create an vector of tasks
(defn create-some-tasks []
[(delayed-value 2000 2000)
(delayed-value 4000 4000)
(delayed-value 6000 6000)])
(t/wait-all-results (create-some-tasks)) ;; => (2000 4000 6000)
(t/wait-any-result (create-some-tasks)) ;; => 2000 (most likely)
(t/wait-all-results (create-some-tasks 500)) ;; => nil (times out)
(t/wait-any-result (create-some-tasks) 500) ;; => nil (times out)