Exploring Concurrency in Modern Software Development

Daniel Saunders
March 10, 2025

Summary

Concurrency is difficult to model and conceptualize. In this post, I am going to discuss how I conceptualize concurrency when writing software. I will use Java and TypeScript for my examples.

An random series of lines pointing in different directions giving the impression of the confusion around concurrency
Credit: DALL-E 3

Concurrency vs Parallelism

In Computer Science, a distinction is made between concurrency and parallelism. For the purposes of this article, I am mostly interested in exploring concurrency as it can be understood as the superset of both concepts (concurrent execution units can be carried out in parallel or serially. Any execution units running in parallel by definition will be concurrent.)

Parallelism is an emphasis on multiple processing streams occurring literally at the same point in time on different hardware.

Concurrency models execution units that might happen out of order—whether it happens in parallel or serially can be understood as an implementation detail.

It is also worth discussing the somewhat more ambiguous terms “synchronous” and “asynchronous.”

Synchronous code often refers to sequential lines of a high-level programming language that execute in order in the same thread.

Asynchronous code often refers to sequential lines of a high-level programming language that execute in some way out-of-order, often not on the same thread and sometimes with other lines executing interleaved with the original lines in the same thread.

In my observation, many software developers do not find it particularly difficult to understand parallelism intuitively. After all, that is how much of the real world behaves. Who has not stood in line at a grocery store and watched multiple queues proceed at the same time but at different paces for each cashier?

In contrast, concurrency, synchronous, and asynchronous code are more subtle and many programming languages expose interfaces to allow generic forms of concurrency—whether that physically happens in parallel or whether the programmer has much influence in the actual order of execution. However, in languages such as Java and TypeScript, how concurrency is expressed may not always be obvious when reading code.

Exploring Concurrency in Java

To start off, we’re going to investigate an example of asynchronous coding in Java. Below is a small example of how concurrency might be accomplished in Java.

1Future<Integer> myAsyncAdder(int a, int b) {
2    return CompletableFuture.runAsync(() -> {
3        Thread.sleep(1000);
4        return a + b;
5    })
6}
7
8var result = myAsyncAdder(1, 2).get();
9System.out.println(result);

What happens in the code above?

Let’s start by focusing on the lines that will be sequentially executed in the same thread in order highlighted below:

1Future<Integer> myAsyncAdder(int a, int b) {
2    return CompletableFuture.runAsync(() -> {
3        Thread.sleep(1000);
4        return a + b;
5    })
6}
7
8var result = myAsyncAdder(1, 2).get();
9System.out.println(result);

One way in which we can understand this code is by imagining that the highlighted lines execute sequentially. We’ll call this the “Synchronous” code.

What about lines 3 and 4 then?

They are part of a lambda expression that will be executed at some point but not as part of the immediate execution flow. From the perspective of the code flow which starts at the top of this code snippet, it does not yet matter when that happens and we should understand it as happening “later”. The method

runAsync()
is an instruction requesting that the provided lambda be executed out-of-order. Internally, it will be placed on some queue of work that can be picked up by another thread to execute later. It is possible that execution actually happens in parallel and before we get to line 8; it is just as possible that it happens much later.

Is this the “Asynchronous” code then? From one frame of reference, yes! When reading the original block of code we typically read each line one-by-one, mentally incrementing a program counter to represented the simulated state of the executing computer. As part of that exercise, it is important to mentally excise this code from the sequentially executed code invoked when the code is first encountered. Again, it will execute, “later”.

3        Thread.sleep(1000);
4        return a + b;

Look at lines 3 and 4 on their own though. Is this code “Synchronous” now? Sure! Each line will execute serially from whatever context the lambda is executed later. The

Thread.sleep()
is also executed in the same thread and will (depending on the JVM’s environment) suspend the execution of the thread and instruct the OS to not re-schedule the thread and continue execution until 1000 milliseconds have elapsed.

Communicating Between Concurrent Work Units

Now let’s return to this interesting line which needs further investigation:

8	var result = myAsyncAdder(1, 2).get();

Why is the

