2 - “go语言进阶与依赖管理” | 青训营

68 阅读4分钟

“go语言进阶与依赖管理” 并发-goroutine-channe-lock-WaitGroup

1. 并发——并行

并发:多线程程序在同一核的CPU运行

并行:多线程程序在多个核的CPU运行

2023-05-24-17-20-41.png

协程:用户态,轻量级线程,由go调度控制,KB级的栈

线程:内核态,跑多个协程,MB级的栈

  • 线程切换需要陷入内核,然后进行上下文切换,而协程在用户态由协程调度器完成
  • 协程的切换时间点是由调度器决定,而不是由系统内核决定的
  • 垃圾回收的必要条件是内存位于一致状态,需要暂停所有的线程。对于Go语言来说,调度器知道什么时候内存位于一致状态,所以也就没有必要暂停所有运行的线程
  • Go的协程采用了动态扩张收缩的策略,初始化为2KB

1.1 goroutine

每一个并发的执行单元叫作一个goroutine

使用goroutine快速打印


func hello(i int) {
    println("hello: " + fmt.Sprint(i))
}
func helloGoroutine() {
    for i := 0; i < 5; i++ {
        go func(input_i int) {
            hello(input_i)
        }(i) //i参数
    }
    time.Sleep(time.Second) //1s后 保证子协程执行完之前主协程不要退出
    fmt.Printf("end")
}  //打印乱序 并行

​运行结果如下图。可以看到输出是随机顺序的,输出任务是并行的。 2023-05-23-00-06-33.png

1.2 CSP

2023-05-24-17-24-40.png

通过内存共享实现通信

通过通信实现内存共享(推荐)

  • 不要通过共享内存来实现通信,而是通过通信来实现共享内存。

1.3 channel/chan

管道,多用于多个 goroutine 之间通信,默认情况下,通道是双向的,这意味着goroutine可以通过同一通道发送或接收数据

make(chan 元素类型,\[缓冲大小])

  • 无缓冲通道 make(chan int) 同步通道,发送接收同步(发送者会阻塞直到接收者接收了发送的值)。如果连续向一个无缓冲channel 发送2个元素,并且没有接收的话,第二次一定会被阻塞
  • 有缓冲通道 make(chan int,2) 发送方在缓冲区满的情况下阻塞,接收方在缓冲区空的情况下阻塞,所以是“异步”的。
  • 关闭不再需要使用的 channel 并不是必须的。跟其他资源比如打开的文件、socket 连接不一样,这类资源使用完后不关闭后会造成句柄泄露,channel 使用完后不关闭也没有关系,channel 没有被任何协程用到后最终会被 GC 回收。关闭 channel 一般是用来通知其他协程某个任务已经完成了。

2023-05-24-17-10-59.png

func CalSquare() {
    src := make(chan int)   //无缓冲通道
    dest := make(chan int, 3)   // 有缓冲
    go func() { //子协程A发送0~9
        defer close(src)
        for i := 0; i < 10; i++ {
            src <- i //发给src channel
        }
    }()
    go func() { //协程B计算数字的平方
        defer close(dest)
        for i := range src { //遍历src
            dest <- i * i
        }
    }()
    for i := range dest { //主协程输出平方结果
        println(i)
    }
}

子协程A发送0~9,子协程B计算数字的平方,主协程输出平方结果。

无缓冲通道保证了输出的顺序,并发安全。有缓冲通道解决了生产消费效率问题。

2023-05-24-17-16-30.png

1.4 lock

将变量执行2000次+1,5个协程并发执行

var (
    x    int
    lock sync.Mutex
)
func addLock() {
    for i := 0; i < 2000; i++ {
        lock.Lock() //加锁
        x += 1
        lock.Unlock() //解锁
    }
}
func add() {
    for i := 0; i < 2000; i++ {
        x += 1
    }
}
func main() {
    x = 0
    for i := 0; i < 5; i++ {
        go add()
    }
    time.Sleep(time.Second)
    fmt.Println("no lock:", x)
    x = 0
    for i := 0; i < 5; i++ {
        go addLock()
    }
    time.Sleep(time.Second)
    fmt.Println("lock:", x)
}

线程同步时,可能会有多个线程需要使用这个资源,为了避免资源竞争,我们需要锁机制。无锁的共享内存可能产生安全问题。

2023-05-28-23-18-16.png

1.5 WaitGroup

sync.WaitGroup结构体对象用于等待一组线程的结束,可以通过WaitGroup来表达这一组协程的任务是否完成,以决定是否继续往下走,或者取任务结果;

计数器:协程Add(delta int)内部计数器加上delta,delta可以是负数,Done()结束-1;Wait()将主协程阻塞到计数器为0

type WaitGroup struct { 
    noCopy noCopy 
    state1 [3]uint32 
}
func goWait() {
    var wg sync.WaitGroup
    wg.Add(5)
    for i := 0; i < 5; i++ {  
        go func(input_i int) {
            defer wg.Done()
            hello(input_i)
        }(i)
    }
    wg.Wait()
    fmt.Println("end")
}

2023-05-24-22-45-59.png

23/7/27