Company blog

Asynchronicity in the Go Programming Language

Post date 23.11.2020
0 Comments

A few words to begin with

The Go language allows easy and quick implementation of an asynchronous code fragments. Usage of a key word “go” and channels is the basis for action in the Go. Channels are used for communication between concurrent programme activities. In the context of the Go language, they are called goroutine functions. Every programme started in the Go has a main goroutine called the main function. Consecutive functions can be created by adding the key word “go” before them.

By default the Go language may use as many operating system threads as high is the number of machine processors.

Did you know?

The Go language executive environment has its own planner whose responsibilities are analogous to the ones of an operating system kernel planner. They take care solely of a one programme goroutine.

To the code! Primitive routines.

I wrote a simple call() function which simulates some time-consuming tasks, e.g. waiting time for a server response. It generates a random int number from the <0;350) range. Then, it executes operations that convert its type to time. Duration that represents time range (here, expressed in miliseconds) waits for a generated amount of miliseconds before it returns that value.

After calling rand.Seed() at the beginning of the main(), the programme will generate different random numbers every time.

func main() {
    rand.Seed(time.Now().UnixNano())
}
func call() time.Duration {
    resp_time := time.Duration(rand.Intn(350)) * time.Millisecond
    time.Sleep(resp_time)
    return resp_time
}

The main() function calls the call() function in a loop n numer of times. Eventually, the programme writes out the sum of values stored in a variable linear_time returned by the call() function to the standard output and the real time of the programme execution (variable real_time).

For now, these times are nearly identical as the code runs synchronously.

func main() {
    rand.Seed(time.Now().UnixNano())
    start := time.Now()
    const n = 10
    var linear_time time.Duration = 0
     
    for i := 0; i < n; i++ {
        linear_time += call()
    }
    real_time := time.Since(start)
     
    fmt.Println("Linear time:\t",linear_time)
    fmt.Println("Real time:\t", real_time.String())
}

The whole synchronous code below:

package main
 
import (
    "time"
    "math/rand"
    "fmt"
)
func main() {
    rand.Seed(time.Now().UnixNano())
    start := time.Now()
    const n = 10
    var linear_time time.Duration = 0
 
    for i := 0; i < n; i++ {
        linear_time += call()
    }
    real_time := time.Since(start)
 
    fmt.Println("Linear time:\t",linear_time)
    fmt.Println("Real time:\t", real_time.String())
}
 
func call() time.Duration {
    resp_time := time.Duration(rand.Intn(350)) * time.Millisecond
    time.Sleep(resp_time)
    return resp_time
}

An exemplary print from the standard output:

To the code! – asynchronicity

It’s time to use the command „go” and create a channel which allows communication between goroutines main() and call(). I’ll call this channel resp time and I’ll store/keep values returned by the call() function (variables time.Duration type) in it.

I will pass resp time to the call() function that I will asynchronously call in a loop adding the key word „go”.

func main() {
    rand.Seed(time.Now().UnixNano())
    start := time.Now()
 
    const n = 10
    var linear_time time.Duration = 0
    resp_time := make(chan time.Duration)
    for i := 0; i < n; i++ {
        go call(resp_time)
 
    }
    real_time := time.Since(start)
    fmt.Println("Real time:\t", real_time.String())
    fmt.Println("Linear time:\t",linear_time)
}
 
func call(resp_time chan time.Duration) {
    duration := time.Duration(rand.Intn(350)) * time.Millisecond
    time.Sleep(duration)
    resp_time <- duration
}

The above code lacks data reading from the channel so data escapes and the main() function doesn’t even wait for a return of n calls of the call() function.

For a change I will create a function that reads data from a channel as anonymous. I will call it a separate goroutine as I did with the call() function.

for i := 0; i < n; i++ {
    go func(n time.Duration) {
        linear_time += n
    }(<-resp_time)
}

The whole asynchronous code below:

package main
 
import (
 "time"
 "math/rand"
 "fmt"
)
 
 
func main() {
    rand.Seed(time.Now().UnixNano())
    start := time.Now()
 
    const n = 10
    var linear_time time.Duration = 0
    resp_time := make(chan time.Duration)
    for i := 0; i < n; i++ {
        go call(resp_time)
 }
    for i := 0; i < n; i++ {
        go func(n time.Duration) {
            // fmt.Println("reading  ",n)
            linear_time += n
        }(<-resp_time)
    }
    real_time := time.Since(start)
    fmt.Println("Real time:\t", real_time.String())
    fmt.Println("Linear time:\t",linear_time)
}
 
func call(resp_time chan time.Duration) {
    duration := time.Duration(rand.Intn(350)) * time.Millisecond
    // fmt.Println("writening",duration)
    time.Sleep(duration)
    resp_time <- duration
}

Thanks to taking out the comment from commands: fmt.PrintIn („reading” „n”) and fmt.PrinIn („writening”, duration) it can be noticed in the printout from the standard output that saving operations to the channel and reading from the channel are executed – by all appearances – in a random sequence. If you look closely, you will notice that reading comes from the shortest to the longest time. Time of the programme execution is not equal with the sum of all call() functions executions but with the longest of call() functions executions. Eventually, programme worked for about 340 ms instead of 1 second.

Summary

The Go language has a very efficient in-built system that allows creation of an asynchronous code. Without much effort it is possible to significantly quicken the programme via execution of its fragments simultanously on a few/all machine cores.

Instead of channels, Once and WaitGroup types from the ‘sync’ library can be used in communication between goroutines. It contains other types too but those are destined for synchronisation of goroutines on a lower level. It is prescribed to execute a high-level synchronisation with the use of channels.

Sources:

  • Język Go. Poznaj i programuj, Alan A. A. Donovan, Brian W. Kernighan, str.9-13, 215-231
  • https://golang.org/pkg/time/
  • https://golang.org/pkg/sync/

Author: Paulina Sorys

Comments (0)

No comments

Leave a Reply

Your email address will not be published. Required fields are marked *

All articles

Do you have a question?
Write to us!