Task and Await: Consuming Awaitable Methods [Article]

Solarion

Honorary Master
Joined
Nov 14, 2012
Messages
28,051
Reaction score
17,804
There's a lot of confusion about async/await, Task/TPL, and asynchronous and parallel programming in general. I've had some requests to dive into the topic, so we'll start with the basics and go deeper from there.

Before we look at creating our own asynchronous methods, we should look at how to consume asynchronous methods. More specifically, we'll look at how to consume "awaitable" methods. What is an "awaitable" method? These are methods that return Task or Task<T> (and they can also return "void" and there are some other awaitables, but we'll just worry about Tasks today).

Full article
 
Asynchronous allows other things to run while waiting on the result of something else, essentially making use of the process instead of blocking.
Parallel is independent streams/threads and process individually regardless of what the other is doing (except for resource locks etc).

Async <> Parallel.

You're welcome :)
 
Asynchronous allows other things to run while waiting on the result of something else, essentially making use of the process instead of blocking.
Parallel is independent streams/threads and process individually regardless of what the other is doing (except for resource locks etc).

Async <> Parallel.

You're welcome :)

Still confusing for me but you've given me a better idea thanks vamp. I currently use Async (await and task.run ) when pulling large amounts of data into say, a datagridview in WinForms, which allows me to give status updates via a progressbar for example. It works that's all I know.

Just to add to the bolded bit, you can use:

PHP:
await Task.Factory.StartNew(() =>
{
//Your process
});

or

PHP:
await Task.Run(() =>
{
//Your process
});

Task.Factory.StartNew has ContinueWith whereas Task.Run does not. Correct me if I'm wrong.

PHP:
Task.Factory.StartNew(() =>
    {
        Console.WriteLine("Step 1");
        Thread.Sleep(2000);
    })
    .ContinueWith((prevTask) =>
    {
        Console.WriteLine("I waited step 1 to be completed!");
    })
    .ContinueWith((prevTask) =>
    {
        Console.WriteLine("Step 3");
    });
 
Last edited:
I have a full working example, if you want me to post the code here just shout I'm happy to.
 
Asynchronous allows other things to run while waiting on the result of something else, essentially making use of the process instead of blocking.
Parallel is independent streams/threads and process individually regardless of what the other is doing (except for resource locks etc).

Async <> Parallel.

You're welcome :)
Careful about the choice of operator because a vb-esque <> != (not equal)
As example: in Haskell & in general Functional Programming <> refers to a binary associative morphism of semigroups & monoids.
  • 1 <> 2 == 3
  • produces the categorical equivalence of 1 + 2 == 3
 
Asynchronous allows other things to run while waiting on the result of something else, essentially making use of the process instead of blocking.
Parallel is independent streams/threads and process individually regardless of what the other is doing (except for resource locks etc).

Async <> Parallel.

You're welcome :)

To clarify a bit:
Asynchronous really just means that tasks can be split up in a way that they can run out of order for a period. Ability to parallelize computation is one benefit of asynchronous execution, but it does not necessarily mean parallel work is being done.

Parallel means simply that tasks are running at the same time. It does not mean that they necessarily run independently though - with SIMD or SIMT for example, one thread (in the logical sense, not OS threads) of execution can compute something that gets sourced as an input for another thread in the next clock. OS threads can also cooperate locklessly. Parallel execution doesn't have to be asynchronous.
 
Last edited:
Thinking in parallel, easy it's like a gatling gun, thinking in async, is like shooting a double-barrel shotgun and keeping track of the buckshot. Any advice on how to plan/reason async, vampire?
Async / Await is designed to solve a specific problem that occurs when writing e.g. a GUI application. Basically Async / Await allows you to minimally change your straightforward blocking implementation to run achieve non-blocking code. FYI Under the covers this is achieved by wrapping the code in a state machine that functions quite similar to the state machines that accompany a typical game run loop.

As for planning / determining a code base propensity for parallelisation; I'll leave the opinions to the others, and rather point to towards a strictly mathematical approach ito concurrency; which originates from two primary branches of mathematics: category theory and abstract algebra:

Designing / Determining Propensity for Parallelisation
Canvas 1.png

The above diagram basically shows that:
are all structures which are considered to be Magmas i.e. a structure that is equipped with binary operation.

More so a Monoid is both a Semigroup and Magma i.e. it is both associative and includes an identity, where a Bounded Semilattice includes the abilities of the all the other structures: binary operation, associative, identity, commutative and idempotence.

