Skip to main content

Introduction

Concurrency and Parallelism on Different Cores

In single core, if program is executed synchronously. Firstly one process will be executed completely, then second, then third, and so on.

The tasks can execute in two different ways i.e. Synchronous and Asynchronous.

Synchronous Programming

This means that each task will execute one after another. The task will wait until the preceding task completes its execution.

Asynchronous Programming

This means that each task will execute independently of other tasks. Tasks will not wait for other tasks before completing their own execution.

💡
On a single core with synchronous programming, we can’t achieve concurrency because the block of code will execute sequentially.
💡
Please note that while explaining synchronous and asynchronous programming in single core and multi-core system, we will be using WaitGroups in Go. Therefore, we would suggest you to focus on the intuition of the examples and in future we will discuss in detail what is WaitGroups in Go.

Synchronous Programming with Single Core

Synchronous Programming with Single Core

In single core, if program is being executed synchronously, then firstly one process will be executed completely, then second, then third, and so on.

Let's take one example where we want to sum from 1 to 40000, running 3 different processes synchronously.

First process will calculate the sum from 1 to 10000. Second process will calculate the sum from 10001 to 20000. And the third process will calculate the sum from 20001 to 40000. We will calculate the total time taken while executing these three tasks. Let's look at the code example.

Code Example

Create a file single-core-synchronous.go in your current directory.

package main

import (
	"fmt"
	"runtime"
	"time"
)

func rangeSumSynchronous(start int, end int) int {
	sum := 0
	for num := start; num <= end; num++ {
		sum += num
	}
	return sum
}

func SingleCoreSynchronous() {
	runtime.GOMAXPROCS(1) // use 1 core
	start := time.Now()
	sum := 0
	
    sum += rangeSumSynchronous(1, 10000) // task 1
	sum += rangeSumSynchronous(10001, 20000) // task 2
	sum += rangeSumSynchronous(20001, 40000) // task 3
	
    duration := time.Since(start)
	
    fmt.Printf("Duration: %d microseconds\n", duration.Microseconds())
	fmt.Printf("Sum: %d\n", sum)
}

Now, in your main.go file, call SingleCoreSynchronous() method.

package main

import "fmt"

func main() {
	SingleCoreSynchronous()
}

Run go run . to see the output.

Output

Duration: 13 microseconds
Sum: 800020000

While running the same in your computer, the output may vary. If you will run the same code again and again, you will notice the difference.

So, we have calculated the time to execute 3 different tasks one after each other i.e. synchronously. Now let's see Asynchronous Programming with Single Core.

Asynchronous Programming with Single Core

Asynchronous Programming with Single Core

We will try to execute the same 3 tasks but this time we will do it asynchronously.

When we talk about asynchronous execution that too within a single core that means there are no other cores available that can execute tasks in parallel which means Context Switching will happen if we want to execute asynchronously (shown above in the image).

Code Example

Create a file single-core-asynchronous.go in your current directory.

package main

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

func rangeSumAsynchronous(start int, end int, wg *sync.WaitGroup) int {
	defer wg.Done()
	sum := 0
	for num := start; num <= end; num++ {
		sum += num
	}
	return sum
}

func SingleCoreAsynchronous() {
	runtime.GOMAXPROCS(1) // use 1 core
	var wg sync.WaitGroup

	wg.Add(3)

	start := time.Now()

	sum := 0
	go func() { // task 1 in separate goroutine
		sum += rangeSumAsynchronous(1, 10000, &wg)
	}()
	go func() { // task 2 in separate goroutine
		sum += rangeSumAsynchronous(10001, 20000, &wg)
	}()
	go func() { // task 3 in separate goroutine
		sum += rangeSumAsynchronous(20001, 40000, &wg)
	}()

	wg.Wait()

	duration := time.Since(start)
	fmt.Printf("Duration: %d microseconds\n", duration.Microseconds())
	fmt.Printf("Sum: %d\n", sum)
}

Now, in your main.go file, call SingleCoreAsynchronous() method.

package main

import "fmt"

func main() {
	SingleCoreAsynchronous()
}

Run go run . to see the output.

Output

Duration: 24 microseconds
Sum: 800020000

