《Go 语言上手 - 工程实践》笔记 | 青训营笔记

89 阅读4分钟

《Go 语言上手 - 工程实践》笔记| 青训营笔记

这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记

并发编程

协程 Goroutine

进程(Process),线程(Thread),协程(Coroutine,也叫轻量级线程)

  • 进程进程是一个程序在一个数据集中的一次动态执行过程,它是CPU资源分配和调度的独立单位。进程的局限是创建、撤销和切换的开销比较大。
  • 线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,一个进程可以包含多个线程。 线程的优点是减小了程序并发执行时的开销,提高了操作系统的并发性能,缺点是线程没有自己的系统资源,但同一进程的各线程可以共享进程所拥有的系统资源。
  • 协程协程是一种用户态的轻量级线程,又称微线程,协程的调度完全由用户控制。 通常将协程和子程序(函数)比较着理解。 子程序调用总是一个入口,一次返回,一旦退出即完成了子程序的执行。

协程的特点在于是一个线程执行,与多线程相比,其优势体现在:协程的执行效率极高。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。

go中使用Goroutine来实现并发concurrently。Goroutine是与其他函数或方法同时运行的函数或方法。Goroutines可以被认为是轻量级的线程。与线程相比,创建Goroutine的成本很小,它就是一段代码,一个函数入口。以及在堆上为其分配的一个堆栈(初始大小为4K,会随着程序的执行自动增长删除)。因此它非常廉价,Go应用程序可以并发运行数千个Goroutines。

 package main
 ​
 import (
 "fmt"
 )
 ​
 func hello() {
     fmt.Println("Hello world goroutine")
 }
 func main() {
     go hello()
     time.Sleep(1 * time.Second)
     fmt.Println("main function")
 }

在上面的程序中,我们已经调用了时间包的Sleep方法,它会在执行过程中睡觉。在这种情况下,main的goroutine被用来睡觉1秒。现在调用go hello()有足够的时间在main Goroutine终止之前执行。这个程序首先打印Hello world goroutine,等待1秒,然后打印main函数。

通道 Channel

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

  • 无缓冲通道 make(chan int)
  • 有缓冲通道 make(chan int, 2)

image-20220520103953460.png

 package concurrence
 ​
 func CalSquare() {
     src := make(chan int)
     dest := make(chan int, 3)
     //A子协程发送0~9数字
     go func() {
         defer close(src)
         for i := 0; i < 10; i++ {
             src <- i
         }
     }()
     //B子协程计算输入数字的平方
     go func() {
         defer close(dest)
         for i := range src {
             dest <- i * i
         }
     }()
     //主协程输出最后的平方数
     for i := range dest {
         println(i)
     }
 ​
 }
 ​

执行结果为:

image-20220520104219616.png

锁 Lock

对变量执行2000次+1操作,5个协程并发执行

 package concurrence
 ​
 import (
     "sync"
     "time"
 )
 ​
 var (
     x    int64
     lock sync.Mutex
 )
 ​
 func addWithLock() {
     for i := 0; i < 2000; i++ {
         lock.Lock()
         x += 1
         lock.Unlock()
     }
 }
 func addWithoutLock() {
     for i := 0; i < 2000; i++ {
         x += 1
     }
 }
 ​
 func Add() {
     x = 0
     for i := 0; i < 5; i++ {
         go addWithoutLock()
     }
     time.Sleep(time.Second)
     println("WithoutLock:", x)
     x = 0
     for i := 0; i < 5; i++ {
         go addWithLock()
     }
     time.Sleep(time.Second)
     println("WithLock:", x)
 }
 ​

执行结果为:

image-20220520110235572.png

上述结果可知,两个线程试图在几乎在同一时刻对同一个变量做增操作而不进行同步的话,结果就可能出现不一致。

线程同步 WaitGroup

WaitGroup用于线程同步,WaitGroup等待一组线程集合完成,才会继续向下执行。 主线程(goroutine)调用Add来设置等待的线程(goroutine)数量。 然后每个线程(goroutine)运行,并在完成后调用Done。 同时,Wait用来阻塞,直到所有线程(goroutine)完成才会向下执行。

 package main
 ​
 import (
     "fmt"
     "sync"
 )
 ​
 func main() {
     var wg sync.WaitGroup
     var urls = []string{
         "AAA",
         "BBB",
         "CCC",
     }
     for _, url := range urls {
         // Increment the WaitGroup counter.
         wg.Add(1)
         go func(url string) {
             // Launch a goroutine to fetch the URL.
             defer wg.Done()
             // Fetch the URL.
             fmt.Println(url)
         }(url)
     }
     // Wait for all goroutines to finish.
     wg.Wait()
     fmt.Println("GG")
 }

执行结果为:

 AAA
 BBB
 CCC
 GG

可以看出3个线程(goroutine)全部执行完成后,wg.Wait()才停止等待,程序继续往下执行,输出GG。