Concurrency, Javascript and Go

Here’s a small and brief write up of my understanding of concurrency and how it relates to Javascript and Go.

What is Concurrency?

Of course, let’s start off with a Wikipedia quote:

In computer science, concurrency is the ability of different parts or units of a program, algorithm, or problem to be executed out-of-order or in partial order, without affecting the final outcome.

Sometimes parts of a program are expensive and the CPU is waiting for external events to happen. As a result we are able to allow the CPU to switch between parts of a program (known as context switching) in order to increase efficiency.

But it gets better! If our CPU has multiple cores, then we can achieve parallelism and achieve even more speed! Rob Pike, a major contributor to Go, puts it incredibly well:

Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once.

Concurrency in Javascript

Can we achieve concurrency in Javascript? Well, sort of.

Asynchronicity in Javascript is event driven and can be achieved in a variety of ways:

Let’s use a basic callback inside a setTimeout. After 500ms has passed, the log will execute:

setTimeout(() => {
  console.log("Hello");
}, 500);

What actually happens here? As setTimeout is part of the web api, when it is called, the browser/Node will call an internal method mapped to the api call, in this case one that uses it’s built in timer. Which under the hood probably calls the OS timer.

Once 500ms has passed, the registered callback is pushed to a callback queue. The callback will be taken off the queue and executed once both the call stack is empty on the main thread and there are no renders in the queue to process (usually every 16ms to achieve 60 fps). This is called the event loop.

The important point here is a callback is only executed when the call stack is empty. Below is an example that demonstrates the problem with this. The loop underneath will have to be completely done (regardless of duration) before the setTimeout callback can be executed. Therefore, the for loop is blocking and not truly concurrent. Even if the time set was 0ms!

new Promise(resolve => {
  setTimeout(() => {
    console.log("Hello");
  }, 0);
});

for (i = 0; i <= 100000; i++) {
  console.log(i);
}

So Javascript, in itself, is not concurrent. But the environment in which it is run in, is, as we can have various http requests, setTimeouts, etc in process, but only deal with them one at a time and with an empty call stack on the single thread that Javascript runs on.

It must be noted that there are some ways to achieve true concurrency using web workers, but that is out of scope here.

Concurrency in Go

In contrast, the somewhat equivalent in Go, would be:

package main

import (
  "fmt"
  "sync"
  "time"
)

func delayedLog(wg *sync.WaitGroup) {
  time.Sleep(time.Second / 2)
  fmt.Println("Hello")
  wg.Done()
}

func main() {
  var wg sync.WaitGroup
  wg.Add(1)

  go delayedLog(&wg)

  for i := 0; i <= 100000; i++ {
    fmt.Println(i)
  }

  wg.Wait()
}

The difference here, however, is that “Hello” can get printed at any point, even halfway through the loop (as fmt.Println is non-atomic).

Essentially, using the keyword go, the expression provided on the right hand side is run in a goroutine. A goroutine is a bit like a thread (a thread of execution at the OS level), except that it is handled by the go runtime and operates more like a green thread.

To communicate between goroutines, this example uses waitgroups, this allows the calling goroutine to know when the goroutine, or multiple goroutines, are complete. We require it in the example above as otherwise it’s possible the program will terminate before the goroutine is complete.

If you wish to pass data between goroutines, then you would want to look at channels.

Concurrency Issues

With true concurrent languages like Go, it requires a little more thought and work as we can run into various types of issues with order of execution if we are not careful. Because sometimes ordering is important. One of these is known as a race condition.

A race condition is where two or more threads can access shared data and try to alter it at the same time. Suppose we have 2 threads that are trying to update the value of a user’s account balance:

  1. Thread A reads the current balance of £100.
  2. Thread B reads the current balance of £100.
  3. Thread A is processing a debit transaction of £60, so with the previous read value, calculates the new balance to be £40.
  4. Thread A writes the new balance of £40.
  5. Thread B is processing a debit transaction of £20, so with the previous read value, calculates the new balance to be £80.
  6. Thread B writes the new balance of £80.

As a user, you would be over the moon at this, the first transaction was completely void due to Thread B reading the initial balance of £100 and operating on that.

A common way to resolve this issue is using locks (or mutexes). A lock prevents a thread from being accessed by other threads at certain points. With locks in place, the process would look like this:

  1. Thread A reads the current balance of £100.
  2. Thread A locks the thread from being accessed.
  3. Thread B attempts to read, but is unable to due to the lock, so waits.
  4. Thread A is processing a debit transaction of £60, so with the previous read value, calculates the new balance to be £40.
  5. Thread A writes the new balance of £40.
  6. Thread A unlocks the thread.
  7. Thread B reads the current balance of £40.
  8. Thread B is processing a debit transaction of £20, so with the previous read value, calculates the new balance to be £20.
  9. Thread B writes the new balance of £20.

This solves the issue quite well and is used particularly often. However, if we had a huge amount of updates to make, then there would be a lot of threads just waiting for their turn. If 100% accuracy was trivial, then one approach you could take is batching updates together to prevent as many locks.

Here is just one issue of many when it comes to concurrency management. But with the right approach you can make some serious efficiency gains.