Chapter 9. Concurrency

Concurrency is considered to be the one of the most attractive features of Go. Adopters of the language revel in the simplicity of its primitives to express correct concurrency implementations without the pitfalls that usually come with such endeavors. This chapter covers the necessary topics to understand and create concurrent Go programs, including the following:

  • Goroutines
  • Channels
  • Writing concurrent programs
  • The sync package
  • Detecting race conditions
  • Parallelism in Go

Goroutines

If you have worked in other languages, such as Java or C/C++, you are probably familiar with the notion of concurrency. It is the ability of a program to run two or more paths of execution independently. This is usually done by exposing a thread primitive directly to the programmer to create and manage concurrency.

Go has its own concurrency primitive called the goroutine, which allows a program to launch a function (routine) to execute independently from its calling function. Goroutines are lightweight execution contexts that are multiplexed among a small number of OS-backed threads and scheduled by Go's runtime scheduler. That makes them cheap to create without the overhead requirements of true kernel threads. As such, a Go program can initiate thousands (even hundreds of thousands) of goroutines with minimal impact on performance and resource degradation.

The go statement

Goroutines are launched using the go statement as follows:

go <function or expression>

A goroutine is created with the go keyword followed by the function to schedule for execution. The specified function can be an existing function, an anonymous function, or an expression that calls a function. The following code snippet shows an example of the use of goroutines:

func main() { 
   go count(10, 50, 10) 
   go count(60, 100, 10) 
   go count(110, 200, 20) 
} 
func count(start, stop, delta int) { 
   for i := start; i <= stop; i += delta { 
         fmt.Println(i) 
   } 
} 

golang.fyi/ch09/goroutine0.go

In the previous code sample, when the go count() statement is encountered in the main function, it launches the count function in an independent execution context. Both the main and count functions will be executing concurrently. As a side effect, main will complete before any of the count functions get a chance to print anything to the console.

Later in the chapter, we will see how to handle synchronization idiomatically between goroutines. For now, let us use fmt.Scanln() to block and wait for keyboard input, as shown in the following sample. In this version, the concurrent functions get a chance to complete while waiting for keyboard input:

func main() { 
   go count(10, 30, 10) 
   go count(40, 60, 10) 
   go count(70, 120, 20) 
   fmt.Scanln() // blocks for kb input 
} 

golang.fyi/ch09/goroutine1.go

Goroutines may also be defined as function literals directly in the go statement, as shown in this updated version of the example shown in the following code snippet:

func main() { 
   go count(10, 30, 10) 
   go func() { 
         count(40, 60, 10) 
   }() 
   ... 
}  

golang.fyi/ch09/goroutine2.go

The function literal provides a convenient idiom that allows programmers to assemble logic directly at the site of the go statement. When using the go statement with a function literal, it is treated as a regular closure with lexical access to non-local variables, as shown in the following example:

func main() { 
   start := 0 
   stop := 50 
   step := 5 
   go func() { 
         count(start, stop, step) 
   }() 
} 

golang.fyi/ch09/goroutine3.go

In the previous code, the goroutine is able to access and use the variables start, stop, and step. This is safe as long as the variables captured in the closure are not expected to change after the goroutine starts. If these values are updated outside of the closure, it may create race conditions causing the goroutine to read unexpected values by the time it is scheduled to run.

The following snippet shows an example where the goroutine closure captures the variable j from the loop:

func main() { 
   starts := []int{10,40,70,100} 
   for _, j := range starts{ 
         go func() { 
               count(j, j+20, 10) 
         }() 
   } 
} 

golang.fyi/ch09/goroutine4.go

Since j is updated with each iteration, it is impossible to determine what value will be read by the closure. In most cases, the goroutine closures will see the last updated value of j by the time they are executed. This can be easily fixed by passing the variable as a parameter in the function literal for the goroutine, as shown here:

func main() { 
   starts := []int{10,40,70,100} 
   for _, j := range starts{ 
         go func(s int) { 
               count(s, s+20, 10) 
         }(j) 
   } 
} 

golang.fyi/ch09/goroutine5.go

The goroutine closures, invoked with each loop iteration, receive a copy of the j variable via the function parameter. This creates a local copy of the j value with the proper value to be used when the goroutine is scheduled to run.

Goroutine scheduling

In general, all goroutines run independently of each other, as depicted in the following illustration. A function that creates a goroutine does not wait for it to return, it continues with its own execution stream unless there is a blocking condition. Later, the chapter covers synchronization idioms to coordinate goroutines:

Goroutine scheduling

Go's runtime scheduler uses a form of cooperative scheduling to schedule goroutines. By default, the scheduler will allow a running goroutine to execute to completion. However, the scheduler will automatically yield to another goroutine for execution if one of the following events occurs:

  • A go statement is encountered in the executing goroutine
  • A channel operation is encountered (channels are covered later)
  • A blocking system call (file or network IO for instance) is encountered
  • After the completion of a garbage collection cycle

The scheduler will schedule a queued goroutines ready to enter execution when one of the previous events is encountered in a running goroutine. It is important to point out that the scheduler makes no guarantee of the order of execution of goroutines. When the following code snippet is executed, for instance, the output will be printed in an arbitrary order for each run:

func main() { 
   go count(10, 30, 10) 
   go count(40, 60, 10) 
   go count(70, 120, 20) 
   fmt.Scanln() // blocks for kb input 
} 
func count(start, stop, delta int) { 
   for i := start; i <= stop; i += delta { 
         fmt.Println(i) 
   } 
} 

golang.fyi/ch09/goroutine1.go

The following shows possible output for the previous program:

10
70
90
110
40
50
60
20
30
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.216.175