Go 语言入门指南:并发入门| 青训营

109 阅读5分钟

go语言并发入门

本文中内容和代码基本都为原创,在结束部分有参考(或者说是学习总结而来的,毕竟概念没法原创)今天的课程和网上的相关介绍

当谈论 Go (Golang) 语言中的并发时,有三个主要方面需要考虑:goroutinechannelsync 。这些是 Go 语言中实现并发的核心

  1. Goroutine(协程): Goroutine 是 Go 语言中轻量级的执行单位。它们是由 Go 运行时管理的并发执行的函数或方法。相比于传统的线程,Goroutine 更轻量,启动和销毁代价较低,并且可以更高效地处理大量的并发任务。

    使用 Goroutine 很简单,只需在函数或方法调用前加上 go 关键字即可启动一个 Goroutine。例如:

    package main
    ​
    import (
        "fmt"
        "strconv"
        "time"
    )
    ​
    func work1(x int) {
        fmt.Println(strconv.Itoa(x) + ":process is starting ")
        time.Sleep(time.Duration(x) * time.Second)
        fmt.Println(strconv.Itoa(x) + ":process ends ")
    }
    func main() {
    ​
        go work1(1)
        go work1(2)
        go work1(3)
        go work1(4)
        time.Sleep(5 * time.Second)
    }
    ​
    

    上面是我在go中进行的一个使用goroutine的并发例子,就是一个函数中,在他开始的时候,让它输出一个值,然后进行一段时间的阻塞,再让它结束的时候输出一下。

    4:process is starting
    2:process is starting
    1:process is starting
    3:process is starting
    1:process ends 
    2:process ends 
    3:process ends 
    4:process ends 

    可以看到它成功的如预期了

    Goroutine 之间的通信通常通过 channel 实现,它是连接不同 Goroutine 的管道。

  2. Channel(通道): Channel 是一种用于在 Goroutine 之间传递数据的通信机制。它可以让不同的 Goroutine 安全地交换数据,从而实现协调和同步。

    在 Go 中,要通过 make 函数来创建一个 Channel,指定通道中元素的类型。例如,创建一个整数类型的通道:

    ch := make(chan int)
    

    信道是带有类型的管道,你可以通过它用信道操作符 <- 来发送或者接收值。

    ch <- v    // 将 v 发送至信道 ch。
    v := <-ch  // 从 ch 接收值并赋予 v。
    

    信道可以是 带缓冲的。将缓冲长度作为第二个参数提供给 make 来初始化一个带缓冲的信道:

    ch := make(chan int, 100)
    

    信道如果你想像流一样的理解使用,go的channel有range和close两者,前者进行循环会对信道上的内容进行监听和接收值,后者是信道的发送者用来关闭信道的

    监听者使用range进行监听的时候,可以有两个参数,后者是用来检测的,如信道是否没有值可以接收且信道已关闭,则其值为falsee

    v, ok := <-ch
    

    我写了一个如下的代码来验证和测试channel的功能,不幸的是,它需要后面sync的waitgroups中的知识点,导致我一开始搞了好久才搞明白那里出了问题(遇到了死锁)

    package main
    ​
    import (
        "fmt"
        "strconv"
        "sync"
    )
    ​
    // 我当时设想的这个代码要能正常实现功能还需要后面的sync.waitgroups来进行结束了的监测
    func addi(st int, ed int, ch chan<- int, wg *sync.WaitGroup) {
        for i := st; i < ed; i++ {
            ch <- i
        }
        fmt.Println(strconv.Itoa(st) + "to" + strconv.Itoa(ed) + "done")
        wg.Done()
    }
    func demon(wg *sync.WaitGroup, ch chan int) {
        wg.Add(3)
        go addi(1, 1000, ch, wg)
        go addi(1001, 2000, ch, wg)
        go addi(2001, 3000, ch, wg)
        wg.Wait()
        defer func() {
            fmt.Println("all goroutine done")
            close(ch)
    ​
        }()
    }
    func sums(ch chan int, wg1 *sync.WaitGroup) {
        sumi := 0
        for i := range ch {
            sumi += i
        }
        fmt.Println(sumi)
        wg1.Done()
    ​
    }
    func main() {
        ch := make(chan int, 5)
        wg := sync.WaitGroup{}
        wg1 := sync.WaitGroup{}
        wg1.Add(1)
        go demon(&wg, ch)
        go sums(ch, &wg1)
        wg1.Wait()
    ​
    }
    ​
    

    输出如下:

    2001 to 3000 done
    1 to 1000 done
    1001 to 2000 done
    all goroutine done
    Sum: 4495500
    
    
    
  3. Sync(同步): sync 包提供了例如 Mutex、RWMutex、WaitGroup 等来帮助管理并发访问共享资源和实现同步。这些原语可以防止多个 Goroutine 同时访问共享资源,从而避免竞态条件和数据竞争。

    • Mutex(互斥锁):用于保护临界区,确保同一时间只有一个 Goroutine 可以进入临界区代码。防止多个 Goroutine 同时修改共享数据。
    package main
    ​
    import (
        "fmt"
        "sync"
        "time"
    )func sets(mu *sync.Mutex, x *int) {
    ​
        (*mu).Lock()
        (*x) = (*x) + 1
        (*mu).Unlock()
    }
    ​
    func main() {
        mu := sync.Mutex{}
        ans := 0
        for i := 0; i < 1000; i++ {
            go sets(&mu, &ans)
        }
        time.Sleep(4 * time.Second)
        fmt.Println(ans)
    ​
    }
    ​
    
    • RWMutex 全称为 "读写互斥锁"(Read-Write Mutex),它提供了读写锁的功能,可以在多个 Goroutine 之间有效地管理对共享资源的访问。

      1. 读锁定和写锁定互斥: 在任何时候,RWMutex 要么可以被多个 Goroutine 同时读取(共享访问),要么可以被单个 Goroutine 写入(互斥访问)。在写锁定状态下,任何其他 Goroutine 都无法获得读锁定或写锁定,直到写锁定被释放。
      2. 多读锁定优先: 在读锁定状态下,多个 Goroutine 可以同时获得读锁定,实现了多读单写的并发模型。只有在没有读锁定的情况下,写锁定才能获得。

      RWMutex 提供了以下方法:

      • func (*RWMutex) Lock():获取写锁定。如果已经有其他 Goroutine 持有读锁定或写锁定,则调用 Lock() 的 Goroutine 将阻塞,直到它获得写锁定为止。
      • func (*RWMutex) Unlock():释放写锁定。
      • func (*RWMutex) RLock():获取读锁定。如果已经有其他 Goroutine 持有写锁定,则调用 RLock() 的 Goroutine 将阻塞,直到它获得读锁定为止。
      • func (*RWMutex) RUnlock():释放读锁定。

    (和数据库差不多)

    • WaitGroup(等待组):用于等待一组 Goroutine 完成任务。它可以阻塞主 Goroutine,直到所有其他 Goroutine 完成工作。

      WaitGroup中有三种操作,分别是wair,add,done

      • ADD(x)计数器中增加x个计数
      • DONE()表面当前的完成,计数器中-1
      • WAIT()阻塞使其等待直到计数器为0
    package main
    ​
    import (
        "fmt"
        "math/rand"
        "sync"
        "time"
    )
    ​
    func waiters(x string, wg *sync.WaitGroup) {
        fmt.Println(x + "  come")
        time.Sleep(time.Duration(rand.Intn(5)) * time.Second)
        defer func() {
    ​
        }()
        wg.Done()
        fmt.Println(x + "  go")
    }
    ​
    func main() {
        wg := sync.WaitGroup{}
        wg.Add(4)
        go waiters("alex", &wg)
        go waiters("wangcr", &wg)
        go waiters("yaossg", &wg)
        go waiters("dnk", &wg)
        wg.Wait()
        defer fmt.Println("all done")
    }
    ​
    

这三个方面共同组成了 Go 语言中并发的基础。使用 Goroutine 和 Channel 可以轻松实现并发任务的执行和数据交换,而 Sync 包中的原语可以保护共享资源和确保 Goroutine 之间的同步。这些特性使得 Go 在处理并发任务时非常强大和高效。