Golang High-Performance Programming EP6 : Tips For Asynchronous Programming

 

Introduction

The primary mission of Golang is to simplify asynchronous programming. When faced with operations requiring batch processing and lengthy execution times, traditional single-threaded execution becomes cumbersome, prompting the need for asynchronous parallel processing. This article introduces some tips for asynchronous programming in Golang.

This article was first published in the Medium MPP plan. If you are a Medium user, please follow me on Medium. Thank you very much.

sourcegraph/conc

Let’s start by introducing a library that simplifies concurrent programming: conc. It encapsulates many useful tools, such as WaitGroup and iter.Map. While we may not necessarily use conc in production code, learning from its concepts is still beneficial.

Usage

Using go

The simplest and most common method: use the go keyword.

1
2
3
4
5
6
7
8
func main() {  
 go func() {  
  fmt.Println("hello world1")  
 }()  
 go func() {  
  fmt.Println("hello world2")  
 }()  
}

Or:

1
2
3
4
5
6
7
func main() {  
 go Announce("hello world1")  
 go Announce("hello world2")  
}  
func Announce(message string) {  
 fmt.Println(message)  
}

Using anonymous functions to pass parameters:

1
2
3
4
5
data := "Hello, World!"  
go func(msg string) {  
 // Use msg to perform asynchronous task logic processing
 fmt.Println(msg)  
}(data)

This method doesn’t require consideration of return values. If return values are needed, the following method can be used.

Implementing Timeout Control with Goroutines and Channels

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
ch := make(chan int, 1)  
timer := time.NewTimer(time.Second)  
go func() {  
    time.Sleep(2 * time.Second)  
    ch <- 1  
    close(ch)  
}()  
select {  
case <-timer.C:  
    fmt.Println("timeout")  
case result := <-ch:  
    fmt.Println(result)  
}

Using sync.WaitGroup

sync.WaitGroup is used to wait for a collection of goroutines to finish their tasks. The Add() method increases the number of goroutines to wait for, the Done() method marks a goroutine as completed, and the Wait() method blocks until all goroutines are finished.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var wg sync.WaitGroup  
// Start multiple goroutines  
for i := 0; i < 5; i++ {  
   wg.Add(1)  
   go func(index int) {  
      defer wg.Done()  
      // Asynchronous task logic  
   }(i)  
}  
wg.Wait()

Error Handling with errgroup for Goroutine Groups

The errgroup package is useful for easily capturing errors from goroutines. It’s a utility in the Go standard library for managing a group of goroutines and handling their errors.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var eg errgroup.Group  
for i := 0; i < 5; i++ {  
    eg.Go(func() error {  
     return errors.New("error")  
    })  

    eg.Go(func() error {  
     return nil  
    })  
}  

if err := eg.Wait(); err != nil {  
    // Handle error  
}

Tips and Techniques

Using Range and Close Operations with Channels

The range can be used to iterate over the values received on a channel until the channel is closed. Use the close function to close the channel, signaling no more values will be sent.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
ch := make(chan int)  

go func() {  
    for i := 0; i < 5; i++ {  
        ch <- i // Send values to the channel  
    }  
    close(ch) // Close the channel  
}()  

// Use range to iterate over received values
for val := range ch {  
    // Process received values  
}

Waiting Multiple Goroutines With Select

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
ch1 := make(chan int)  
ch2 := make(chan string)  

go func() {  
    // Asynchronous task 1 logic  
    ch1 <- result1  
}()  

go func() {  
    // Asynchronous task 2 logic  
    ch2 <- result2  
}()  

// Wait for multiple asynchronous tasks to complete in the main goroutine
select {  
case res1 := <-ch1:  
    // Process result 1  
case res2 := <-ch2:  
    // Process result 2  
}

Implementing Timeout Control with Select and time.After()

If you need to set a timeout for asynchronous operations, you can use the select statement in conjunction with the time.After() function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
ch := make(chan int)  

go func() {  
    // Asynchronous task logic  
    time.Sleep(2 * time.Second)  
    ch <- result  
}()  

// Set a timeout
select {  
case res := <-ch:  
    // Process result  
case <-time.After(3 * time.Second):  
    // Handle timeout  
}

Using time.Tick() and time.After() for Timed Operations

The time.Tick() function returns a channel that sends time values periodically, useful for executing timed operations. The time.After() function returns a channel that sends a time value after a specified duration.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
tick := time.Tick(1 * time.Second) // Execute an operation every second  

for {  
    select {  
    case <-tick:  
        // Perform timed operation  
    }  
}  

select {  
case <-time.After(5 * time.Second):  
    // Execute operation after 5 seconds  
}

Using sync.Mutex or sync.RWMutex for Concurrent Safe Access

When multiple goroutines concurrently access shared data, it’s essential to ensure data access safety. sync.Mutex and sync.RWMutex provide mutual exclusion locks and read-write locks for locking before accessing shared resources, preventing data races.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var mutex sync.Mutex  
var data int  

// Write operation protected by a mutex
mutex.Lock()  
data = 123  
mutex.Unlock()  

// Read operation protected by a read lock
mutex.RLock()  
value := data  
mutex.RUnlock()  

var rwMutex sync.RWMutex  
var sharedData map[string]string  

// Read operation protected by a read lock
func readData(key string) string {  
    rwMutex.RLock()  
    defer rwMutex.RUnlock()  
    return sharedData[key]  
}  

