Day2 进阶Go语言 | 青训营笔记

76 阅读3分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天

一、并发编程

1.1 Goroutine/Channel

goroutine是go的协程实现,在函数前面写上关键字go,就表示go程序会起一个协程去执行这个函数。而channel是管道,用于协程之间的通信。管道有自己的类型和空间,可以理解成一个队列——协程a可以往管道里写数据,而协程b可以从管道里拿数据。管道是线程安全的,每次能有一个协程访问这个管道。下面是一个生产者消费者的例子,生产者依次生成0到9这几个数字,然后消费者消费这些数字并计算他们的平方交给用户。

首先我们用make来创建管道用来通信。make的第二个参数表示管道的大小,如果没有默认是0,也就是必须同步的读写。这里我们用了两个管道,第一个管道src是生产者用来生产数字给消费者的。消费者通过src消费了数字后,计算他们的平方,然后通过第二个管道dest传给用户。

func main() {
	src := make(chan int)
	dest := make(chan int, 3)

	go func() {
		defer close(src)
		for i := 0; i < 10; i++ {
			src <- i
		}
	}()

	go func() {
		defer close(dest)
		for i := range src {
			dest <- i * i
		}
	}()

	for v := range dest {
		fmt.Println(v)
	}
}

1.2 sync.Mutex

sync.Mutex是go提供的互斥量,对互斥量加锁解锁可以实现对临界资源的保护,避免并发安全问题。在sync包中,sync.Mutex有两个方法,Lock和Unlock,这两个方法就是标准的互斥量用法,在进入临界区的时候加锁,退出的时候解锁。协程如果想要进入临界区,必须先获得锁。如果已经有其他的协程获取锁,并进入了临界区。那么这个协程只能等锁释放。

下面是一个并发安全问题的例子,我们用5个协程对一个临界变量做自增,每个协程都自增2000次。如果不对x加锁,显然由于并发问题会导致实际上x值小于2000 * 5。

var x int
var lock sync.Mutex

func addWithLock() {
	for i := 0; i < 2000; i++ {
		lock.Lock()
		x++
		lock.Unlock()
	}
}

func addWithoutLock() {
	for i := 0; i < 2000; i++ {
		x++
	}
}

func main() {
	x = 0
	for i := 0; i < 5; i++ {
		go addWithLock()
	}
	time.Sleep(time.Second)
	fmt.Println(x)

	x = 0
	for i := 0; i < 5; i++ {
		go addWithoutLock()
	}
	time.Sleep(time.Second)
	fmt.Println(x)
}

1.3 WaitGroup

sync.WaitGroup主要是用来做协程之间的同步的。它适用于这样的场景,一个主协程有很多子协程。主协程希望等待所有的子协程的任务执行完了以后,再执行别的任务。这时候可以用WaitGroup实现这种需求。

WaitGroup的本质就是一个计数器,它会计数当前等待的协程数量。它有3个常用的方法Add,Done和Wait。

  • Add(i int) 表示计数器增加i
  • Done() 表示计数器减一
  • Wait() 这个方法一般是主协程调用,用来等待其他的协程执行完。
func hello(i int) {
	fmt.Println("hello goroutine,", i)
}

func HelloGoroutine() {
	wg := sync.WaitGroup{}
	wg.Add(5)
	for i := 0; i < 5; i++ {
		go func(i int) {
			defer wg.Done()
			hello(i)
		}(i)
	}
	wg.Wait()
}

func main() {
	HelloGoroutine()
}