Understanding Go routines & concurremcy with examples

goroutine is lightweight execution thread running in background. goroutine is a key ingredient to achieve concurrency in go.

It is very common for a go application to have thousands of goroutines running concurrently. Concurrency can speed up application significantly as well as help us write code with separation of concerns (SoC).

☛ What is a goroutine?

We understood in theory that how goroutine works, but in code, what is it? Well, a goroutine is simply a function or method that is running in background concurrently with other goroutines. It’s not a function or method definition that determine if it is a goroutine, it is determined by how we call it.

go provides a special keyword go to create a goroutine. When we call a function or a method with go prefix, that function or method executes in a goroutine. Let’s see a simple example.

In above program, we created a function printHello which prints Hello World! to the console. In main function, we called printHello() like normal function call and we got desired result.

Now let’s create goroutine from the same printHello function.

Well, as per goroutine syntax, we prefixed function call with go keyword and program executed well. It yielded following result
main execution started main execution stopped

It is a bit strange that Hello World did not get printed. So what happened?
goroutines always runs in the background. When a goroutine is executed, here go printHello(), go does not block the program execution unlike normal function call as we seen in previous example. Instead, the control is returned immediately to the next line of code and any returned value from goroutine is ignored. But even then, why we can’t see the function output?

By default, every go standalone application creates one goroutine. This is known as the main goroutine that the main function operates on. In above case, main goroutine spawns another goroutine of printHello function, let’s call it printHello goroutine. Hence when we execute above program, there are two goroutines running concurrently. As we saw in earlier program, goroutines are schedules cooperatively. Hence when main goroutine starts executing, go scheduler dot not pass control to the printHello goroutine until main goroutine does not executes completely. Unfortunately, when main goroutine is done with execution, program terminates immediately and scheduler does not get time to schedule printHello goroutine. But as we know from other lesson, using blocking condition, we can pass control to other goroutines manually AKA telling scheduler to schedule other available gorutines. Let’s use time.Sleep() call to do it.

We have modified program in such a way that before main goroutine pass control to the last line of code, we pass control to printHello goroutine using time.Sleep(10 * time.Millisecond) call. In this case, main goroutine sleeps for 10 milli-seconds and won’t be scheduled again for another 10 milli-seconds. Once printHello goroutine executes, it prints ‘Hello World!’ to the console and terminates, then main goroutine is schedules again (after 10 milli-seconds) to execute last line of code where stack pointer is. Hence above program yields following result
main execution started Hello World! main execution stopped

If we add a sleep call inside the function which will tell goroutine to schedule another available goroutine, in this case main goroutine. But from last lesson, we learned that only non-sleeping goroutines are considered for scheduling, main won’t be scheduled again for 10 milli-seconds while it’s sleeping. Hence main goroutine will print ‘main execution started’, spawning printHellogoroutine but still actively running, then sleeping for 10 milli-seconds and passing control to printHello goroutine. printHello goroutine the will sleep for 1 milli-second telling scheduler to schedule another goroutine but since there isn’t any available, waking up after 1 milli-second and printing ‘Hello World!’ and then dying. Then main goroutine will wake up after few milli-seconds, printing ‘main execution stopped’ and exiting the program.

Above program will still print same result
main execution started Hello World! main execution stopped

What if, instead of 1 milli-second, printHello goroutine sleeps for 15 milli-seconds.

In that case, main goroutine will be available to schedule for scheduler before printHello goroutine wakes up, which will also terminate the program immediately before scheduler had time to schedule printHello goroutine again. Hence it will yield below program
main execution started main execution stopped

. . .

☛ Working with multiple goroutines

As I said earlier, you can create as many gorutines as you can. Let’s define two simple functions, one prints characters of the string and one prints digit of the integer slice.

In above program, we are creating 2 goroutines from 2 function calls in series. Then we are scheduling any of the two goroutines and which goroutines to schedule is determined by the scheduler. This will yield following result
main execution started H e l l o 1 2 3 4 5 main execution stopped

Above result again proves that goroutines are cooperatively scheduled. Let’s add
another time.Sleep call in-between print operation in function definition to tell scheduler to schedule other available goroutine.

In above program, we printed extra information to see when a print statement is executing since the time of execution of the program. In theory, main goroutine will sleep for 200 milli-seconds, hence all other goroutine must do their job in 200 milli-seconds before it wakes up and kill the program. getChars goroutine will print 1 character and sleep for 10 milli-second, passing control to getDigits goroutine which will print a digit and sleeping for 3 milli-seconds passing control to getChars goroutine again when it wakes up.

Since getChars goroutine can print and sleep multiple times, at least 2 times while other goroutines are sleeping, we are hoping to see more characters printed in succession than digits.

Below result is taken from running above program in Windows machine.
main execution started at time 0s
H at time 1.0012ms <-| 1 at time 1.0012ms | almost at the same time e at time 11.0283ms <-| l at time 21.0289ms | ~10ms apart l at time 31.0416ms 2 at time 31.0416ms o at time 42.0336ms 3 at time 61.0461ms <-| 4 at time 91.0647ms | 5 at time 121.0888ms | ~30ms apart
main execution stopped at time 200.3137ms | exiting after 200ms

We can see the pattern we talked about. This will be cleared to you once you see the program execution diagram. We will approximate that print command takes 1ms of CPU time, compared on 200ms scale, that’s negligible.

Now we understood how to create goroutine and how to work with them. But using time.Sleep is just a hack to see the result. In production, we don’t know how much time a goroutine is going to take for the execution. Hence we can't just add random sleep call in main function. We want our goroutines to tell when they finished the execution. Also at this point, we don’t know how we can get data back from other goroutines or pass data to them, simply, communicate with them. This is where channels comes in. Let’s talk about them in next lesson.

. . .

☛ Anonymous goroutines

If an anonynous function can exist then anonymous goroutine can also exit. Please read Immedietly invoked function from functions lesson to understand this section.

Let’s modify our earlier example of printHellogoroutine.

The result is quite obvious as we defined function and executed as goroutine in the same statement.
All goroutines are anonymous as we learned from concurrency lesson as goroutine does not have an identity. But we are calling that in the sense that function from which it was created was anonymous.

It's not the same without you

Join the community to find out what other Ednsquare users are discussing, debating and creating.