You might notice here that the time taken in case of Asynchronous is more than the time taken in case of Synchronous. It's not true that this will always give you bigger time. If you will run it multiple times, you might get the results somewhat closer to Synchronous.

💡
Asynchronous programming on a single core involves context switching, a process that requires time to store the current state of the executing process or thread and later retrieve that stored state to resume. This additional step contributes to increased time compared to synchronous programming on a single core.

Synchronous Programming with Multi Core

Synchronous Programming with Multi Core

In a multicore system with synchronous programming, we can achieve parallelism. There will be no change in time because we’re still waiting for the primary task to complete execution. That means if this task would have been executed with single core that would have made no difference.

Let's have a look at code example.

Code Example

Create a file multi-core-synchronous.go in your current directory.

package main

import (
	"fmt"
	"runtime"
	"time"
)

// no need to create this function in your code because 
// this function is already created in single-core-synchronous.go
// within the same package
func rangeSumSynchronous(start int, end int) int {
	sum := 0
	for num := start; num <= end; num++ {
		sum += num
	}
	return sum
}

func MultiCoreSynchronous() {
	runtime.GOMAXPROCS(4) // using multi core
	start := time.Now()
	sum := 0
	
    sum += rangeSumSynchronous(1, 10000) // task 1
	sum += rangeSumSynchronous(10001, 20000) // task 2
	sum += rangeSumSynchronous(20001, 40000) // task 2
	
    duration := time.Since(start)
	
    fmt.Printf("Duration: %d microseconds\n", duration.Microseconds())
	fmt.Printf("Sum: %d\n", sum)
}

Now, in your main.go file, call MultiCoreSynchronous() method.

package main

import "fmt"

func main() {
	MultiCoreSynchronous()
}

Run go run . to see the output.

Output

Duration: 13 microseconds
Sum: 800020000

In most cases, this time would be same as that of single core synchronous. You can try running it multiple times and visualise it.

Asynchronous Programming with Multi Core

Asynchronous Programming with Multi Core

In a multicore system with asynchronous programming, we can achieve concurrency as well as parallelism. Time code allows execution, and the hardware also allows us to run two threads or tasks simultaneously.

Code Example

Create a file multi-core-asynchronous.go in your current directory.

package main

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

// no need to create this function in your code because 
// this function is already created in single-core-asynchronous.go
// within the same package
func rangeSumAsynchronous(start int, end int, wg *sync.WaitGroup) int {
	defer wg.Done()
	sum := 0
	for num := start; num <= end; num++ {
		sum += num
	}
	return sum
}

func MultiCoreAsynchronous() {
	runtime.GOMAXPROCS(4) // using multi core
	var wg sync.WaitGroup

	wg.Add(3)

	start := time.Now()

	sum := 0 
	go func() { // task 1 in separate goroutine
		sum += rangeSumAsynchronous(1, 10000, &wg)
	}()
	go func() { // task 2 in separate goroutine
		sum += rangeSumAsynchronous(10001, 20000, &wg)
	}()
	go func() { // task 3 in separate goroutine
		sum += rangeSumAsynchronous(20001, 40000, &wg)
	}()

	wg.Wait()

	duration := time.Since(start)
	fmt.Printf("Duration: %d microseconds\n", duration.Microseconds())
	fmt.Printf("Sum: %d\n", sum)
}

Now, in your main.go file, call MultiCoreAsynchronous() method.

package main

import "fmt"

func main() {
	MultiCoreAsynchronous()
}

Run go run . to see the output.

Output

Duration: 12 microseconds
Sum: 800020000

In your case, this time might be different. And this time should be faster than the all the cases discussed above. But because we are visualising these cases on very lesser range or tasks which are taking really lesser time, therefore, it is possible that we are not able to see minimum time. But if we will execute these tasks on bigger inputs, you would definitely notice that Asynchronous with Multi Core is faster than all.

Conclusion

Single Core Multi Core
Synchronous Neither Concurrent nor Parallel Parallel
Asynchronous Concurrent Concurrent and Parallel

We are hoping that you have got the complete picture of how synchronous and asynchronous programming works on single core and multi core CPU.