get()
method call necessary? The
myAsyncAdder()
method synchronously returns a
Future<V>
which is used to track an asynchronous execution unit (similar to JavaScript’s
Promise
discussed in the next section.) The holder of a
Future<V>
can inspect it to determine if that work is finished yet. But what if—from our current program counter—we just want to wait until that work is done before we continue?

In general, we have two approaches:

  1. Defer the execution of another asynchronous work unit until the
    Future<V>
    completes and the current program counter will just continue onto the next lines (demonstrated in the next section.)
  2. Block the current execution thread until that asynchronous, concurrent work completes.

The

get()
method call is an instance of the second case. The current thread will invoke
park()
and will not be scheduled by the OS until later (depending on exact implementation, there may be some combination of signaller and spin lock that will re-schedule the thread either periodically or once the
Future<V>
has completed. See OpenJDK’s implementation for an example.)

From this, I think it can be shown that labelling code as asynchronous is often misleading because it depends on the frame of reference.

Multiple Async Steps

Let us move onto a more complicated example. This example will be somewhat intentionally obtuse, in order to highlight the concurrency.

 1const myAsyncMather = async (a: number, b: number) => {
 2    const value = a + b;
 3
 4    return await sleep(1000)
 5        .then(async () => {
 6            return value * 2;
 7        })
 8}
 9
10myAsyncMather(1, 2).then(result => {
11    console.log(result);
12});

Note that the code above depends on a small helper function

sleep()
for succinctness, but I won‘t spend anymore time on this—JavaScript does not provide a builtin function that behaves like this function. Just assume that
sleep()
is an
async
function. A possible implementation is below:

1const sleep = (duration: number) =>
2    new Promise(resolve => setTimeout(resolve, duration));

Multiple Asynchronous Patterns

Let‘s start to parse the sequence of events in this example. While I find the TypeScript syntax more elegant than the Java syntax, there is a lot more going on in this code block than in the Java example.

The Async Operator

To begin, a

Promise
is a JavaScript object that is created by the runtime (although it can also be created manually, such as in our example
sleep()
function) which can be used to track an asynchronous execution unit just like Java’s
Future<V>
. If we wanted to, we could pass the
Promise
around as an indicator of the result of asynchronous work, just like we did in Java.

To relate asynchronous execution units, invoking

then()
on a
Promise
instructs the runtime to execute the function passed as the argument to
then()
after the original
Promise
has completed executing its function’s body. The invocation of
then()
also returns a new
Promise
which will complete once both steps have completed. This creates a relatively convenient method of chaining asynchronous work units in a way that visually looks similar to normal in-order executing code.

That similarity can be deceptive though.

Modern JavaScript provides the

async
operator as syntax sugar to simplify the construction and handling of
Promise
s. A function marked with
async
instructs the JavaScript runtime to effectively modify the function to return a
Promise
which itself will return the result of whatever the
async
function returns.

In effect the following code:

1async () => {
2    return 'myValue';
3}

might become something like:

1() => {
2    return new Promise(resolve => resolve('myValue'));
3}

Note that the body of an

async
function is not necessarily executed asynchronously (i.e. out of order and sometime in the future). For example, the following code:

