Reader small image

You're reading from  Go Programming - From Beginner to Professional - Second Edition

Product typeBook
Published inMar 2024
Reading LevelBeginner
PublisherPackt
ISBN-139781803243054
Edition2nd Edition
Languages
Right arrow
Author (1)
Samantha Coyle
Samantha Coyle
author image
Samantha Coyle

Samantha Coyle, a Software Engineer at Diagrid, specializes in Go for cloud-native developer tooling, abstracting application development challenges. Committed to Open Source, she contributes to projects like Dapr and Testcontainers. She boasts a rich history in retail computer vision solutions and successfully stabilized industrial edge use cases with testing and diverse deployments for biopharma data pipelines. Her expertise extends to being CKAD certified and reviewing Go textbooks. She is passionate about empowering early-career, diverse professionals. Samantha is in a family of gophers, and enjoys GopherCon with her brother and identical twin sister. She's a seasoned speaker, having presented at various conferences, including GopherCon.
Read more about Samantha Coyle

Right arrow

Concurrent Work

Overview

This chapter introduces you to Go features that will allow you to perform concurrent work, or, in other words, achieve concurrency. The first feature you will learn is called a Goroutine. You’ll learn what a Goroutine is and how you can use it to achieve concurrency. Then, you’ll learn how to utilize WaitGroups to synchronize the execution of several Goroutines. You will also learn how to implement synchronized and thread-safe changes to variables shared across different Goroutines using atomic changes. To synchronize more complex changes, you will work with mutexes.

Later in the chapter, you will experiment with the functionalities of channels and use message tracking to track the completion of a task. We will also cover the importance of concurrency, concurrency patterns, and more.

Technical requirements

For this chapter, you'll require Go version 1.21 or higher. The code for this chapter can be found at: https://github.com/PacktPublishing/Go-Programming-From-Beginner-to-Professional-Second-Edition-/tree/main/Chapter18.

Introduction

There is software that’s meant to be used by a single user, and most of what you’ve learned so far in this book allows you to develop such applications. There is other software, however, that is meant to be used by several users at the same time. An example of this is a web server. You created web servers in Chapter 16, Web Servers. They are designed to serve websites or web applications that are generally used by thousands of users at the same time.

When multiple users are accessing a web server, it sometimes needs to perform a series of actions that are totally independent and whose result is the only thing that matters to the final output. All these situations call for a type of programming in which different tasks can be executed at the same time, independently from each other. Some languages allow parallel computation, where tasks are computed simultaneously.

In concurrent programming, when a task starts, all other tasks start as well, but instead...

Goroutines

Imagine several people have some nails to hammer into a wall. Each person has a different number of nails and a different area of the wall, but there is only one hammer. Each person uses the hammer for one nail, then passes the hammer to the next person, and so on. The person with the fewest nails will finish earlier, but they will all share the same hammer; this is how Goroutines work.

Using Goroutines, Go allows multiple tasks to run at the same time (they are also called coroutines). These are routines (read tasks) that can co-run inside the same process but are totally concurrent. Goroutines do not share memory, which is why they are different from threads. However, we will see how easy it is to pass variables across them in your code and how this might lead to some unexpected behavior.

Writing a Goroutine is nothing special; they are just normal functions. Each function can easily become a Goroutine; all we must do is write the word go before calling the function...

WaitGroup

In the previous exercise, we used a not-so-elegant method to ensure that the Goroutine ended by making the main Goroutine wait for a second. The important thing to understand is that even if a program does not explicitly use Goroutines via the go call, it still uses one Goroutine, which is the main routine. When we run our program and create a new Goroutine, we are running two Goroutines: the main one and the one we just created. In order to synchronize these two Goroutines, Go gives us a function called WaitGroup. You can define a WaitGroup using the following code:

wg := sync.WaitGroup{}

WaitGroup needs the sync package to be imported. Typical code using the WaitGroup will be something like this:

