Golang 入门系列(十五)如何理解go的并发?
接下来要说的是golang的并发,其实之前简单介绍过协程(goroutine)和管道(channel) 等基础内容,只是比较简单,只讲了基本的语法。今天就详细说说golang的并发编程。
一、并发和并行
Go是并发语言,而不是并行语言。所以我们在讨论,我们首先必须了解什么是并发,以及它与并行性有什么不同。
什么是并发
并发就是一段时间内处理许多事情。
比如,一个人在晨跑。在晨跑时,他的鞋带松了。现在这个人停止跑步,系鞋带,然后又开始跑步。这是一个典型的并发。这个人能够同时处理跑步和系鞋带,这是一个人能够同时处理很多事情。
什么是并行
并行就是同一时刻做很多事情。这听起来可能与并发类似,但实际上是不同的。
再比如,这个人正在慢跑,并且使用他的手机听音乐。在这种情况下,一个人一边慢跑一边听音乐,那就是他同时在做很多事情。这就是所谓的并行。
二、协程(Goroutines)
go中使用Goroutines来实现并发。Goroutines是与其他函数或方法同时运行的函数或方法。Goroutines可以被认为是轻量级的线程。与线程相比,创建Goroutine的成本很小。因此,Go应用程序可以并发运行数千个Goroutines。
Goroutines在线程上的优势。
-
与线程相比,Goroutines非常便宜。它们只是堆栈大小的几个kb,堆栈可以根据应用程序的需要增长和收缩,而在线程的情况下,堆栈大小必须指定并且是固定的
-
Goroutines被多路复用到较少的OS线程。在一个程序中可能只有一个线程与数千个Goroutines。如果线程中的任何Goroutine都表示等待用户输入,则会创建另一个OS线程,剩下的Goroutines被转移到新的OS线程。所有这些都由运行时进行处理,我们作为程序员从这些复杂的细节中抽象出来,并得到了一个与并发工作相关的干净的API。
-
当使用Goroutines访问共享内存时,通过设计的通道可以防止竞态条件发生。通道可以被认为是Goroutines通信的管道。
如何使用Goroutines
在函数或方法调用前面加上关键字go,您将会同时运行一个新的Goroutine。
实例代码:
package main import ( "fmt" "time" ) func hello() { fmt.Println("Hello world goroutine") } func main() { go hello() time.Sleep(1 * time.Second) fmt.Println("main function") }
运行结果: Hello world goroutine main function
如何启动多个Goroutines
示例代码:
package main import ( "fmt" "time" ) func numbers() { for i := 1; i <= 5; i++ { time.Sleep(250 * time.Millisecond) fmt.Printf("%d ", i) } } func alphabets() { for i := 'a'; i <= 'e'; i++ { time.Sleep(400 * time.Millisecond) fmt.Printf("%c ", i) } } func main() { go numbers() go alphabets() time.Sleep(3000 * time.Millisecond) fmt.Println("main terminated") } 运行结果: 1 a 2 3 b 4 c 5 d e main terminated
Goroutine切换
下面通过素数计算的例子来说明goland是如何通过切换不同的goroutine实现并发的。
package main
import (
"fmt"
"runtime"
"sync"
)
var wg sync.WaitGroup
func main() {
runtime.GOMAXPROCS(1)
wg.Add(2)
go printPrime("A")
go printPrime("B")
fmt.Println("Wait for finish")
wg.Wait()
fmt.Println("Program End")
}
func printPrime(prefix string) {
defer wg.Done()
nextNum:
for i := 2; i < 6000; i++ {
for j := 2; j < i; j++ {
if i%j == 0 {
continue nextNum
}
}
fmt.Printf("%s:%d\n", prefix, i)
}
fmt.Printf("complete %s\n", prefix)
}
运行结果:
Wait for finish
B:2
B:3
B:5
B:7
B:11
...
B:457
B:461
B:463
B:467
A:2
A:3
A:5
A:7
...
A:5981
A:5987
complete A
B:5939
B:5953
B:5981
B:5987
complete B
Program End
通过以上的输出结果,可以看出两个Goroutine是在一个处理器上通过切换goroutine实现并发执行。
三、通道(channels)
通道可以被认为是Goroutines通信的管道。类似于管道中的水从一端到另一端的流动,数据可以从一端发送到另一端,通过通道接收。
声明通道
每个通道都有与其相关的类型。该类型是通道允许传输的数据类型。(通道的零值为nil。nil通道没有任何用处,因此通道必须使用类似于地图和切片的方法来定义。)
示例代码:
package main import "fmt" func main() { var a chan int if a == nil { fmt.Println("channel a is nil, going to define it") a = make(chan int) fmt.Printf("Type of a is %T", a) } } 运行结果: channel a is nil, going to define it Type of a is chan int
也可以简短的声明:
a := make(chan int)
发送和接收
发送和接收的语法:
data := <- a // read from channel a a <- data // write to channel a
在通道上箭头的方向指定数据是发送还是接收。
一个通道发送和接收数据,默认是阻塞的。当一个数据被发送到通道时,在发送语句中被阻塞,直到另一个Goroutine从该通道读取数据。类似地,当从通道读取数据时,读取被阻塞,直到一个Goroutine将数据写入该通道。
这些通道的特性是帮助Goroutines有效地进行通信,而无需像使用其他编程语言中非常常见的显式锁或条件变量。
示例代码:
package main import ( "fmt" "time" ) func hello(done chan bool) { fmt.Println("hello go routine is going to sleep") time.Sleep(4 * time.Second) fmt.Println("hello go routine awake and going to write to done") done <- true } func main() { done := make(chan bool) fmt.Println("Main going to call hello go goroutine") go hello(done) <-done fmt.Println("Main received data") }
运行结果:
Main going to call hello go goroutine
hello go routine is going to sleep
hello go routine awake and going to write to done
Main received data
定向通道
之前我们学习的通道都是双向通道,我们可以通过这些通道接收或者发送数据。我们也可以创建单向通道,这些通道只能发送或者接收数据。
创建仅能发送数据的通道,示例代码:
package main import "fmt" func sendData(sendch chan<- int) { sendch <- 10 } func main() { sendch := make(chan<- int) go sendData(sendch) fmt.Println(<-sendch) }
报错:
# command-line-arguments
.\main.go:12:14: invalid operation: <-sendch (receive from send-only type chan<- int)
示例代码:
package main import "fmt" func sendData(sendch chan<- int) { sendch <- 10 } func main() { chnl := make(chan int) go sendData(chnl) fmt.Println(<-chnl) }
运行结果: 10
死锁
为什么会死锁?非缓冲信道上如果发生了流入无流出,或者流出无流入,也就导致了死锁。或者这样理解 Go启动的所有goroutine里的非缓冲信道一定要一个线里存数据,一个线里取数据,要成对才行 。
示例代码:
package main
func main() {
c, quit := make(chan int), make(chan int)
go func() {
c <- 1 // c通道的数据没有被其他goroutine读取走,堵塞当前goroutine
quit <- 0 // quit始终没有办法写入数据
}()
<-quit // quit 等待数据的写
}
报错: fatal error: all goroutines are asleep - deadlock! goroutine 1 [chan send]: main.main() /tmp/sandbox249677995/main.go:11 +0x80
关闭通道
关闭通道只是关闭了向通道写入数据,但可以从通道读取。
package main import ( "fmt" ) var ch chan int = make(chan int, 3) func main() { ch <- 1 ch <- 2 ch <- 3 close(ch) for v := range ch { fmt.Println(v) } }
四、缓冲通道
之前学习的所有通道基本上都没有缓冲。发送和接收到一个未缓冲的通道是阻塞的。
可以用缓冲区创建一个通道。发送到一个缓冲通道只有在缓冲区满时才被阻塞。类似地,从缓冲通道接收的信息只有在缓冲区为空时才会被阻塞。
可以通过将额外的容量参数传递给make函数来创建缓冲通道,该函数指定缓冲区的大小。
语法:
ch := make(chan type, capacity)
上述语法的容量应该大于0,以便通道具有缓冲区。默认情况下,无缓冲通道的容量为0,因此在之前创建通道时省略了容量参数。
示例代码:
func main() {
done := make(chan int, 1) // 带缓存的管道
go func(){
fmt.Println("你好, 世界")
done <- 1
}()
<-done
}