// Write operation protected by a write lock
func writeData(key, value string) {  
    rwMutex.Lock()  
    defer rwMutex.Unlock()  
    sharedData[key] = value  
}

Note: sync.Mutex locks cannot be nested. sync.RWMutex read locks (RLock()) can be nested if there are no write locks, and multiple read locks can be acquired.

Using sync.Cond for Conditional Variable Control

sync.Cond is a conditional variable used for communication and synchronization between goroutines. It can block and wait until a specified condition is met, then wake up waiting goroutines when the condition is satisfied.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
var cond = sync.NewCond(&sync.Mutex{})  
var ready bool  

go func() {  
    // Asynchronous task logic  
    ready = true  

    // Notify waiting goroutines that the condition is met
    cond.Broadcast()  
}()  

// Wait for the condition to be met
cond.L.Lock()  
for !ready {  
    cond.Wait()  
}  
cond.L.Unlock()

Managing Object Pools with sync.Pool

sync.Pool is an object pool for caching and reusing temporary objects, improving allocation and recycling efficiency.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
type MyObject struct {  
    // Object structure  
}  

var objectPool = sync.Pool{  
    New: func() interface{} {  
        // Create a new object  
        return &MyObject{}  
    },  
}  

// Get an object from the pool
obj := objectPool.Get().(*MyObject)  

// Use the object  

// Put the object back into the pool
objectPool.Put(obj)

Ensuring One-Time Execution with sync.Once

sync.Once ensures that an operation is executed only once, regardless of how many goroutines attempt to execute it. It’s commonly used for initialization or loading resources.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
var once sync.Once  
var resource *Resource  

func getResource() *Resource {  
    once.Do(func() {  
        // Perform resource initialization, executed only once
        resource = initResource()  
    })  
    return resource  
}  

// Get the resource in multiple goroutines
go func() {  
    res := getResource()  
    // Use the resource  
}()  

go func() {  
    res := getResource()  
    // Use the resource  
}()

Resource Cleanup with sync.Once and context.Context

Combine sync.Once and context.Context to ensure a resource cleanup operation is executed only once across multiple goroutines, triggered on cancellation or timeout.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
var once sync.Once  

func cleanup() {  
    // Perform resource cleanup  
}  

func doTask(ctx context.Context) {  
    go func() {  
        select {  
        case <-ctx.Done():  
            once.Do(cleanup) // Execute resource cleanup only once
        }  
    }()  

    // Asynchronous task logic
}

Concurrent Safe Maps with sync.Map

sync.Map is a concurrent-safe map type in the

Go standard library, enabling safe read and write operations across multiple goroutines.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var m sync.Map  

// Store key-value pairs
m.Store("key", "value")  

// Retrieve values
if val, ok := m.Load("key"); ok {  
    // Use the value  
}  

// Delete a key
m.Delete("key")

Managing Goroutines and Cancellation with context.Context

context.Context is used to pass context information between goroutines and can be used for cancellation or timeout control. Use context.WithCancel() to create a cancellable context, and context.WithTimeout() to create a context with a timeout.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
ctx, cancel := context.WithCancel(context.Background())  

go func() {  
    // Asynchronous task logic  
    if someCondition {  
        cancel() // Cancel the task  
    }  
}()  

// Wait for the task to complete or be canceled
select {  
case <-ctx.Done():  
    // Task canceled or timed out  
}

Setting Deadlines with context.WithDeadline() and context.WithTimeout()

context.WithDeadline() and context.WithTimeout() functions create contexts with deadlines to limit the execution time of asynchronous tasks.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func doTask(ctx context.Context) {  
    // Asynchronous task logic  

    select {  
    case <-time.After(5 * time.Second):  
        // Handle timeout  
    case <-ctx.Done():  
        // Handle context cancellation  
    }  
}  

func main() {  
    ctx := context.Background()  
    ctx, cancel := context.WithTimeout(ctx, 3*time.Second)  
    defer cancel()  

    go doTask(ctx)  

    // Continue with other operations  
}

Passing Context Values with context.WithValue()

context.WithValue() allows passing key-value pairs within a context, enabling sharing and passing context-related values between goroutines.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
type keyContextValue string  

func doTask(ctx context.Context) {  
    if val := ctx.Value(keyContextValue("key")); val != nil {  
        // Use the context value  
    }  
}  

func main() {  
    ctx := context.WithValue(context.Background(), keyContextValue("key"), "value")  
    go doTask(ctx)  

    // Continue with other operations  
}

Atomic Operations with the atomic Package

The atomic package provides functions for atomic operations, ensuring atomicity in read and write operations on shared variables in concurrent environments.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
var counter int64

func increment() {
    atomic.AddInt64(&counter, 1)
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            increment()
        }()
    }
    wg.Wait()
    fmt.Println("Counter:", counter)
}

Summary

In this article, we introduce some commonly used keywords. Mastering these keywords will make it easy to deal with common concurrent programming. I believe you have also discovered that the go source code does not provide the commonly used Barrier function in java and c#. Although we can also implement the Barrier function using basic concurrent statements, it is not as convenient as calling the ready-made API interface. There is SingleFlight in golang.org/x and the third-party CyclicBarrier. In the next article, we will introduce these two concurrent primitives.

Licensed under CC BY-NC-SA 4.0
Last updated on Jul 22, 2024 16:47 CST
Built with Hugo
Theme Stack designed by Jimmy