Home     Interfacing     Software     About     Sitemap   
 EN   
Quick links

Synchronizing Goroutines

It is often necessary to synchronize goroutines. In our first goroutines example, we had two competing athletes in a running race. The only synchronization we used was in the main routine which had to wait for both athletes to reach the finish.

Real-life situations are often not that easy. When a program runs multiple processes, just waiting for completion is not enough. Often data must be exchanged between running processes or tasks must be synchronized halfway to ensure proper flow of execution and data. Luckily, the designers of Go have implemented this functionality in the core of the language. We will expand our athletes example by adding a more realistic workflow to it.

If you ever watched a running race, you will know that the starter not just yells “On your mark, Get set, GO!”, but that between each command, the starter waits until the previous action is completed. Within reasonable time of course. If a starter orders the athletes to go to their starting position with “On your mark”, they may decide to proceed with the next order, even if one or more athletes do not comply with the order within reasonable time. That is also how real-life programming works. If a command is sent to another process and there is no response in a timely fashion, the main routine may decide to continue processing. Timeouts are specifically designed for that.

In our previous example the athlete() goroutines were only started after the started issued the GO! command. To demonstrate the use of inter-process synchronization, I will change the workflow where the processes are started right away, and only then the starter starts with the starting procedure.

Athletes which are not reacting to the starter’s commands will be disqualified from the race, just as in real life. This example will therefore also show what happens if processes exit prematurely and how we can prevent any race conditions and lockups because of exiting tasks. Just as in a real running race, the starter–represented by the function main()–is the only entity in the process which we can rely on.

If you have watched running races in the past, you may have noticed that athletes sometimes take a lot of time wandering around before going to the start position. That human behaviour is also implemented in this example. The starter gives two seconds for the athletes to wander around and two seconds to position. Athletes who are not in the starting position by that time are disqualified.

package main

// Copyright (C) 2021 - Lammert Bies
// Distributed under terms of the MIT license.

/*
 * We need some functionality defined in other packages
 */
 
import (
        "fmt"
        "sync"
        "time"
        "math/rand"
)

/*
 * Athletes and the running race can be in various states
 * Each of the states is defined as a unique constant
 */
 
const (
        PREPARING    = iota
        POSITIONING  = iota
        RUNNING      = iota
        FINISHED     = iota
        DISQUALIFIED = iota
)

/*
 * We use a global variable to store the race state.
 * This allows it to be accessed by all athlete goroutines.
 * This is a simple approach for this example alone. In
 * further example programs, channels will be used which
 * are a much more versatile way to communicate between parallel
 * processes in the program.
 */

var race_state int = PREPARING

/*
 * func random_time( min int max int ) time.Duration
 *
 * Several stages in the process have to wait a random
 * amount of time. The random_time() function returns a
 * millisecond value between a minimum and maximum value.
 */

func random_time( min int, max int ) time.Duration {

        var milliseconds int

        if max <= min { milliseconds = min                        }
        else          { milliseconds = min + rand.Intn( max-min ) }

        return time.Duration( milliseconds ) * time.Millisecond

}  /* random_time */

/*
 * func athlete( name string, wg *sync.WaitGroup )
 *
 * Each athlete is represented by a state machine. The state of
 * the athlete changes based on the match state and on random
 * timers.
 */
 
func athlete( name string, wg *sync.WaitGroup ) {

        var preparation_time time.Duration
        var running_time time.Duration

        defer wg.Done()

        athlete_state := PREPARING
        fmt.Printf( "%s is preparing\n",  name )

        for {
                switch ( athlete_state ) {

                case PREPARING :
                        switch ( race_state ) {
                        case PREPARING :
                                preparation_time = random_time( 500, 5000 )
                                fmt.Printf( "%s takes %s preparing time\n", name, preparation_time );
                                time.Sleep( preparation_time )
                        case POSITIONING :
                                fmt.Printf( "%s is positioning\n", name )
                                athlete_state = POSITIONING
                        default :
                                athlete_state = DISQUALIFIED
                        }

               case POSITIONING :
                        switch ( race_state ) {
                        case POSITIONING :
                                time.Sleep( random_time( 20, 30 ) )
                        case RUNNING :
                                fmt.Printf( "%s Starts running\n", name )
                                athlete_state = RUNNING
                                running_time  = random_time( 4000, 8000 )
                                time.Sleep( running_time )
                        default :
                                athlete_state = DISQUALIFIED
                        }

                case RUNNING :
                        fmt.Printf( "%s finished after %s\n", name, running_time )
                        athlete_state = FINISHED

                case FINISHED : 
                        fmt.Printf( "%s has finished the race\n", name )
                        return

                case DISQUALIFIED :
                        fmt.Printf( "%s was disqualified\n", name )
                        return

                default :
                        athlete_state = DISQUALIFIED;
                }
        }
 }  /* athlete */

