1. 首页
  2. 后端

Golang异步编程,快速理解Goroutines通信、各种锁的使用

  Golang异步编程,快速理解Goroutines通信、各种锁的使用

==================================

Golang以其简洁的语法、高效的并发模型以及内置的垃圾回收机制,在现代软件开发中占据了一席之地。特别是在处理高并发、分布式系统时,Go语言的并发特性尤为突出。其核心并发模型基于CSP(Communicating Sequential Processes,通信顺序进程)理论,主要通过goroutines和channels实现轻量级线程和安全的数据通信。

本文将带你入门Go语言中的异步编程方式,包括goroutines、channels、各种锁的基本使用,以及如何通过它们构建高效、安全的并发程序。

一、Goroutines:Go中的轻量级线程

概念介绍: Goroutine是Go并发模型的核心,它是Go函数的轻量级线程。与操作系统线程相比,goroutine的创建和切换成本非常低,使得在Go程序中可以轻松创建成千上万个goroutine来处理并发任务,而不会像传统线程那样消耗大量资源。

启动Goroutine: 在Go中,通过在函数调用前加上go关键字即可启动一个新的goroutine执行该函数。示例代码如下:

package main

import (
    "fmt"
    "time"
)

func say(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}

func main() {
    go say("world") // 启动一个goroutine执行say函数
    say("hello")    // 主goroutine同时执行say函数
}

在这个例子中,say("world")在一个新的goroutine中运行,而say("hello")直接在main goroutine中执行。由于goroutines的执行调度由Go运行时管理,因此”hello”和”world”的打印顺序不确定。

二、Channels:安全的数据共享

概念介绍: Channel是Go中用于goroutines之间通信的桥梁,它允许我们安全地发送和接收数据。Channel本身是类型化的,这意味着它只能传输特定类型的数据,并且支持同步和缓冲两种模式。

基本使用

  1. 无缓冲Channel:同步发送和接收,发送者必须等待接收者准备好接收数据。
  2. 带缓冲Channel:允许一定数量的数据暂存,从而解耦发送者和接收者的执行。

示例代码

package main

import "fmt"

func main() {
    // 创建一个无缓冲的channel
    ch := make(chan int)

    // 启动一个goroutine发送数据
    go func() {
        for i := 0; i < 5; i++ {
            ch <- i     // 发送数据到channel
        }
        close(ch) // 发送完毕后关闭channel
    }()

    // 主goroutine接收数据
    for data := range ch { // 使用for-range循环自动接收直到channel被关闭
        fmt.Println(data)
    }
}

三、Select:多路复用

概念介绍select语句用于监控多个channel的操作,类似于在其他语言中的事件监听或多路复用。它可以等待多个channel的操作完成,并根据完成的channel执行相应的代码块。

示例代码

package main

import (
    "fmt"
    "time"
)

func main() {
    // 创建两个channel
    c1 := make(chan string)
    c2 := make(chan string)

    // 启动两个goroutine向channel发送数据
    go func() {
        time.Sleep(1 * time.Second)
        c1 <- "one"
    }()
    go func() {
        time.Sleep(2 * time.Second)
        c2 <- "two"
    }()

    // 使用select监听两个channel
    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-c1:
            fmt.Println("received", msg1)
        case msg2 := <-c2:
            fmt.Println("received", msg2)
        }
    }
}

四、锁

在Go语言中,为了在并发环境下安全地访问共享资源,除了使用goroutines和channels进行通信同步外,还可以利用sync包提供的同步原语,如Mutex(互斥锁)、RWMutex(读写锁)等。读写锁(Read-Write Mutex或RWMutex)是一种特殊的锁,它允许多个读取者同时访问共享资源,但在任何时候只允许一个写入者。这种设计极大地提高了并发性能,特别是在读多写少的场景下。

RWMutex基础知识

  • 读锁(RLock):多个goroutine可以同时持有读锁,只要没有goroutine持有写锁。
  • 写锁(Lock):当有一个goroutine持有写锁时,任何其他goroutine(无论是读还是写)都必须等待,直到写锁释放。
  • 解锁:读锁和写锁分别通过RUnlockUnlock方法释放。

示例代码

下面的示例展示了如何使用sync.RWMutex来保护一个简单的计数器,实现并发安全的读写操作。

package main

import (
    "fmt"
    "sync"
    "time"
)

type Counter struct {
    mu     sync.RWMutex
    value  int
}

func (c *Counter) Inc() {
    c.mu.Lock()       // 获取写锁
    defer c.mu.Unlock() // 使用完后立即释放
    c.value++
}

func (c *Counter) Dec() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.value--
}

func (c *Counter) GetValue() int {
    c.mu.RLock()      // 获取读锁
    defer c.mu.RUnlock() // 读操作完成后释放读锁
    return c.value
}

func main() {
    counter := &Counter{}

    // 启动多个读goroutine
    for i := 0; i < 10; i++ {
        go func(id int) {
            for {
                val := counter.GetValue()
                fmt.Printf("Reader %d: Value = %d\n", id, val)
                time.Sleep(100 * time.Millisecond)
            }
        }(i)
    }

    // 启动一个写goroutine
    go func() {
        for {
            counter.Inc()
            counter.Dec()
            time.Sleep(500 * time.Millisecond)
        }
    }()

    // 让程序运行一段时间后退出
    time.Sleep(5 * time.Second)
}

在这个例子中,Counter结构体包含一个整型值value和一个RWMutexIncDec方法在修改value之前会先获取写锁,确保同一时刻只有一个goroutine能修改它。GetValue方法则只获取读锁,允许多个读goroutine并发执行,提高了效率。

五、Context:优雅地取消和超时

概念介绍context包提供了context类型,用于在API之间传递请求的截止时间、取消信号等信息。这对于管理长时间运行的goroutines特别有用,可以优雅地取消操作并清理资源。

示例代码

package main

import (
    "context"
    "fmt"
    "time"
)

func longRunningTask(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done(): // 检查是否被取消
            fmt.Printf("Task %d was canceled\n", id)
            return
        default:
            fmt.Printf("Task %d is running...\n", id)
            time.Sleep(500 * time.Millisecond)
        }
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) // 设置2秒超时
    defer cancel() // 在结束时取消

    go longRunningTask(ctx, 1)
    time.Sleep(3 * time.Second) // 等待一段时间以观察输出
}

六、总结

Go语言通过goroutines和channels提供了一种简洁而强大的并发编程模型,使得开发者能够轻松地编写出高并发、高性能的应用程序。掌握这些基本概念和技巧,是进行Go异步编程的基础。实践这些示例,并尝试将它们应用到实际项目中,将有助于深入理解Go的并发机制,进而构建更加复杂和高效的系统。

原文链接: https://juejin.cn/post/7363650007269392399

文章收集整理于网络,请勿商用,仅供个人学习使用,如有侵权,请联系作者删除,如若转载,请注明出处:http://www.cxyroad.com/17360.html

QR code