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.
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
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.
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
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.