/*
 * func main()
 *
 * The main() function provides the functionality of the
 * match and the starter. It waits until all athletes have finished
 * or were disqualified and then exits.
 */
 
func main() {

        var wg sync.WaitGroup

        rand.Seed( time.Now().UTC().UnixNano() )

        wg.Add( 2 )
        go athlete( "John", &wg )
        go athlete( "Jane", &wg )

        time.Sleep( 2 * time.Second )
        fmt.Printf( "**** On your mark! Get set ****\n" )
        race_state = POSITIONING

        time.Sleep( 2 * time.Second )
        fmt.Printf( "**** GO! ****\n" )
        race_state = RUNNING

        wg.Wait()
        fmt.Printf( "**** The running race has ended ****\n" )

 }  /* main */

This example builds on our previous example. What has changed though is that we use a state machine for both the running race and each of the athletes. The state of the running race is leading for the state transitions the athletes go through. A number of new Go concepts were introduced in this example and I will step through them one by one.

Enumeration constants

In C we have the enum data type which can be used to define a series of unique constants. Go has a little bit different approach. Constants are defined in a const set where every element is assigned the value iota. The value of iota is initialized to zero every time the word const is encountered in the source file. With every next assignment to an element, the value of iota increases by one.

Functions Returning a Value

The code to determine a random wait time has been moved to a separate function random_time() This function select a random time between a lower and upper limit and converts it to a time.Duration type. That value is returned by the function. Returing a spefic type and not just an integer or float value allows us to feed the value without any additional conversion directly in a time.Sleep() call.

State Machine with switch Statements

The biggest change is in the athlete() function. Instead of a simple timer delaying the execution and then exiting, this version of the athlete() function contains a state machines which cycles through all the possible states of a run, triggered by state changes of the running race itself. The athlete states FINISHED and DISQUALIFIED will cause the goroutine to end.

The initial state of the athlete is PREPARING. If you have watched a real athletics event, this is the time the athletes are wandering around the starting position, concentrating, getting in the right state of mind for an exceptional performance. At some moment in the preparation phase, the starter will direct the athletes to the starting position with the “On your mark” command. Not every athlete will directly adhere to this command and some will be wandering around before actually taking position. Our virtual starter in this Go example program does not allow this to take too much time, and two seconds after the athletes received the order to position, the running race will start. Athletes who were not yet positioned at the starting position will be disqualified.

Because we use a random preparation time for an athlete between 500 and 5000 msec to prepare for the start, and a fixed time of 4 seconds before the GO! command is given by the starter, there is a significant change that one of the athletes is not ready for the start and is disqualified. The program flow of disqualification shows an important concept of goroutines and the way they can end. Basically, an external routine cannot force a goroutine to stop. There is no kill() function to terminate a goroutine and every goroutine must give up execution voluntarily. Therefore the determination of an athlete is qualified is not done by the external main function. Each athlete must check for themselves if execution of the routine is still possible, and exit if external situations demand it to do so.

So, when developing goroutines it is very important to design them in such a way that they can always stop by themselves. Go does not provide the language constructs to stop and remove goroutines which have become unresponsive due to a continuing lock of a resource or an infinite loop.

I have used a switch statement in this example because it is a common way in C programs to deal with state machines. Using a switch statement in Go makes the function quite bulky and difficult to read. In other examples I will show specific Go constructs which can make the implementation of state machines much more elegant.

The main() Routine Controlling the Running Race

The main() routine is slightly more complex than our previous example but still readable. The main difference is that the start of the goroutines has been moved to the beginning of the function. Therefore the athlete() goroutines are able to see al the changes of the state of the running race and adapt the change of the state of the athlete accordingly. The main() function will only exit when all goroutines have finished properly.

What is robbing a bank compared with founding a bank?
BERTOLD BRECHT
  Mar. 2021
   Copyright © 1997-2021 Lammert Bies, All rights reserved