Skip to content

Instantly share code, notes, and snippets.

@mrange
Last active August 2, 2023 16:43
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mrange/ac7a57677105b76956e6e126ed223bd8 to your computer and use it in GitHub Desktop.
Save mrange/ac7a57677105b76956e6e126ed223bd8 to your computer and use it in GitHub Desktop.

Thinking about Async/Await

A couple of weeks ago I listened to .Net Rocks! #1433 about Visual Studio 17 with Kathleen Dollard. A very good episode interest but with stuck with me was Kathleen's comment that after all these years she sees very smart people still struggle with async. This is my and others experience as well.

A few years ago I wrote a blog post on why I thought async in C# isn't that great. I took it down a bit later even though it was popular it was also divisive and my understanding of async had grown which made me want to rewrite the whole piece into something better and less divisive.

I used to think that the implicit coupling in async to the SynchronizationContext is a major problem for async but today I think that it's a minor problem for most developers. The reason is that if you are writing ASP.NET you will always have the ASP.NET SynchronizationContext and that gives particular but consistent async behavior. If you are writing a console you will always have the default SynchronizationContext that gives you a different but consistent async behavior. Only library developers will have to write SynchronizationContext agnostic code which is why one often see task.ConfigureAwait(false) in libraries.

Today I think the major problem comes from a lack of understanding that there's no safe way for a function to run async synchronously. This is made worse because the async Task exposes unsafe methods like Task.Result.

async, subroutines and coroutines

async is another word for coroutine, a term that was coined in 1958 by Donald Knuth and Melvin Conway. Simula (1960s) and Modula-2 (1980s) were early examples of languages with support for coroutines but async popularized coroutines as it was added to a mainstream language, C#, and the value add of coroutines is more relevant to developers in 2000s than in 1960s.

It was Donald Knuth that said that "Subroutines are special cases of ... coroutines".

Subroutines also known as methods or functions are one of our fundamental software building blocks. We invoke a subroutine in order to do some work and it will eventually return a value.

A coroutine is a subroutine in that we can invoke a coroutine and it will eventually return a value. A subroutine is not a coroutine because a coroutine can also yield and resume.

Operations Subroutine Coroutine Comment
Invoke x x
Return x x
Yield x Pauses the coroutine while waiting for something else like I/O
Resume x Resumes the coroutine after data is available

This leads to a very important conclusion. A subroutine can not run a coroutine synchronously because the coroutine might wish to yield and that isn't supported by a subroutine.

Instead, as Kathleen says, you have to use coroutines all the way to the top of the stack.

Unfortunately, async in .NET uses Task which supports .Result and .RunSynchronously.

Why is async good?

So if async is causing problems then why should we bother?

I think there are two major use cases for async

The C10K problem is handling concurrently 10,000 connections to a server.

Let's assume that each of these connection will at least do one database lookup.

"Classical" servers accept an incoming connection through a socket and spin up a new thread for each new connection. This means that for the C10K problem we will have 10,000 threads. Each thread need roughly 1 meg of continous memory for the stack ==> 10 Gig memory for stack space alone. In addition the OS will spend some time on thread switching and scheduling 10,000 threads.

If one think that each connection needs a certain workflow executed in the "classical" solution there's a tight coupling between what executes the code ie the thread and the work flow itself.

With async or coroutines there's a loose coupling between the executing thread and the work flow meaning a we can have 100 threads serving 10,000 work flows which will typically improve performance and reduce memory footprint.

One can even go the Node.js path and have single thread servicing all work flows eliminating many problems of concurrency through preemptive thread scheduling.

The thread affine event driven GUI model

A major challenge in software engineering is how to implement a good application with a Graphical User Interface (GUI).

It's a challenge because a GUI might several multiple workflows running that are initiated by user events. These workflows might need be paused while waiting for data. We want the GUI to always be responsive which often implying threads. In addition, most GUI frameworks are thread affine meaning that the GUI objects might only be used by the GUI thread creating the issue that we need to switch back to the GUI thread in order to updates to the GUI from our background threads.

In fact, it's such a challenge that it seems that most strong developers gives up on GUI and focuses on the easy problem; back-end.

async tries to mitigate problem by making sure that when the coroutine is resumed it's executed by the GUI thread. This means that we have the benefit of not blocking the GUI while awaiting on the data but we can freely access the GUI objects because we are always executing in the correct thread with the help of the implicit SynchronizationContext.

