Skip to content
DUONG Phu-Hiep edited this page Oct 29, 2018 · 9 revisions
  • APM = Asynchronous Programming Model = IAsyncResult = Begin / End (not recommended)
  • EAP = Event-based Asynchronous Pattern = Async suffix + events, event handler + EventArg (not recommended)
  • TAP = Task-based Asynchronous Pattern = async / await (recommended)

TAP method return Task or Task<TResult> and don't accept out and ref parameters. It might support Cancellation / Progression

Task.Run (since .NET 4.5) = sugar shortcut for Task.Factory.StartNew

Task.Factory.StartNew oftens return Task<Task> you will have to unwrap() it..

var t = Task.Factory.StartNew(async ()=>{
	await Task.Delay(300);
}); //t is Task<Task<VoidTaskResult>>
var tt = t.Unwrap(); //tt is Task<VoidTaskResult>
tt.Wait(); //t.Wait() will return immediately so we must to wait the unwrap task

Source

  • If the work you have is I/O-bound, use async and await. You should not use the TPL (Task.Runor Parallel.For..)
  • If the work you have is CPU-bound and you care about responsiveness, use async and await but spawn the work off on another thread with Task.Run

You will mostly see I/O-bounds code. The CPU-bounds code is quite rare, it is codes with big for-loop, heavy computational eg. regular expression, linq over object..

Example: I/O-bounds code Load data from 100 databases:

Good code: attack all the database at once with asyn / await

public async Task<List<House>> LoadHousesAsync() {
	var tasks = new List<Task<House>>();
	foreach (var db in DatabaseList) {
		Task<House> t = db.LoadHouseAsync();
		tasks.Add(t);
	}
	House[] resu = await Task.WhenAll(tasks);
	return resu.ToList();
}

Bad code: use TPL to spawn new threads which wait for I/O

public async List<House> LoadHousesInParalle() {
	var resu = new BlockingCollection<House>();
	Parallel.ForEach (DatabaseList, db => {
		House house = db.LoadHouse();
		resu.Add(house);
	});
	return resu.ToList();
}

Example 2:

private static Random rand = new Random();

/// let say this is an I/O Bound task
private static async Task doSomeReadWriteAsync(int id) {
    var t = rand.Next(1000);
    Log.Information($"{id} start {t}");
    await Task.Delay(t);
    Log.Information($"{id} end");
}

Bad code do read/write once by once

private static async Task ForLoopTpl() {
    Log.Information("ForLoopTpl start");
    for (int i = 0; i < 3; i++) {
        await doSomeReadWriteAsync(i);
    }
    Log.Information("ForLoopTpl end");
}
/*
ForLoopTpl start
0 start 443
0 end
1 start 906
1 end
2 start 186
2 end
ForLoopTpl end
*/

Good code: read/write all at once in parallel

private static async Task ForLoopTpl() {
    Log.Information("ForLoopTpl start");
    var t = new List<Task>();
    for (int i=0; i<3; i++) {
        t.Add(doSomeReadWriteAsync(i));
    }
    await Task.WhenAll(t); //don't use Task.WaitAll() -> it will block the current thread
    Log.Information("ForLoopTpl end");
}
/*
ForLoopTpl start
0 start 238
1 start 419
2 start 29
2 end
0 end
1 end
ForLoopTpl end
*/
  • async void f() is a void-returning task (vrt)

  • async Task f() is a task-returning task

  • A void-returning task (vrt) is a "fire and forget" mechanism

    • The caller is unable to know when the vrt will finish: await vrt won't wait until the vrt finish, it only wait until the first await inside the vrt body.
    • The caller is unable to catch exceptions inside of the vrt.
  • Use vrt only for top-level event handlers (or if you have no choices)

  • Whenever you see an async lambda, it could be void-returning or task-returning => you will have to verify it every single time that you see it

Example 1:

Action     f1 = async () => { Console.WriteLine("f1 begin"); await Task.Delay(300); Console.WriteLine("f1 end"); };
Func<Task> f2 = async () => { Console.WriteLine("f2 begin"); await Task.Delay(300); Console.WriteLine("f2 end"); };