1const asyncLog = async () => {
2    console.log('log from async function');
3};
4
5asyncLog();
6console.log('log from outside function'

will always print:

1log from async function
2log from outside function

On its own, the

async
just wraps the return value in a
Promise
and a
Promise
is just a convenient object used to indicate the result of a possibly-asynchronous operation. A
Promise
can be constructed in the “completed” state. Until code within the
async
function instructs the runtime to execute something asynchronously, everything within the function body will execute synchronously before it returns.

Where the

async
function becomes interesting is when the
await
operator is employed within the body of the function. Recall that earlier patterns of asynchronous code execution essentially provided instructions to the runtime to, “run this code block later at some point,” and then code execution continued immediately after that point. The
await
instructs the runtime to return a
Promise
and then the remainder of the function after the line executed with
await
will continue execution after the asynchronous operation completes.

To illustrate that, the following example will print

returning from function
immediately after this function is executed and
I am awake
will print at least 1 second later.

1async () => {
2    sleep(1000).then(() => console.log('I am awake'));
3    console.log('returning from function')
4}

In the nearly-identical function below which employs an

await
operator, the function will return a
Promise
immediately after
sleep()
is invoked and
returning from function
will not be logged until all the asynchronous steps on line 2 have completed.

1async () => {
2    await sleep(1000).then(() => console.log('I am awake'));
3    console.log('returning from function')
4}

The function above could be conceptualized as if it were being re-written by the runtime to look like this:

1async () => {
2    return sleep(1000).then(() => console.log('I am awake'))
3        .then(() => {
4            console.log('returning from function');
5        });
6}

Returning to the original code, hopefully we can start to parse out the messy mixture of concurrent work units.

 1const myAsyncMather = async (a: number, b: number) => {
 2    const value = a + b;
 3
 4    return await sleep(1000)
 5        .then(async () => {
 6            return value * 2;
 7        })
 8}
 9
10myAsyncMather(1, 2).then(result => {
11    console.log(result);
12});

The first execution unit will comprise the highlighted code below. Note that in particular, a few lines such as 5 and 10 are deliberately highlighted. As this code is executed synchronously, the lambdas will be declared at the same time. For example,

sleep()
will immediately return a
Promise
representing the asynchronous, “work,” to be completed in 1 second. With the returned
Promise
object, the instance method
then()
is immediately invoked and consequently the lambda passed to it must be immediately declared as well. However, the lambdas will not be invoked until later.

 1const myAsyncMather = async (a: number, b: number) => {
 2    const value = a + b;
 3
 4    return await sleep(1000)
 5        .then(async () => {
 6            return value * 2;
 7        })
 8}
 9
10myAsyncMather(1, 2).then(result => {
11    console.log(result);
12});

This execution will complete and then sometime after this the

Promise
to sleep for 1 second completes, the event loop will pick up and run the next execution unit which is the highlighted code below:

5        .then(async () => {
6            return value * 2;
7        })

Even though this second execution unit is not internally taking any asynchronous actions, there will still be a final, distinct, asynchronous execution unit shown below:

10myAsyncMather(1, 2).then(result => {
11    console.log(result);
12});

Note also that in this case, all these concurrent execution units must happen sequentially (even if not immediately after one another) because they were chained together with

then()
. That imposes an ordering that is not always going to exist with
async
functions though.

“Later” is a Matter of Perspective

Now that we have reviewed two examples of concurrency in TypeScript and Java, what does it mean to say that code is “asynchronous”? It is really a matter of perspective and—depending on the language—it can sometimes be hard to tell.

Viewed in isolation, there is nothing asynchronous/concurrent about this line below. When the program counter reaches it—however that may happen—we can consider the execution of this function body as a synchronous, independent execution unit.

5        .then(async () => {
6            return value * 2;
7        })

However, if you start to view it from a certain outside context, it does not look that way anymore, and we need to understand it as something that will be executed in a different timeframe than the current top-down reading context:

1const myAsyncMather = async (a: number, b: number) => {
2    const value = a + b;
3
4    return await sleep(1000)
5        .then(async () => {
6            return value * 2;
7        })
8}

A Note on Threads

This article has largely focused on abstract concurrency and has only seldomly discussed threads—let alone process, green threads, coroutines, etc. For a given programming language and framework, it is important to know how the respective concurrent paradigms relate.

Ultimately, the thread is a sequence of instructions. How a high-level programming language’s operators and expressions map to machine instructions that runs in a thread varies. As an example, Java historically correlated its instructions fairly closely to machine instructions. Often, a reader could assume local code will all run on the same thread and would have exclusive use of that thread, baring an obvious indicator otherwise such as

new Thread()
. Concurrency was often managed by executing asynchronous tasks on entirely new threads (or at least on thread pools where each task would get complete ownership of a thread until it was finished).

Other runtimes such as JavaScript’s are quite different. JavaScript’s runtime will generally have a single thread with the expectation that independent units of instructions will yield the thread as fast as possible, e.g. by the use of the

await
operator, which gives the runtime’s event loop the opportunity to schedule another task.