package main
import "sync"
func main() {
  wg := &sync.WaitGroup{}
  wg.Add(1)
  …………………..
  wg.Wait()
  ………….
  …...

Race conditions

One important thing to consider is that whenever we run multiple functions concurrently, we have no guarantee in what order each instruction in each function will be performed. In many architectures, this is not a problem. Some functions are not connected in any way with other functions, and whatever a function does in its Goroutine does not affect the actions performed in other Goroutines. This is, however, not always true. The first situation we can think of is when some functions need to share the same parameter. Some functions will read from this parameter, while others will write to this parameter. As we do not know which operation will run first, there is a high likelihood that one function will override the value updated by another function. Let’s see an example that explains this situation:

func next(v *int) {
  c := *v
  *v = c + 1
}

This function takes a pointer to an integer as a parameter. It is a pointer because we want to...

Atomic operations

Let’s imagine we want to run independent functions again. However, in this case, we want to modify the value held by a variable. We still want to sum the numbers from 1 to 100, but we want to split the work into two concurrent Goroutines. We can sum the numbers from 1 to 50 in one routine and the numbers from 51 to 100 in another routine. At the end, we will still need to receive the value of 5050, but two different routines can add a number at the same time to the same variable. Let’s see an example with only four numbers where we want to sum 1, 2, 3, and 4, and the result is 10.

Think of it like having a variable called s := 0 and then making a loop where the value of s becomes the following:

s = 0
s = 1
s = 3 // (1 + 2)
s = 6
s = 10

However, we could also have the following loop. In this case, the order in which the numbers are summed is different:

s = 0
s = 1
s = 4 // 3 + 1, the previous value of 1
s = 6 // 2 + 4 the previous value of...

Invisible concurrency

We’ve seen in the previous exercise the effects of concurrency through race conditions, but we want to see them in practice. It is easy to understand that concurrency problems are difficult to visualize as they do not manifest in the same way every time we run a program. That’s why we are focusing on finding ways to synchronize concurrent work. One easy way to visualize it, however, but that is difficult to use in tests, is to print out each concurrent routine and see the order in which these routines are called. In the previous exercise, for example, we could have sent another parameter with a name and printed the name of the function at each iteration in the for loop.

If we want to see the effects of concurrency and still be able to test it, we could use the atomic package again, this time with strings so that we can build a string containing a message from each Goroutine. For this scenario, we will use the sync package again, but we will not...

Channels

We’ve seen how to create concurrent code via Goroutines, how to synchronize it with WaitGroup, how to perform atomic operations, and how to temporarily stop concurrency to synchronize access to shared variables. We will now introduce a different concept – the channel, which is typical of Go. A channel is what the name essentially suggests – it’s something where messages can be piped, and any Goroutine can send or receive messages through a channel. Similar to that of a slice, a channel is created the following way:

var ch chan int
ch = make(chan int)

Of course, it is possible to instantiate the channel directly with the following:

ch := make(chan int)

Just like with slices, we can also do the following:

ch := make(chan int, 10)

Here, a channel is created with a buffer of 10 items.

A channel can be of any type, such as integer, Boolean, float, and any struct that can be defined, and even slices and pointers, though the last two...

The importance of concurrency

So far, we’ve seen how to use concurrency to split work over several Goroutines, but in all of these exercises, concurrency was not really needed. In fact, you do not save much time doing what we did, nor do you have any other advantage. Concurrency is important when you need to perform several tasks that are logically independent of each other, and the easiest case to understand is a web server. You saw in Chapter 16, Web Servers, that several clients will most likely connect to the same server and all these connections will result in the server performing some actions. Also, these actions are all independent; that’s where concurrency is important, as you do not want one of your users to have to wait for all other HTTP requests to be completed before their request gets handled. Another case for concurrency is when you have different data sources to gather data and you can gather that data in different Goroutines and combine the result at...

Concurrency patterns

The way we organize our concurrent work is pretty much the same in every application. We will look at one common pattern that is called a pipeline, where we have a source, and then messages are sent from one Goroutine to another until the end of the line, until all Goroutines in the pipeline have been utilized. Another pattern is the fan out/ fan in pattern where, as in the previous exercise, work is sent to several Goroutines reading from the same channel. All these patterns, however, are generally made of a source stage, which is the first stage of the pipeline and the one that gathers, or sources, the data, then some internal steps, and at the end, a sink, which is the final stage where the results of the process from all the other routines get merged. It is known as a sink because all the data sinks into it.

Buffers

You’ve seen in the previous exercises that there are channels with a defined length and channels with an undetermined length:

ch1 := make(chan int)
ch2 := make(chan int, 10)

Let’s see how we can make use of this.

A buffer is like a container that needs to be filled with some content, so you prepare it when you expect to receive that content. We said that operations on channels are blocking operations, which means the execution of the Goroutine will stop and wait whenever you try to read a message from the channel. Let’s try to understand what this means in practice with an example. Let’s say we have the following code in a Goroutine:

i := <- ch

We know that before we can carry on with the execution of the code, we need to receive a message. However, there is something more about this blocking behavior. If the channel does not have a buffer, the Goroutine is blocked as well. It is not possible to write to a channel or to receive a...

HTTP servers

You’ve seen how to build HTTP servers in Chapter 16, Web Servers, but you might remember that there was something difficult to handle with HTTP servers, and this was the application’s state. Essentially, an HTTP server runs as a single program and listens to requests in the main Goroutine. However, when a new HTTP request is made by one of the clients, a new Goroutine is created that handles that specific request. You have not done it manually, nor have you managed the server’s channels, but this is how it works internally. You do not actually need to send anything across the different Goroutines because each Goroutine and each request is independent since they have been made by different people.

However, what you must think of is how to not create race conditions when you want to keep a state. Most HTTP servers are stateless, especially if you’re building a microservice environment. However, you might want to keep track of things with a...

Methods as Goroutines

So far, you’ve only seen functions used as Goroutines, but methods are simple functions with a receiver; hence, they can be used asynchronously too. This can be useful if you want to share some properties of your struct, such as for your counter in an HTTP server.

With this technique, you can encapsulate the channels you use across several Goroutines belonging to the same instance of a struct without having to pass these channels everywhere.

Here is a simple example of how to do that:

type MyStruct struct {}
func (m MyStruct) doIt()
. . . . . .
ms := MyStruct{}
go ms.doIt()

But let’s see how to apply this in an exercise.

Exercise 18.10 – a structured work

In this exercise, we will calculate a sum using several workers. A worker is essentially a function, and we will be organizing these workers into a single struct:

  1. Create your folder and main file. In it, add the required imports and define a Worker struct with two...

Go context package

We’ve seen how to run concurrent code and run it until it has finished, waiting for the completion of some processing through WaitGroup or channel reads. You might have seen in some Go code, especially code related to HTTP calls, some parameters from the context package, and you might have wondered what it is and why it is used.

All the code we’ve written here is running on our machines and does not pass through the internet, so we hardly have any delay due to latency; however, in situations involving HTTP calls, we might encounter servers that do not respond and get stuck. In such cases, how do we stop our call if the server does not respond after a while? How do we stop the execution of a routine that runs independently when an event occurs? Well, we have several ways, but a standard one is to use contexts, and we will see now how they work. A context is a variable that is passed through a series of calls and might hold some values or may be empty...

Concurrent work with sync.Cond

Efficient coordination between different Goroutines is crucial to ensure smooth execution and resource management. Another powerful synchronization primitive provided by the Go standard library is sync.Cond (condition). The Cond type is associated with sync.Mutex and provides a way for Goroutines to wait for or signal the occurrence of a particular condition or changes in shared data.

Let’s explore how to use sync.Cond by creating a simple example of a work-in-progress (WIP) limited queue.

Exercise 18.12 – creating a WIP limited queue

Suppose you have a scenario where multiple Goroutines produce and consume items, but you want to limit the number of items in progress currently. sync.Cond can help achieve this synchronization. Here’s how to use it:

  1. Create your folder and a main.go file, then write the following:
    package main
    import (
      "fmt"
      "sync"
      "time"...

The thread-safe map

In concurrent programming, safely managing access to shared data structures is crucial to avoid race conditions and ensure consistency. Go’s standard library provides a powerful tool for concurrent map access – the sync.Map type. Unlike the regular Map type, sync.Map is specifically designed to be used concurrently without the need for external synchronization.

The sync.Map type is part of the sync package and provides fine-grained locking internally to allow multiple readers and a single writer to access a map concurrently without blocking operations. This makes it suitable for scenarios where you have multiple Goroutines that need to read or modify a map concurrently.

Let’s look at an exercise showcasing the utility of sync.Map.

Exercise 18.13 – counting how many times random numbers are between 0 and 9 using sync.Map

Suppose we want to count how many times random numbers fall between the values of zero and nine in a concurrent...

Summary

In this chapter, you’ve learned how to create production-ready concurrent code, how to handle race conditions, and how to make sure that your code is concurrent-safe. You’ve learned how to use channels to make your Goroutines communicate with each other and how to stop their executions using a context.

You’ve worked on several techniques to handle concurrent computation and learned about sync.Cond and sync.Map as powerful tools in your toolbelt for concurrent programming In many real-life scenarios, you might just use functions and methods that handle concurrency for you, especially if you’re doing web programming, but there are cases where you must handle work coming from some different sources by yourself. You need to match requests with your response through different channels. You might need to gather different data into one single Goroutine from different ones. With what you’ve learned here, you’ll be able to do all that. You...

lock icon
The rest of the chapter is locked
You have been reading a chapter from
Go Programming - From Beginner to Professional - Second Edition
Published in: Mar 2024Publisher: PacktISBN-13: 9781803243054
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
undefined
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $15.99/month. Cancel anytime

Author (1)

author image
Samantha Coyle

Samantha Coyle, a Software Engineer at Diagrid, specializes in Go for cloud-native developer tooling, abstracting application development challenges. Committed to Open Source, she contributes to projects like Dapr and Testcontainers. She boasts a rich history in retail computer vision solutions and successfully stabilized industrial edge use cases with testing and diverse deployments for biopharma data pipelines. Her expertise extends to being CKAD certified and reviewing Go textbooks. She is passionate about empowering early-career, diverse professionals. Samantha is in a family of gophers, and enjoys GopherCon with her brother and identical twin sister. She's a seasoned speaker, having presented at various conferences, including GopherCon.
Read more about Samantha Coyle