PASSWORD RESET

Your destination for complete Tech news

Go tutorial – Chapter 4: Concurrency

285 0
5 min read

In this course, you will learn about concurrency in Go and how to use goroutines and channels to write concurrent programs. You will learn about the select statement and how to use it to communicate with multiple channels. You will also learn about the Communicating Sequential Processes (CSP) model and how it applies to Go’s concurrency model. You will learn about common concurrency patterns and how to avoid common pitfalls such as race conditions, deadlocks, and livelocks. By the end of this course, you will have a strong understanding of concurrency in Go and how to write concurrent programs effectively.

Introduction to concurrency

  • What is concurrency? Concurrency is the ability of a program to execute multiple tasks concurrently, or at the same time. This allows a program to make efficient use of resources and improve performance by running tasks in parallel.
  • Why is concurrency important in Go? Go is a concurrent programming language, which means it was designed to make it easy to write concurrent programs. Go has built-in support for concurrency through the use of goroutines and channels, which provide a simple and efficient way to write concurrent programs.

Goroutines

  • What are goroutines? Goroutines are lightweight threads of execution that run concurrently with other goroutines in the same program. Goroutines are created using the go keyword, which starts a new goroutine and returns immediately.
  • How to create and start a goroutine? To create and start a goroutine, you can use the go keyword followed by a function call:
go foo()

This creates a new goroutine and starts it by calling the foo function. The calling goroutine continues to execute concurrently with the new goroutine.

  • Example: printing numbers in parallel using goroutines Here is an example of using goroutines to print numbers in parallel:
package main

import (
    "fmt"
    "time"
)

func main() {
    for i := 0; i < 10; i++ {
        go func(n int) {
            fmt.Println(n)
        }(i)
    }
    time.Sleep(time.Second)
}
  • This program creates 10 goroutines and starts them by calling a function that prints the number passed as an argument. The main goroutine waits for 1 second before exiting, to give the other goroutines time to finish.

Channels

What are channels?

Channels are a way to communicate between goroutines. A channel is a type that allows you to send and receive values of a specific type.

How to create and use a channel?

To create a channel, you can use the make function:

ch := make(chan int)

This creates a new channel of type int. To send a value on a channel, you can use the <- operator:

ch <- 1

This sends the value 1 on the channel ch. To receive a value from a channel, you can use the <- operator:

n := <-ch

Example: using a channel to synchronize goroutines Here is an example of using a channel to synchronize goroutines:

package main

import (
    "fmt"
    "time"
)

func worker(done chan bool) {
    fmt.Print("working...")
    time.Sleep(time.Second)
    fmt.Println("done")
    done <- true
}

func main() {
    done := make(chan bool)
    go worker(done)
    <-done
}

This program creates a goroutine that sleeps for 1 second and then sends a value on the done channel. The main goroutine waits for the value to be received on the done channel, which synchronizes the execution of the two goroutines.

Select statement

What is the select statement?

The select statement is a way to choose from multiple communication operations. It allows a goroutine to wait on multiple channels and execute a case when a channel is ready.

How to use the select statement to communicate with multiple channels?

To use the select statement, you can use the select keyword followed by a list of cases:

select {
case <-ch1:
    // do something
case <-ch2:
    // do something else
default:
    // do something else if no channel is ready
}

This select statement waits for a value to be received on either ch1 or ch2, and executes the corresponding case. If neither channel is ready, the default case is executed.

Example: using the select statement to receive from multiple channels Here is an example of using the select statement to receive from multiple channels:

package main

import (
    "fmt"
    "time"
)

func main() {
    c1 := make(chan string)
    c2 := make(chan string)

    go func() {
        time.Sleep(time.Second * 1)
        c1 <- "one"
    }()
    go func() {
        time.Sleep(time.Second * 2)
        c2 <- "two"
    }()

    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-c1:
            fmt.Println("received", msg1)
        case msg2 := <-c2:
            fmt.Println("received", msg2)
        }
    }
}

