这是我参与「第五届青训营 」伴学笔记创作活动的第 1 天
并发基本概念
首先区分一下并发和并行:
-
并发:并发指的是多个程序在同一时间段内被调度执行,一般是多个 CPU 同时运行多个程序。
-
并行:一般是一个 CPU 但是同时执行多个程序,具体执行可能是这样的,先执行程序 A,然后执行一小段时间之后快速切换到执行程序 B,然后再切换到 A,一般这里切换时间较短,肉眼看不出来,所以两个程序看起来好像在同时运行。
-
并发和并行看起来都是多个程序同时运行,但是背后的机制却完全不同。
然后就是常见的问题:进程、线程和协程的基本概念
-
进程是操作系统中资源分配和调度的基本单位,它是操作系统独立运行和管理的基本单位。每个进程都有自己的内存空间和系统资源,相互之间互不干扰。
-
线程是进程中的一个实体,是被操作系统独立调度和分派的基本单位,它是一条执行路径。线程可以共享进程的资源,但是拥有自己独立的执行栈和寄存器。
-
协程是用户态下的轻量级线程,相对于线程而言,它需要更少的系统资源。协程主要由用户自己控制调度,因此它可以更灵活地实现异步IO等操作,也可以利用协程来实现并发编程。
Go语言中的协程
Go 语言中开启一个协程非常容易,可以通过 go + 一个函数名来启动一个协程,例如:
go f(x, y)
下面是一个简单的并发示例,开 5 个协程执行分别执行 Hello 函数:
package main
import (
"fmt"
"time")
func Hello(n int) {
fmt.Println("hello ", n)
}
func main() {
for i := 1; i <= 5; i++ {
go Hello(i)
}
time.Sleep(time.Second)
}
/* 打印的结果顺序可能不一致
hello 1
hello 5
hello 2
hello 4
hello 3
*/
注意上面使用了 time.Sleep 函数来模拟协程执行的时间,如果不加上的话,两个协程可能会同时运行结束,看不到并发运行的结果。
Channel
无缓冲的 Channel
Go 语言中,Channel 是一种通信机制,用于在不同的协程之间传递数据。Channel 允许你在两个协程之间进行双向通信,并且保证了在这两个协程之间的数据传递是线程安全的。
使用关键字 make 来创建一个 Channel,并使用 <- 操作符来读写 Channel。
ch <- v // 将 v 发送至信道 ch。
v := <-ch // 从 ch 接收值并赋予 v。
(“箭头”就是数据流的方向)
下面是一个简单的例子,展示了如何在两个协程之间使用channel进行通信:
package main
import "fmt"
func sum(a []int, c chan int) {
total := 0
for _, v := range a {
total += v
}
c <- total
}
func main() {
a := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(a[:len(a)/2], c)
go sum(a[len(a)/2:], c)
x, y := <-c, <-c
fmt.Println(x, y, x+y) // -5 17 12
}
这个程序中,我们使用了两个协程来计算一个数组中元素的和,并使用了一个 Channel 来在两个协程之间传递计算结果。在这个例子中, Channel是一个只能传递 int 类型数据的 Channel。
当两个协程都向 Channel 中写入了数据之后,主协程就可以从 Channel 中读取数据,并将两个结果相加得到最终的结果。
带缓冲的 Channel
上面的例子使用的是不带缓冲的 Channel,而 Channel 可以是 带缓冲的。下面是声明带缓冲 Channel 的方法,第二个参数表示缓冲的长度:
ch := make(chan int, 2)
下面是一个简单的例子:
package main
import "fmt"
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
fmt.Println(<-ch)
fmt.Println(<-ch)
}
使用 range 遍历 Channel
Go 语言中可以使用 range 来遍历 Channel,当使用 range 遍历channel 时,程序会不断地从 Channel 中读取数据,直到 Channel 关闭,这时候for 循环就会终止。下面是一个例子:
package main
import (
"fmt"
)
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)
}
func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
for i := range c {
fmt.Println(i)
}
}
/* 打印结果
0
1
1
2
3
5
8
13
21
34
*/
上面的例子计算出斐波那契前十个数字,每次计算出一个结果后写入 Channel 中,然后通过 range 遍历读取 Channel 中的数据,知道 Channel 关闭。
并发安全 Lock
除了 Channel 用来进行通信外,也可以采用共享内存的方式。共享内存需要利用互斥锁来保证每次只有一个协程来访问一个共享的变量。
Go 标准库中提供了 sync.Mutex 互斥锁类型及其两个方法:Lock 和Unlock。我们可以通过在代码前调用 Lock 方法,在代码后调用 Unlock 方法来保证一段代码的互斥执行。
下面是分别采用互斥锁和不采用互斥锁来并发的进行求和:
package main
import (
"fmt"
"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 main() {
x = 0
for i := 0; i < 5; i++ {
go addWithLock()
}
time.Sleep(time.Second)
fmt.Println("WithLock:", x)
x = 0
for i := 0; i < 5; i++ {
go addWithoutLock()
}
time.Sleep(time.Second)
fmt.Println("WithoutLock:", x)
}
/*
WithLock: 10000
WithoutLock: 7149
*/
可以看到加互斥锁能确保结果的正确性,而不加锁最终的结果并不正确。
WaitGroup
Go语言的 sync.WaitGroup 类型是一种用于同步多个协程的工具。它允许你等待一组协程结束。
使用方法是在每个协程启动之前调用 Add(1) 来增加计数器,在每个协程结束之后调用 Done() 来减少计数器。最后,在主协程中调用 Wait() 来等待所有协程结束。
下面是一个简单的例子,展示了如何使用 sync.WaitGroup 来同步多个协程:
package main
import (
"fmt"
"sync"
)
func printHello(wg *sync.WaitGroup, id int) {
defer wg.Done()
fmt.Printf("Hello from %d!\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go printHello(&wg, i)
}
wg.Wait()
fmt.Println("All goroutines finished executing")
}
/*
Hello from 1!
Hello from 3!
Hello from 0!
Hello from 4!
Hello from 2!
All goroutines finished executing
*/