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

77 阅读3分钟

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

本文主要记录 goroutine 并发模式

程序通常是顺序执行并完成一个独立的任务,不过也有很多需求是需要并行执行多个任务,这样可以极大地减少任务执行总时间。比如 Web 中,需要在各自独立的接口上同时处理多个数据请求。

Go 语言中的并发是指某个函数能独立于其他函数运行。同步模型来自 CSP(通信顺序进程)。它是一种消息传递模型,通过在 goroutine 之间传递数据来传递消息,而不是通过对数据加锁来实现同步访问。在 goroutine 之间同步和传递数据使用的数据结构叫通道(channel)。

并发和并行

当一个应用程序运行时,系统会为这个程序开启一个进程,并且分配在运行中可能使用到的各种资源。 每个进程至少包括一个线程,进程的初始线程被称为主线程,当主线程停止后,整个应用程序也会关闭。

并行不是并发。并行指任务在不同的处理器单元上同时执行。而并发是逻辑上同时运行,一个任务可以被暂停,然后处理器资源可以去处理其他任务,这样就达到了同时运行多个任务的目的。

goroutine

package main

import (
	"fmt"
	"time"
)

func printNum(num int) {
	for i := 0; i < 3; i++ {
		fmt.Println(num, i)
	}
}

func main() {
	go printNum(1)
	go printNum(2)
	time.Sleep(time.Duration(time.Second))
}

输出结果

1 0
2 0
2 1
2 2
1 1
1 2

如果不加 time.Sleep(time.Duration(time.Second)) 这行代码,没有输出结果,因为 printNum 还没有执行完毕,主线程就结束了。

这里声明了一个 printNum 函数进行数据打印,num 为 goroutine 编号。使用 go 关键字来创建 goroutine。可以看出结果并不是按照顺序执行的输出,因为这两个 goroutine 是并发执行的。

还可以使用 sync.WaitGroup 来监控 goroutine 是否完成。

互斥锁

package main

import (
	"fmt"
	"sync"
)

var count int
var wg sync.WaitGroup

func printNum(num int) {
	defer wg.Done()
	for i := 0; i < 10000; i++ {
		count++
	}
}

func main() {
	wg.Add(2)
	go printNum(1)
	go printNum(2)
	wg.Wait()
	fmt.Println(count)
}

上述代码输出 12301(每次均不同),原因是 count 临界资源,两个 goroutine 竞争的时候发生了读写问题。

可以在临界区加入互斥锁(mutux)来保证正确的输出。

var mutex sync.Mutex

func printNum(num int) {
	mutex.Lock()
	defer wg.Done()
	for i := 0; i < 10000; i++ {
		count++
	}
	mutex.Unlock()
}

如上所示,使用 mutex.Lock()mutex.Unlock() 来锁住临界代码。

通道

使用通道,通过发送和接受需要共享的资源,当一个资源需要在 goroutine 之间共享是,通道就是其桥梁。

c := make(chan int)  // 无缓冲的通道
c3 := make(chan int, 3)  // 有缓冲的通道
c <- 1  // 写入通道
v := <- c // 读取通道内容

下面是一个简单同步示例来保证资源安全访问

package main

import (
	"fmt"
	"time"
)

func main() {
	c := make(chan int)
	go func() {
		time.Sleep(time.Duration(time.Second * 3))
		c <- 1
	}()
	fmt.Println(<-c)
}

输出结果为 1,可以发现 main 函数中并未使用时间延时来保持主线程,但是还是得到了正确的输出结果。

原因是当程序运行到 fmt.Println(<-c) 后,读取通道会被锁住,等待通道 c 中被写入数据。当执行到 c <- 1,写入通道代码也会锁住,等数据交换完成后,代码立即执行。

func main() {
	c := make(chan int)
	go func() {
		time.Sleep(time.Duration(time.Second * 3))
		fmt.Println(<-c)

	}()
	c <- 1
}
// 输出 1
func main() {
	c := make(chan int, 1)
	go func() {
		time.Sleep(time.Duration(time.Second * 3))
		fmt.Println(<-c)

	}()
	c <- 1
}
// 无任何输出

若是仅写入或者仅读取均会错误,因为已经造成了死锁。