Is Task an overworked MVP?

Task helps us with:

  1. Parallelism as we can create multiple Task backed by User Mode Threads that are executed by the different CPU cores in order to complete a CPU intensive task ASAP
  2. Long running I/O background tasks
  3. Avoiding spinning too many threads while servicing a lot of connections
  4. Making sure we get back to the right thread

All of this is related to concurrency but different aspect of concurrency. Perhaps we should have a different API for coroutines as some aspects of Task are useful and safe for say parallism like Task.Result but fundamentally incompatible with coroutines.

What is a sensible coroutine abstraction?

I think it's very important that the coroutine API must only allow safe invocations from subroutines. As a coroutine can yield but a subroutine can't this means that the invocation from a subroutine will always report back the answer in callback.

We don't want to compose coroutines using callback hell but when invoking a coroutine from a subroutine we have to rely on callbacks.

I think the C10K problem and the event driven GUI problem are two distinct problems that shouldn't be solved with a single abstraction despite being superflous similar. I will focus on the C10K problem as I think that is the problem that brings the most value.

The fundamental coroutine abstraction will make or break everything the coroutine API. We should learn from prior art so let's look at some existing coroutine abstraction:

Task

Task is big (~200 methods/properties) in order to support multiple scenarios so I filtered the API heavily into what I think is the essential Task API for supporting async.

public interface INotifyCompletion
{
  void OnCompleted(Action continuation);
}

public interface ICriticalNotifyCompletion : INotifyCompletion
{
  void UnsafeOnCompleted(Action continuation);
}

public struct TaskAwaiter<TResult> : ICriticalNotifyCompletion, INotifyCompletion
{
  public bool IsCompleted { get; }

  public TResult GetResult();

  public void OnCompleted(Action continuation);

  public void UnsafeOnCompleted(Action continuation);
}

public class Task : IAsyncResult, IDisposable
{
  public ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext);

  public void Dispose();

  public TaskAwaiter GetAwaiter();

  public void RunSynchronously(TaskScheduler scheduler);

  public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken);
}

public class Task<TResult> : Task
{
  public TResult Result { get; }

  public ConfiguredTaskAwaitable<TResult> ConfigureAwait(bool continueOnCapturedContext);

  public TaskAwaiter<TResult> GetAwaiter();
}

One could make the argument that RunSynchronously, Wait and Result aren't essential for Task async API as they should never be used. However, it's a common mistake to use them that they are essential defects of the Task async API.

When you use await task in C# what happens is roughly this:

// Get the awaiter object
var awaiter = task.GetAwaiter ();
// Is the result already there??
if (awaiter.IsCompleted)
{
  // Ok get the result, if the task failed or
  //  was cancelled this will throw that exception
  var result = awaiter.GetResult ();
  // Let's do some stuff with result
}
else
{
  awaiter.UnsafeOnCompleted (() =>
  {
    // Ok get the result, if the task failed or
    //  was cancelled this will throw that exception
    var result = awaiter.GetResult ();
    // Let's do some stuff with result
  });
}

What this means is that when async needs to yield (awaited.IsCompleted is false) it registers a callback with the awaiter object and then let the thread continue do something else. When the task is done the callback will be called, resume, and the async functions continue to execute until it's done or it needs ot yield again.

Why doesn't async use the ContinueWith API as there seems to be an overlap? Don't know! If we turn to the documentation it says:

This API supports the product infrastructure and is not intended to be used directly from your code.