var t1 = Task.Run(f1); //t1 is Task<void> (t1 is a void-returning task)
t1.Wait(); //it won't wait, but return immediately (Run and Forget)
Console.WriteLine("t1 wait exit");

var t2 = Task.Run(f2); //t2 is Task<VoidTaskResult> (t2 is a task-returning task)
t2.Wait(); //it will wait
Console.WriteLine("t2 wait exit");

Task.Delay(1000).Wait();

result

f1 begin
t1 wait exit
f2 begin
f1 end
f2 end
t2 wait exit

in case overloading ambigues: Task.Run(async()=>{..}) => it will consider the parameter as task-returning => no worry!

Example 2:

//Task RunAsync(Priority priority, DispatchedHandler h);
//delegate void DispatchedHandler();

try {
	await dispatcher.RunAsync(Priority.Normal, async() => {
		await LoadAsync();
		throw new Exception();
	})
}
catch (Exception) {
	...
}

There is a async lambda in this code => we must to check if it is "async void" or "async task".. The async lambda is a DispatchedHandler which returns void => so it is a void-returning task => so the await and try / catch in the caller is completly useless. It won't wait nor catching any exception..

1. Avoid fake async codes: All methods suffixed with Async must to be marked with the async keyword. Example:

Good code

static async Task ComputeAsync() {
	..await somewhere
}

=> if the body won't spawn any new thread => the code is truely async

BAD code

static Task ComputeAsync() {
	return Task.Run(() => { //spaw a new thread
		Compute();
	});
}

=> the code look like async, but it is fake, it use up CPU and ThreadPool => it is beter to keep only the sync version of Compute() and let the callers decide whether they will run it in other thread or not..

2. Avoid fake sync codes

VERY BAD code

static Task Compute() {
	var t = ComputeAsync();
	t.Wait(); //freeze the UI Thread (SynchronizationContext) 
}

static async Task ComputeAsync() {
	await
	return something; //wait for the UI Thread
}

it look like sync codes, but it isn't, and it might cause dead-lock:

  • the UI-Thread is freezed because the ComputeAsync is not finished
  • the ComputeAsync is not finished because the UI-Thread is freezed

To avoid deadlock:

  • In your “library” async methods, use ConfigureAwait(false) wherever possible.
  • Don’t block on Tasks; use async all the way down.

Old reading

to read: Avoid async void link 1, link 2

https://msdn.microsoft.com/en-us/library/hh873177(v=vs.110).aspx

Workload: Compute-bound Tasks or I/O-bound Tasks or Mixed

  • if a method is purely compute-bound, it should be exposed only as a synchronous implementation. Let the user choice if he want to wrap it to another Task.Run (or TaskFactory.StartNew for more fine grained-control).

  • All method invole I/O should have an async implementation. the TAP (Task Async Pattern) is implemented

    • with help of the compiler: async await

      • compilers perform the necessary transformations to implement the method asynchronously
      • An asynchronous method should return either a System.Threading.Tasks.Task or a System.Threading.Tasks.Task<TResult> object
      • In the case of the latter, the body of the function should return a TResult, and the compiler ensures that this result is made available through the resulting task object
      • any exceptions that go unhandled within the body of the method are marshaled to the output task and cause the resulting task to end in the TaskStatus.Faulted state
      • The exception is when an OperationCanceledException (or derived type) goes unhandled, in which case the resulting task ends in the TaskStatus.Canceled state
    • or manually with TaskCompletionSource

      • Perform the asynchronous operation, and when it completes, call the SetResult, SetException, or SetCanceled method, or the Try version of one of these methods

      • Example:

        public static Task<int> ReadTask(this Stream stream, byte[] buffer, int offset, int count, object state)
        {
          var tcs = new TaskCompletionSource<int>();
          stream.BeginRead(buffer, offset, count, ar =>
          {
              try { tcs.SetResult(stream.EndRead(ar)); }
              catch (Exception exc) { tcs.SetException(exc); }
          }, state);
          return tcs.Task;
        }