This program creates two goroutines that send a value on a channel after sleeping for 1 second or 2 seconds, respectively. The main goroutine uses a `select

Communicating Sequential Processes (CSP)

What is CSP?

Communicating Sequential Processes (CSP) is a formal language for describing patterns of communication and cooperation among concurrent processes. It was developed by Tony Hoare in the 1970s and has been influential in the design of concurrent programming languages, including Go.

How does CSP apply to Go’s concurrency model?

Go’s concurrency model is based on CSP, which means it uses channels and message passing to communicate and coordinate between concurrent processes. In Go, goroutines and channels are used to implement CSP, allowing you to write concurrent programs using the concepts of process, channel, and message.

Concurrency patterns

Fan-out

Fan-out is a concurrency pattern where a single goroutine sends tasks to multiple goroutines. This allows the tasks to be processed concurrently and can improve the performance of the program.

Pipeline

Pipeline is a concurrency pattern where a series of goroutines are connected through channels and work together to process a stream of data. Each goroutine performs a specific task and passes the result to the next goroutine through a channel.

Example: using the pipeline pattern to process a stream of data concurrently Here is an example of using the pipeline pattern to process a stream of data concurrently:

package main

import (
    "fmt"
    "sync"
)

func gen(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        for _, n := range nums {
            out <- n
        }
        close(out)
    }()
    return out
}

func sq(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in {
            out <- n * n
        }
        close(out)
    }()
    return out
}

func main() {
    in := gen(2, 3)

    c1 := sq(in)
    c2 := sq(in)

    for n := range c1 {
        fmt.Println(n)
    }
    for n := range c2 {
        fmt.Println(n)
    }
}

This program creates a pipeline of three goroutines: gen, sq, and main. The gen goroutine generates a stream of numbers and sends them on a channel. The sq goroutine receives the numbers from the channel, squares them, and sends the result on another channel. The main goroutine receives the squared numbers from the second channel and prints them.

Concurrency pitfalls

Race conditions

A race condition is a situation where the outcome of a concurrent program depends on the timing of events. This can lead to unexpected behavior and incorrect results.

Deadlocks

A deadlock is a situation where two or more goroutines are blocked and waiting for each other to make progress, causing a standstill.

Livelocks

A livelock is a situation where two or more goroutines are continuously trying to make progress, but none of them can make progress because they are constantly blocked and unblocked by each other.

Example: fixing a race condition using mutual exclusion Here is an example of fixing a race condition using mutual exclusion:

package main

import (
    "fmt"
    "sync"
)

var counter int
var lock sync.Mutex

func increment(wg *sync.WaitGroup) {
    lock.Lock()
    defer lock.Unlock()
    counter++
    wg.Done()
}

func main() {
    var wg sync.WaitGroup
    wg.Add(100)
    for i := 0; i < 100; i++ {
        go increment(&wg)
    }
    wg.Wait()
    fmt.Println(counter)
}

This program uses a mutex to fix a race condition on the counter variable. The increment function acquires the mutex before accessing the counter variable and releases it after the operation is completed. This ensures that only one goroutine can access the counter variable at a time, preventing race conditions.

Conclusion

  • Recap of concurrency in Go In Go, concurrency is achieved through the use of goroutines and channels. Goroutines are lightweight threads of execution that run concurrently with other goroutines in the same program, and channels are a way to communicate between goroutines. Go also has a select statement to choose from multiple communication operations and a concurrency model based on CSP.
  • Tips for writing concurrent programs Here are some tips for writing concurrent programs in Go:
    • Use goroutines and channels to communicate and coordinate between concurrent processes.
    • Use the select statement to choose from multiple communication operations.
    • Use the CSP model to think about concurrent processes and their interactions.
    • Use concurrency patterns such as fan-out and pipeline to improve the performance of your program.
    • Avoid common pitfalls such as race conditions, deadlocks, and livelocks.
    • Use mutual exclusion to synchronize access to shared resources.

Leave A Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.