The downward arrow reflects the increase propensity for parallelisation; i.e. a bounded semilattice is highly parellisable where's a semigroup has certain restrictions.

This is naturally a large field of study and relatively complex to learn; however don't get frightened away by the Latin / Greek terms and the mathematical formulations; as it certainly become quite a bit easier to read this over time.

Here's a very quick intro:

Associativity stipulates that groupings are irrelevant, for example:
  • 1 + 2 + 3 == (1 + 2) + 3 == 1 + (2 + 3)
This works for both addition and multiplication, but does not work for subtraction or division.

Identity relates to the base value that we start associative operations, for example:
  • In addition; zero is the identity because 0 + 1 == 1 (i.e. zero has no effect on the outcome)
  • In multiplication, one is the identity, because 1 * 2 == 2 (i.e. one has no effect on the outcome)

Commutativity indicates that order is also irrelevant, for example:
  • 1 + 2 + 3 == 2 + 1 + 3 == 3 + 2 + 1
i.e. order of the binary operation doesn't matter. This means that we don't have to concerned about the order in which we combine structures because the result will be the same i.e. we can run on multiple threads and not be concerned about the assembly order.

Idempotence is achieved when a binary operation can be applied multiple times without affecting the result, for example:
  • Determining the Math.max or Math.min of a series of integers will always deliver the same result irrespective of how many times the same operation is repeated.
These structures have a high propensity for parellisation, however as the diagram also indicates this is more rare than structures that are only associative.

Ok, so after all of this, you may be wondering if this means that concurrency is only limited to numbers. Good news it isn't, as this is where the abstractness of category theory kicks in, by allowing us to wrap almost any structure in a "wrapper / box type" that support these attributes whilst preserving the values that they wrap. Example of this are: applicative functors, monads, arrows, ...
 
Last edited:
Still confusing for me but you've given me a better idea thanks vamp. I currently use Async (await and task.run ) when pulling large amounts of data into say, a datagridview in WinForms, which allows me to give status updates via a progressbar for example. It works that's all I know.

Never tried doing status updates in an async await structure, not sure if it is the best way to do it but if it is working reliably for you then great. I've always used threading for large workloads with a threadsafe call to UI to update status.

I have been using async await a lot lately for WebAPI stuff but even that is somewhat linear except for data retrieval.

The best way to think about it is think of parallel as 2 or more people doing tasks vs async await as only one. In the parallel design you can have separate people doing separate work, not reliant on each other or knowing what each other are doing unless they need to share a resource, [eg] use the same book etc. Async await, as a single person, would do a task and if they are busy waiting for feedback from something during that task (await) then they can go do something else until the result is returned from the first task and then carry on [eg] waiting for the kettle to boil to make tea so you go get your cup, teabag etc while you wait.
 
@Droid, sincerely thank you, I'll be reading over this a few times today.
Only a pleasure.

Btw most languages have built in support for the lifting of data and boxing it almost magically in structural types that conform to these underlying category theory principles, for example:
  • If you've ever used either map, reduce/fold or flatmap in a language then you've used language features that support these concepts. Btw in category theory terms like map implies the boxing complies with the rules of a functor, reduce/fold implies it's a monoid and flatmap implies it's a monad.
  • In Java 8 it's btw called streams, and these also have support for parallel execution.
  • In C# it's btw called Linq and Enumerable; which although syntactically aligned with SQL terms is exactly the same conceptually, for example: Linq Select is map, and SelectMany is flatMap; and like Java also supports parallel execution.

Note:
  • Most modern languages today support map, flatmap, etc. but there is far more of these concepts that are not available, but can be built by yourself or you'll can find them as a dependency; but remember because this is an actively researched area, not everything discovered is either documented, or available in an easy to consume manner. As example; most languages still do not have built in support for Applicative Functors (introduced in 2007); is a type that is not only very useful for reducing chaining algorithm costs by composition, but unlike monads is also able to easily support parallel execution of these chains: e.g. useful during input / data validation
  • All of these more complex types: (functor, monad, ...) are like all mathematical concepts built from a combination of simple axioms: magmas, semigroups -- so to really understand a functor you have to first understand a magma, and then work your way up; but remember that you don't have to completely understand the underlying mathematic axioms behind a functor in order to use map; however in the long run it will certainly help with more advanced use cases.

Anyway hope this also helps with some further clarity on these new approaches to concurrency. Happy to help if you have any questions, or even just to try to point you to code examples.
 
Last edited:
Top
Sign up to the MyBroadband newsletter
X