Goroutine
也就是 Golang 的多线程
只需在函数调用语句前添加 go
关键字,就可创建并发执行单元。
go func(){
// your code...
}
开发⼈人员无需了解任何执行细节,调度器会自动将其安排到合适的系统线程上执行。事实上,入口函数 main
就以 goroutine
运行。
关于调度器的说明
调度器不能保证多个 goroutine
执行次序,且进程退出时不会等待它们结束。
所以对于这段代码:
package main
import "fmt"
func main() {
go printSomeThing(1)
go printSomeThing(2)
}
func printSomeThing(sth int) {
for i := 0; i < 100; i++ {
fmt.Println(sth)
}
fmt.Println("finish", sth)
}
// 其实什么输出都没有,直接就给结束了
默认情况下,进程启动后仅允许一个系统线程服务于 goroutine
(粗略理解为默认单核运行)。可使用环境变量或标准库函数 runtime.GOMAXPROCS
修改,让调度器用多个线程实现多核并行,而不仅仅是并发。
一个简单的多线程demo
把上面的例子稍微修改亿点点,就是一个简单的多线程demo
package main
import (
"fmt"
"sync"
)
func main() {
// WaitGroup 用于管理线程阻塞
wg := new(sync.WaitGroup)
// 在 WaitGroup 中需要管理两个线程,即 Counter = 2
wg.Add(2)
// 1号线程
go func() {
defer wg.Done() //执行完成时,Counter - 1
printSomeThing(1)
}()
// 2号线程
go func() {
defer wg.Done()
printSomeThing(2)
}()
wg.Wait() // 用于将主线程阻塞,以免发生上面那样直接结束的情况,当 Counter = 0 时结束阻塞
}
//线程执行方法
func printSomeThing(sth int) {
for i := 0; i < 100; i++ {
fmt.Println(sth)
}
fmt.Println("finish", sth)
}
线程控制
停止当前执行的 goroutine
调用 runtime.Goexit
将立即终止当前 goroutine 执行,调度器确保所有已注册 defer
延迟调用被执行。
继续拿上面那个举例
package main
import (
"fmt"
"runtime"
"sync"
)
func main() {
wg := new(sync.WaitGroup)
wg.Add(1)
go func() {
defer wg.Done()
printSomeThing(1)
}()
wg.Wait()
}
//线程执行方法
func printSomeThing(sth int) {
for i := 0; i < 100; i++ {
fmt.Println(sth) // 只会输出一次
runtime.Gosched() // 停止 goroutine
}
fmt.Println("finish", sth) // 不会执行
}
放弃当前运行权,与其他线程重新争夺
和协程 yield
作用类似,Gosched
让出底层线程,将当前 goroutine
暂停,放回队列等待下次被调度执行。(这并不代表其他线程就一定能获得执行权)
package main
import (
"fmt"
"runtime"
"sync"
)
func main() {
wg := new(sync.WaitGroup)
wg.Add(2)
go func() {
defer wg.Done()
printSomeThing(1)
}()
go func() {
defer wg.Done()
printSomeThing(2)
}()
wg.Wait()
}
func printSomeThing(sth int) {
for i := 0; i < 100; i++ {
fmt.Println(sth)
// 每当循环3次就放一次权
if i%3 == 0 {
runtime.Gosched()
}
}
fmt.Println("finish", sth)
}
Channel
一个经典的例子
我们先来看一个经典的例子(售票)
package main
import (
"fmt"
"sync"
)
var i = 100 // 有100张票
func main() {
wg := new(sync.WaitGroup)
wg.Add(2) // 有两个售票窗口
go func() {
defer wg.Done()
sell("routine2")
}()
go func() {
defer wg.Done()
sell("routine1")
}()
wg.Wait()
}
func sell(routineName string) {
for {
if i <= 0 {
fmt.Println(routineName," said no more ticket")
break
}
fmt.Println(routineName," sells", i)
i--
}
}
/* 运行结果:
...
routine1 sells 0
routine1 said no more ticket
routine2 sells 47
routine2 said no more ticket
*/
可以从上面的运行结果中看到 routine1
刚说票卖完了,routine2
转手就卖了一张,这是线程不安全导致的问题 —— 在 golang 中是通过使用管道(channel)并联各个线程来解决这个问题的
语法
声明/开启
管道自声明以后,便是开着的,只有开着的管道才能向线程传输数据
使用 make
关键字来声明 channel
: make (chan datatype)
关闭
使用 close(channel_name)
来关闭管道
判断管道的开闭
第一种方式是使用关键字 range
,如果 channel
关闭,就会退出循环
for value := range channelName {
// your code here...
}
除用 range
外,还可用 ok-idiom
模式判断 channel
是否关闭。
for {
if d, ok := <-data; ok {
fmt.Println(d)
} else {
break
}
}
数据的发送与接收
data <- 1
发送数据到管道(假设管道名为 data
,数据类型为 chan int
)
<- data
从管道接受数据(假设管道名为 data
,数据类型为 chan int
)
向 closed channel 发送数据引发 panic 错误,接收立即返回零值。而 nil channel,无论收发都会被阻塞。
应用模式
下面说说常见的两种应用模式:
同步模式
同步模式,需要发送和接收配对。否则会被阻塞,直到另一方准备好后被唤醒。
package main
import "fmt"
func main() {
data := make(chan int) // 数据交换队列
exit := make(chan bool) // 退出通知
go func() {
for d := range data { // 从队列迭代接收数据,直到 close 。
fmt.Println(d)
}
fmt.Println("recv over.")
exit <- true // 发出退出通知。
}()
data <- 1 // 发送数据。
data <- 2
data <- 3
close(data) // 关闭队列。
fmt.Println("send over.")
<-exit // 等待退出通知。
}
/* 运行结果:
1
2
send over.
3
recv over.
*/
异步模式
异步方式通过判断缓冲区来决定是否阻塞。如果缓冲区已满,发送被阻塞;缓冲区为空,接收被阻塞。
通常情况下,异步 channel
可减少排队阻塞,具备更高的效率。但应该考虑使用指针规避大对象拷贝,将多个元素打包,减小缓冲区大小等。
package main
import "fmt"
func main() {
data := make(chan int, 3) // 缓冲区可以存储 3 个元素
exit := make(chan bool)
data <- 1 // 在缓冲区未满前,不会阻塞。
data <- 2
data <- 3
go func() {
for d := range data { // 在缓冲区未空前,不会阻塞。
fmt.Println(d)
}
exit <- true
}()
data <- 4 // 如果缓冲区已满,阻塞。
data <- 5
close(data)
<-exit
}
缓冲区是内部属性,并非类型构成要素。
var a, b chan int = make(chan int), make(chan int, 3) // 无论是否加了缓冲大小都能给 chan 类型赋值
内置函数 len
返回未被读取的缓冲元素数量,cap
返回缓冲区大小。
d1 := make(chan int)
d2 := make(chan int, 3)
d2 <- 1
fmt.Println(len(d1), cap(d1)) // 0 0
fmt.Println(len(d2), cap(d2)) // 1 3
回到前面的例子
有了 channel
以后,怎么改造前面例子中的代码能够线程安全呢? —— 利用异步阻塞
package main
import (
"fmt"
"sync"
)
func main() {
wg := new(sync.WaitGroup)
wg.Add(2)
ticketChan := make(chan int, 1)
ticketChan <- 100
go func() {
defer wg.Done()
sell("routine1", ticketChan)
}()
go func() {
defer wg.Done()
sell("routine2", ticketChan)
}()
wg.Wait()
}
func sell(routineName string, ticketChan chan int) {
//for {
// if left, ok := <-ticketChan; ok {
// if left <= 0 {
// fmt.Println(routineName, "said no more ticket")
// close(ticketChan)
// break
// }
// fmt.Println(routineName, "sells", left)
// left--
// ticketChan <- left
// } else {
// break
// }
//}
for left := range ticketChan {
if left <= 0 {
fmt.Println(routineName, "said no more ticket")
close(ticketChan)
break
}
fmt.Println(routineName, "sells", left)
left--
ticketChan <- left
}
}
/* 运行结果:
routine1 sells 3
routine2 sells 2
routine1 sells 1
routine2 said no more ticket
*/
问题解决~
参考
Q.E.D.