What is the difference between UnsafeOnCompletedand `OnCompleted?

UnsafeOnCompleted

Schedules the continuation action for the asynchronous task that is associated with this awaiter. This API supports the product infrastructure and is not intended to be used directly from your code.

OnCompleted

Sets the action to perform when the TaskAwaiter object stops waiting for the asynchronous task to complete. This API supports the product infrastructure and is not intended to be used directly from your code.

My personal opinion on this is that I don't like that public API that is used by async, a core feature of C#, isn't documented fully. It makes it very hard for developers to know exactly what is happening and instead we learn through experimentation and anecdotes.

What about the ConfigureAwait used by many libraries in order to be SyncronizationContext agnostic?

Configures an awaiter used to await this Task. true to attempt to marshal the continuation back to the original context captured; otherwise, false.

Ok, so true means use the captured context, this is the default behavior, but what does otherwise mean? Does it use the default SyncronizationContext? I don't know.

Observations

  1. The Task API is in my opinion overloaded in order to support a vast array of different scenarios related to concurrency but not all of the related to async.
  2. The async API is under documented and shouldn't be used directly, most likely because .NET team doesn't want us to take dependency directly on the API to give them some flexibility in evoling the API. My guess is that they don't get that much flexibility as the new API still needs to be ABI compatible and this also means that a critical async API doesn't help us understand how async works.
  3. I am missing a detailed technical specification on how async works. "It just works" don't do it for me.
  4. Each time we invoke an async function we get a Task object back that we will probably register a callback onto through the TaskAwaiter object. That means we are at least making two heap allocations.
  5. There's an implicit coupling to SynchronizationContext which causes confusion in that the async will behave different depending on what SynchronizationContext is used.

I would like an async API that is

  1. Focused on coroutines.
  2. Well documented.
  3. Reduces GC pressure.

Async (F#)

type cont<'T> = ('T -> FakeUnitValue)
type econt = (ExceptionDispatchInfo -> FakeUnitValue)
type ccont = (OperationCanceledException -> FakeUnitValue)

type AsyncParamsAux =
    { token : CancellationToken;
      econt : econt;
      ccont : ccont;
      trampolineHolder : TrampolineHolder
    }

type AsyncParams<'T> =
    { cont : cont<'T>
      aux : AsyncParamsAux
    }

type Async<'T> =
    P of (AsyncParams<'T> -> FakeUnitValue)

Hopac (F#)

type Handler = class
  abstract DoHandle: byref<Worker> * exn -> unit
end

type Work = class
  inherit Handler
  val mutable Next: Work
  abstract DoWork: byref<Worker> -> unit
end

type Worker = struct
  val mutable WorkStack: Work
  val mutable Handler: Handler
end

type Cont<'a> = class
  inherit Work
  val mutable Value: 'a
  abstract DoCont: byref<Worker> * 'a -> unit
end

type Job<'a> = class
  abstract DoJob: byref<Worker> * Cont<'a> -> unit
end

Continuation (kotlin)

public interface CoroutineContext {
    public operator fun <E : Element> get(key: Key<E>): E?

    public fun <R> fold(initial: R, operation: (R, Element) -> R): R

    public operator fun plus(context: CoroutineContext): CoroutineContext

    public fun minusKey(key: Key<*>): CoroutineContext

    public interface Element : CoroutineContext {
        public val key: Key<*>

        @Suppress("UNCHECKED_CAST")
        public override operator fun <E : Element> get(key: Key<E>): E?

        public override fun <R> fold(initial: R, operation: (R, Element) -> R): R

        public override fun minusKey(key: Key<*>): CoroutineContext
    }

    public interface Key<E : Element>
}

public interface Continuation<in T> {
     */
    public val context: CoroutineContext

    public fun resume(value: T)

    public fun resumeWithException(exception: Throwable)
}
module NewAsync_ =
open System
open System.Threading
open System.Threading.Tasks
open FSharp.Core.Printf
type [<Struct>] Maybe<'T> =
| Nothing
| Just of 'T
module Details =
let error (msg : string) = Console.Error.WriteLine msg
let errorf fmt = kprintf error fmt
let info (msg : string) = Console.Out.WriteLine msg
let infof fmt = kprintf info fmt
let dispose (d : IDisposable) =
if isNull d |> not then
try
d.Dispose ()
with
| e -> ()
let inline refEq<'T when 'T : not struct> (l : 'T) (r : 'T) = Object.ReferenceEquals (l, r)
type [<Struct>] AtomicUpdate<'T, 'U> =
| ContinueWith of 'T*'U
| AbortWith of 'U
type AtomicRef<'T when 'T : not struct> (initial : 'T) =
// TODO: Does this work properly?
// Things to look for
// 1. Make sure reading hv introduces a memory barrier
// 2. Make sure writing hv introduces a memory barrier
[<VolatileField>]
let mutable v = initial
member x.CompareExchange (value : 'T) : bool =
let excepted = v
refEq (Interlocked.CompareExchange<'T> (&v, value, excepted)) excepted
member x.Update (u : 'T -> AtomicUpdate<'T, 'U>) : 'U =
let rec loop u =
match u v with
| ContinueWith (nv, rv) ->
if x.CompareExchange nv then
rv
else
loop u
| AbortWith rv ->
rv
loop u
member x.Value = v
type NewAtomicInt (init : int) =
// TODO: Does this work properly?
let mutable v = init
member x.Add (i : int) : int = Interlocked.Add (&v, i)
open Details
exception NewAsyncCancelled of unit
exception NewAsyncProgrammingError of unit
exception NewAsyncTaskNotCompleted of Task
let newAsyncCancelled = NewAsyncCancelled ()
let newAsyncProgrammingError = NewAsyncProgrammingError ()
let newAsyncTaskNotCompleted t = NewAsyncTaskNotCompleted t
type INewAsyncContext =
interface
inherit IDisposable
abstract CancellationToken : CancellationToken
abstract Id : int
abstract IsRunning : bool
abstract ThreadGuard : (unit -> unit) -> unit
end
type NewAsyncContext (bad : exn -> unit) =
class
static let ctxIdGen = NewAtomicInt (1001)
let cts = new CancellationTokenSource ()
let ct = cts.Token
let ctxId = ctxIdGen.Add 1
interface INewAsyncContext with
member x.Dispose () =
cts.Cancel ()
dispose cts
member x.CancellationToken = ct
member x.Id = ctxId
member x.IsRunning = ct.IsCancellationRequested |> not
member x.ThreadGuard (run: unit -> unit) : unit =
try
run ()
with
| ex ->
// bad ex
()
end
type NewAsyncChildContext (parent : INewAsyncContext) =
class
let ct = parent.CancellationToken
let ctxId = parent.Id
interface INewAsyncContext with
member x.Dispose () = ()
member x.CancellationToken = ct
member x.Id = ctxId
member x.IsRunning = ct.IsCancellationRequested |> not
member x.ThreadGuard (run: unit -> unit) : unit =
try
run ()
with
| ex ->
// bad ex
()
end
type [<Struct>] NewAsyncResult<'T> =
| Value of value : 'T
| Delayed of delayed : (('T -> unit) -> unit)
type [<Struct>] NewAsync<'T> = NewAsync of (INewAsyncContext -> NewAsyncResult<'T>)
module NewAsync =
module Details =
let tid () = Thread.CurrentThread.ManagedThreadId
let inline value (ctx : INewAsyncContext) v =
if ctx.IsRunning then
Value v
else
raise newAsyncCancelled
let inline throw (ctx : INewAsyncContext) e =
if ctx.IsRunning then
raise e
else
raise newAsyncCancelled
let inline delayed (ctx : INewAsyncContext) d =
if ctx.IsRunning then
Delayed d
else
raise newAsyncCancelled
type ChildState<'T> =
| Unset
| ValueReceived of value : 'T
| ExceptionReceived of ex : exn
| CallbackReceived of callback : ('T -> unit)
| Done
type [<Struct>] ChildStateAction<'T> =
| DoCallback of callback : ('T -> unit)
| DoValue of value : 'T
| DoRaise of ex : exn
| DoNothing
open Details
// Monad
let nreturn (v : 'T) : NewAsync<'T> =
NewAsync <| fun ctx ->
value ctx v
let nraise (ex : exn) : NewAsync<_> =
NewAsync <| fun ctx ->
throw ctx ex
let nbind (NewAsync t) (uf : 'T -> NewAsync<'U>) : NewAsync<'U> =
NewAsync <| fun ctx ->
match t ctx with
| Value tv ->
let (NewAsync u) = uf tv
u ctx
| Delayed tdel ->
delayed ctx <| fun ugood ->
let tgood tv =
let (NewAsync u) = uf tv
match u ctx with
| Value uv ->
ugood uv
| Delayed udel ->
udel ugood
tdel tgood
// Applicative
let nap (f : NewAsync<'T -> 'U>) (t : NewAsync<'T>) : NewAsync<'U> =
nbind f (fun fv -> nbind t (fun tv -> nreturn (fv tv)))
// Functor
let nmap (m: 'T -> 'U) (t : NewAsync<'T>) : NewAsync<'U> =
nbind t (fun tv -> nreturn (m tv))
// Combinators
let npair (t : NewAsync<'T>) (u : NewAsync<'U>) : NewAsync<'T*'U> =
nbind t (fun tv -> nbind u (fun uv -> nreturn (tv, uv)))
// Fundamentals
let nchild (NewAsync c) : NewAsync<NewAsync<'T>> =
NewAsync <| fun ctx ->
let cctx = new NewAsyncChildContext (ctx)
match c cctx with
| Value cv ->
value ctx (nreturn cv)
| Delayed cdel ->
let cstate = AtomicRef<ChildState<'T>> (Unset)
let cgood cv =
let update s =
match s with
| Unset ->
ContinueWith (ValueReceived cv, DoNothing)
| ValueReceived _ ->
error "Value already received (Value)"
AbortWith DoNothing
| ExceptionReceived _ ->
error "Exception already received (Value)"
AbortWith DoNothing
| CallbackReceived tgood ->
ContinueWith (Done, DoCallback tgood)
| Done ->
error "Already done (Value)"
AbortWith DoNothing
match cstate.Update update with
| DoCallback tgood -> tgood cv
| DoValue _ -> error "Unexpected DoValue (Value)"
| DoRaise _ -> error "Unexpected DoRaise (Value)"
| DoNothing -> ()
let cbad cex =
let update s =
match s with
| Unset ->
ContinueWith (ExceptionReceived cex, DoNothing)
| ValueReceived _ ->
error "Value already received (Exception)"
AbortWith DoNothing
| ExceptionReceived _ ->
error "Exception already received (Exception)"
AbortWith DoNothing
| CallbackReceived tgood ->
ContinueWith (Done, DoCallback tgood)
| Done ->
error "Already done (Exception)"
AbortWith DoNothing
match cstate.Update update with
| DoCallback _ -> raise cex
| DoValue _ -> error "Unexpected DoValue (Exception)"
| DoRaise _ -> error "Unexpected DoRaise (Exception)"
| DoNothing -> ()
cdel cgood
match cstate.Value with
| Unset ->
let ct =
NewAsync <| fun ctx ->
delayed ctx <| fun tgood ->
let update (s : ChildState<'T>) : AtomicUpdate<ChildState<'T>, ChildStateAction<'T>> =
match s with
| Unset ->
ContinueWith (CallbackReceived tgood, DoNothing)
| ValueReceived v ->
ContinueWith (Done, DoValue v)
| ExceptionReceived cex ->
ContinueWith (Done, DoRaise cex)
| CallbackReceived _ ->
error "Callback already received (Callback)"
AbortWith DoNothing
| Done ->
error "Already done (Callback)"
AbortWith DoNothing
match cstate.Update update with
| DoCallback _ -> errorf "Unexpected DoCallBack (Callback)"
| DoValue cv -> tgood cv
| DoRaise cex -> raise cex
| DoNothing -> ()
value ctx ct
| ValueReceived cv ->
value ctx (nreturn cv)
| ExceptionReceived cex ->
value ctx (nraise cex)
| CallbackReceived _ ->
error "Callback shouldn't be received yet"
value ctx (nraise newAsyncProgrammingError)
| Done ->
error "State shouldn't be done yet"
value ctx (nraise newAsyncProgrammingError)
let nthread : NewAsync<unit> =
NewAsync <| fun ctx ->
delayed ctx <| fun tgood ->
let ts _ =
ctx.ThreadGuard tgood
let t = Thread (ThreadStart ts)
t.IsBackground <- true
t.Start ()
let nthreadPool : NewAsync<unit> =
NewAsync <| fun ctx ->
delayed ctx <| fun tgood ->
let wcb _ =
ctx.ThreadGuard tgood
ThreadPool.QueueUserWorkItem (WaitCallback wcb) |> ignore
// Misc
let ndebug (name : string) (NewAsync t) : NewAsync<_> =
NewAsync <| fun ctx ->
printfn "NewAsync - Begin - %s - TID:%d - CTX:%d" name (tid ()) ctx.Id
let tr = t ctx
// TODO: Handle Raised exception (register frame handler)
match tr with
| Value tv ->
printfn "NewAsync - %s - Value - TID:%d - CTX:%d - Value: %A" name (tid ()) ctx.Id tv
tr
| Delayed tdel ->
printfn "NewAsync - %s - Delayed - TID:%d - CTX:%d" name (tid ()) ctx.Id
Delayed <| fun tgood ->
printfn "NewAsync - %s - Continuation - TID:%d - CTX:%d" name (tid ()) ctx.Id
let dgood tv =
printfn "NewAsync - %s - Value (Continuation) - TID:%d - CTX:%d - Value: %A" name (tid ()) ctx.Id tv
tgood tv
tdel dgood
let nrun (t : NewAsync<'T>) (good: 'T -> unit) (bad: exn -> unit) : unit =
let (NewAsync c) = nbind nthreadPool (fun _ -> t)
let ctx = new NewAsyncContext (bad) :> INewAsyncContext
let dgood v = dispose ctx; good v
let run () =
match c ctx with
| Delayed cdel ->
cdel dgood
| _ ->
failwith "nthreadPool didn't return a Delayed value"
ctx.ThreadGuard run
// Task interop
let ntask (t : Task<'T>) : NewAsync<'T> =
NewAsync <| fun ctx ->
if t.IsCanceled then
throw ctx newAsyncCancelled
elif t.IsFaulted then
throw ctx t.Exception
elif t.IsCompleted then
value ctx t.Result
else
delayed ctx <| fun tgood ->
if t.IsCanceled then
throw ctx newAsyncCancelled
elif t.IsFaulted then
throw ctx t.Exception
elif t.IsCompleted then
tgood t.Result
else
let cw (tt : Task<'T>) =
if tt.IsCanceled then
throw ctx newAsyncCancelled
elif tt.IsFaulted then
throw ctx t.Exception
elif tt.IsCompleted then
tgood tt.Result
else
throw ctx (newAsyncTaskNotCompleted tt)
t.ContinueWith (Action<Task<'T>> cw) |> ignore
type Builder () =
member inline x.Bind (t, uf) = nbind t uf
member inline x.Bind (t, uf) = nbind (ntask t) uf
member inline x.Return v = nreturn v
member inline x.ReturnFrom t = t : NewAsync<_>
let newAsync = NewAsync.Builder ()
type NewAsync<'T> with
static member inline (>>=) (t, uf) = NewAsync.nbind t uf
static member inline (|>>) (t, m) = NewAsync.nmap m t
static member inline (<*>) (f, t) = NewAsync.nap f t
static member inline (<&>) (t, u) = NewAsync.npair t u
[<EntryPoint>]
let main argv =
0
public class Task : IThreadPoolWorkItem, IAsyncResult, IDisposable
{
public Task(Action action);
public Task(Action action, CancellationToken cancellationToken);
public Task(Action action, TaskCreationOptions creationOptions);
public Task(Action<object> action, object state);
public Task(Action action, CancellationToken cancellationToken, TaskCreationOptions creationOptions);
public Task(Action<object> action, object state, CancellationToken cancellationToken);
public Task(Action<object> action, object state, TaskCreationOptions creationOptions);
public Task(Action<object> action, object state, CancellationToken cancellationToken, TaskCreationOptions creationOptions);
public static int? CurrentId { get; }
public static TaskFactory Factory { get; }
public static Task CompletedTask { get; }
public TaskCreationOptions CreationOptions { get; }
public bool IsCompleted { get; }
public bool IsCanceled { get; }
public TaskStatus Status { get; }
public AggregateException Exception { get; }
public int Id { get; }
public object AsyncState { get; }
public bool IsFaulted { get; }
public static Task Delay(int millisecondsDelay, CancellationToken cancellationToken);
public static Task Delay(int millisecondsDelay);
public static Task Delay(TimeSpan delay, CancellationToken cancellationToken);
public static Task Delay(TimeSpan delay);
public static Task FromCanceled(CancellationToken cancellationToken);
public static Task<TResult> FromCanceled<TResult>(CancellationToken cancellationToken);
public static Task FromException(Exception exception);
public static Task<TResult> FromException<TResult>(Exception exception);
public static Task<TResult> FromResult<TResult>(TResult result);
public static Task Run(Action action);
public static Task Run(Action action, CancellationToken cancellationToken);
public static Task<TResult> Run<TResult>(Func<TResult> function);
public static Task<TResult> Run<TResult>(Func<TResult> function, CancellationToken cancellationToken);
public static Task Run(Func<Task> function);
public static Task Run(Func<Task> function, CancellationToken cancellationToken);
public static Task<TResult> Run<TResult>(Func<Task<TResult>> function);
public static Task<TResult> Run<TResult>(Func<Task<TResult>> function, CancellationToken cancellationToken);
public static bool WaitAll(Task[] tasks, int millisecondsTimeout, CancellationToken cancellationToken);
public static void WaitAll(Task[] tasks, CancellationToken cancellationToken);
public static bool WaitAll(Task[] tasks, int millisecondsTimeout);
public static void WaitAll(params Task[] tasks);
public static bool WaitAll(Task[] tasks, TimeSpan timeout);
public static int WaitAny(Task[] tasks, int millisecondsTimeout, CancellationToken cancellationToken);
public static int WaitAny(Task[] tasks, int millisecondsTimeout);
public static int WaitAny(Task[] tasks, TimeSpan timeout);
public static int WaitAny(params Task[] tasks);
public static int WaitAny(Task[] tasks, CancellationToken cancellationToken);
public static Task WhenAll(IEnumerable<Task> tasks);
public static Task<TResult[]> WhenAll<TResult>(params Task<TResult>[] tasks);
public static Task<TResult[]> WhenAll<TResult>(IEnumerable<Task<TResult>> tasks);
public static Task WhenAll(params Task[] tasks);
public static Task<Task> WhenAny(IEnumerable<Task> tasks);
public static Task<Task<TResult>> WhenAny<TResult>(IEnumerable<Task<TResult>> tasks);
public static Task<Task> WhenAny(params Task[] tasks);
public static Task<Task<TResult>> WhenAny<TResult>(params Task<TResult>[] tasks);
public static YieldAwaitable Yield();
public ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext);
public Task<TResult> ContinueWith<TResult>(Func<Task, object, TResult> continuationFunction, object state, TaskContinuationOptions continuationOptions);
public Task ContinueWith(Action<Task> continuationAction, CancellationToken cancellationToken);
public Task<TResult> ContinueWith<TResult>(Func<Task, object, TResult> continuationFunction, object state, CancellationToken cancellationToken, TaskContinuationOptions continuationOptions, TaskScheduler scheduler);
public Task ContinueWith(Action<Task> continuationAction, TaskContinuationOptions continuationOptions);
public Task ContinueWith(Action<Task> continuationAction);
public Task ContinueWith(Action<Task> continuationAction, CancellationToken cancellationToken, TaskContinuationOptions continuationOptions, TaskScheduler scheduler);
public Task ContinueWith(Action<Task> continuationAction, TaskScheduler scheduler);
public Task ContinueWith(Action<Task, object> continuationAction, object state, CancellationToken cancellationToken);
public Task<TResult> ContinueWith<TResult>(Func<Task, object, TResult> continuationFunction, object state, TaskScheduler scheduler);
public Task<TResult> ContinueWith<TResult>(Func<Task, object, TResult> continuationFunction, object state, CancellationToken cancellationToken);
public Task ContinueWith(Action<Task, object> continuationAction, object state);
public Task<TResult> ContinueWith<TResult>(Func<Task, TResult> continuationFunction, CancellationToken cancellationToken, TaskContinuationOptions continuationOptions, TaskScheduler scheduler);
public Task<TResult> ContinueWith<TResult>(Func<Task, TResult> continuationFunction, TaskContinuationOptions continuationOptions);
public Task<TResult> ContinueWith<TResult>(Func<Task, TResult> continuationFunction, TaskScheduler scheduler);
public Task<TResult> ContinueWith<TResult>(Func<Task, object, TResult> continuationFunction, object state);
public Task<TResult> ContinueWith<TResult>(Func<Task, TResult> continuationFunction);
public Task<TResult> ContinueWith<TResult>(Func<Task, TResult> continuationFunction, CancellationToken cancellationToken);
public Task ContinueWith(Action<Task, object> continuationAction, object state, TaskScheduler scheduler);
public Task ContinueWith(Action<Task, object> continuationAction, object state, CancellationToken cancellationToken, TaskContinuationOptions continuationOptions, TaskScheduler scheduler);
public Task ContinueWith(Action<Task, object> continuationAction, object state, TaskContinuationOptions continuationOptions);
public void Dispose();
public TaskAwaiter GetAwaiter();
public void RunSynchronously(TaskScheduler scheduler);
public void RunSynchronously();
public void Start(TaskScheduler scheduler);
public void Start();
public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken);
public bool Wait(int millisecondsTimeout);
public bool Wait(TimeSpan timeout);
public void Wait();
public void Wait(CancellationToken cancellationToken);
protected virtual void Dispose(bool disposing);
}
public class Task<TResult> : Task
{
public Task(Func<TResult> function);
public Task(Func<TResult> function, CancellationToken cancellationToken);
public Task(Func<TResult> function, TaskCreationOptions creationOptions);
public Task(Func<object, TResult> function, object state);
public Task(Func<TResult> function, CancellationToken cancellationToken, TaskCreationOptions creationOptions);
public Task(Func<object, TResult> function, object state, CancellationToken cancellationToken);
public Task(Func<object, TResult> function, object state, TaskCreationOptions creationOptions);
public Task(Func<object, TResult> function, object state, CancellationToken cancellationToken, TaskCreationOptions creationOptions);
public static TaskFactory<TResult> Factory { get; }
public TResult Result { get; }
public ConfiguredTaskAwaitable<TResult> ConfigureAwait(bool continueOnCapturedContext);
public Task<TNewResult> ContinueWith<TNewResult>(Func<Task<TResult>, object, TNewResult> continuationFunction, object state, CancellationToken cancellationToken, TaskContinuationOptions continuationOptions, TaskScheduler scheduler);
public Task<TNewResult> ContinueWith<TNewResult>(Func<Task<TResult>, object, TNewResult> continuationFunction, object state, TaskContinuationOptions continuationOptions);
public Task<TNewResult> ContinueWith<TNewResult>(Func<Task<TResult>, object, TNewResult> continuationFunction, object state, TaskScheduler scheduler);
public Task<TNewResult> ContinueWith<TNewResult>(Func<Task<TResult>, object, TNewResult> continuationFunction, object state, CancellationToken cancellationToken);
public Task<TNewResult> ContinueWith<TNewResult>(Func<Task<TResult>, object, TNewResult> continuationFunction, object state);
public Task<TNewResult> ContinueWith<TNewResult>(Func<Task<TResult>, TNewResult> continuationFunction, CancellationToken cancellationToken, TaskContinuationOptions continuationOptions, TaskScheduler scheduler);
public Task<TNewResult> ContinueWith<TNewResult>(Func<Task<TResult>, TNewResult> continuationFunction, TaskContinuationOptions continuationOptions);
public Task<TNewResult> ContinueWith<TNewResult>(Func<Task<TResult>, TNewResult> continuationFunction, TaskScheduler scheduler);
public Task<TNewResult> ContinueWith<TNewResult>(Func<Task<TResult>, TNewResult> continuationFunction, CancellationToken cancellationToken);
public Task<TNewResult> ContinueWith<TNewResult>(Func<Task<TResult>, TNewResult> continuationFunction);
public Task ContinueWith(Action<Task<TResult>, object> continuationAction, object state, CancellationToken cancellationToken, TaskContinuationOptions continuationOptions, TaskScheduler scheduler);
public Task ContinueWith(Action<Task<TResult>, object> continuationAction, object state, TaskContinuationOptions continuationOptions);
public Task ContinueWith(Action<Task<TResult>, object> continuationAction, object state, TaskScheduler scheduler);
public Task ContinueWith(Action<Task<TResult>, object> continuationAction, object state, CancellationToken cancellationToken);
public Task ContinueWith(Action<Task<TResult>> continuationAction, CancellationToken cancellationToken, TaskContinuationOptions continuationOptions, TaskScheduler scheduler);
public Task ContinueWith(Action<Task<TResult>> continuationAction, TaskContinuationOptions continuationOptions);
public Task ContinueWith(Action<Task<TResult>> continuationAction, TaskScheduler scheduler);
public Task ContinueWith(Action<Task<TResult>> continuationAction, CancellationToken cancellationToken);
public Task ContinueWith(Action<Task<TResult>> continuationAction);
public Task ContinueWith(Action<Task<TResult>, object> continuationAction, object state);
public TaskAwaiter<TResult> GetAwaiter();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment