Go基础并发编程
Posted justry_deng
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go基础并发编程相关的知识,希望对你有一定的参考价值。
并发编程
Go并发的设计
Go语言最大的特色是并发,而且Go的并发并不像线程或进程那样,受CPU核心数的限制,只要你愿意,你可以启动成千上万个Goroutine协程
。
相关概念
- 进程:最小的系统资源申请单位。
- 线程:最小的执行单位,一个进程内可以启动多个线程。
- 协程(
Goroutine
):协程是比线程还要小的执行单位,准确地说,协程是通过线程来执行的。
在操作系统层面,线程是最小的执行单位。Go语言调度算法会为每个线程提供一个Goroutine
协程执行列表,CPU在不同的线程间切换时需要记录上下文信息,如图所示:
当线程调度执行某个Goroutine
协程的时,该协程阻塞了,调度该协程的线程会被挂起,此时,该线程也就没法执行其他Goroutine
协程了。此时,Go的调度算法会将该线程的Goroutine
协程队列转移到其他线程的Goroutine
协程队列中去,如图所示:
启动协程
使用关键字
go
+ 函数调用,即可启动一个协程
示例一:
import (
"fmt"
"time"
)
func main() {
fmt.Println("start")
// 开启协程
go func() {
fmt.Println("协程执行了")
}()
// 睡眠1s,给足够的时间,观察协程输出
time.Sleep(time.Second * 1)
fmt.Println("end")
}
输出:
start
协程执行了
end
示例二:
import (
"fmt"
"time"
)
func main() {
// 开启协程,每隔200毫秒打印一下符号,提升用户体验
go spinner(time.Millisecond * 200)
// 计算斐波那契数列
fmt.Printf("\\n%d\\n", fib(45))
}
// 计算斐波那契数列
func fib(x int) int {
if x < 2 {
return x
}
return fib(x-2) + fib(x-1)
}
// 打印符号
func spinner(delay time.Duration) {
for {
for _, r := range `-\\|/` {
fmt.Printf("\\r%c", r)
time.Sleep(delay)
}
}
}
输出:
# 动态效果,循环输出 - \\ | /
\\
同步
在Go中,常用的同步机制有
WaitGroup
注:类似于Java的倒计时锁
CountDownLatch
互斥锁(
Mutex
)读写锁(
RWMutex
)条件变量(
Cond
)
示例:使用同步机制前
import (
"fmt"
)
func main() {
for i := 0; i < 10; i++ {
go func(num int) {
fmt.Println(num)
}(i)
}
fmt.Println("main线程执行了")
}
输出:
0
main线程执行了
示例:使用同步机制后
import (
"fmt"
"sync"
)
var w sync.WaitGroup
func main() {
for i := 0; i < 10; i++ {
// 计数+1
w.Add(1)
go func(num int) {
fmt.Println(num)
// 计数-1
w.Done()
}(i)
}
// 阻塞等待,直到w值为0
w.Wait()
fmt.Println("main线程执行了")
}
输出:
0
2
6
7
4
5
9
3
8
1
main线程执行了
通道channel
Linux
的管到机制:Linux
的管道机制是借助内核开辟的缓冲区实现一个像是"水管"的通道,"水管"两端的进程可以通过这个"水管"进行传递,以达到进程间通信的目标。Go语言的
channel
:Go语言的channel类似于Linux
管道,channel
通过阻塞读和阻塞写的方式,可以精准控制Goroutine
协程的运行、通信,即:实现了Goroutine
协程间的同步。注:channel传递数据的方式有点"不见不散"的意思,读和写的双方同时操作的时候才会解除彼此的阻塞,否者一方会死等另一方的到来。
创建channel
创建channel
make(chan chantype)// 或者make(chan chantype, d uint)
chan
:channel的关键字注:
chantype
代表了通道内可以传递的数据类型,可以是原生类型,也可以是自定义结构。d:代表通道的缓冲区大小;通道本身是同步机制,使用缓冲区可以做到异步操作
channel的读写
channel的读写行为
- 写行为
- 通道缓冲区已满(无缓冲区):写阻塞直到缓冲区有空间(或读端有读行为)
- 通道缓冲区未满:顺利写入,结束
- 读行为
- 缓冲区无数据(无数数据时写端未写数据):读阻塞直到写端有数据写入
- 缓冲区有数据:顺利读数据,结束
channel的读写语法
msg := <- c // 读,将channel c中的数据读取至msgc <- msg // 写,将msg中的数据写入channel c
提示:通过观察channel和箭头的位置即可轻松区分读和写。
示例一:
import ( "fmt" "sync" "time")var c chan stringvar w sync.WaitGroupfunc reader() { msg := <-c // 从c读 fmt.Println("I am reader,", msg)}func main() { c = make(chan string) w.Add(1) go reader() fmt.Println("begin sleep") // 睡眠3秒是为了看执行效果,验证channel阻塞读 time.Sleep(time.Second * 3) c <- "hello" // 往c写 time.Sleep(time.Second * 1) // 睡眠1秒是为了避免main结束得太快而看不到执行效果}
输出:
begin sleepI am reader, hello
示例二:
提示:
channel使用完毕后,需要作出close关闭,以达到广播的目的,这样一来其它使用到该channel的"家伙"就能感知到
注:试想一下,假设
协程writterA
通过通道channelB
向另一个协程readerC
跨协程传输数据,传输完毕后,通道自动关闭(不可能再有人往里面写数据了,因为通道都没了)而没有通知协程readerC
,那么由于channel的读写阻塞机制,协程readerC
就会一直等下去,进而形成死锁。关闭channel的动作一定要由写端发起;如果读端关闭了,写端不知情,写端再往已经关闭了的channel写数据就会报错
下述代码示例为:做一个数字传递的游戏使用3个协程,第一个协程负责将0-9传递给第二个协程,第二个协程将受收到的数据平方后传递给第三个协程,第三个协程负责将收到的数据打印到控制台。
import ( "fmt" "time")func main() { var c1 = make(chan int) var c2 = make(chan int) // 数数的协程 go func() { for i := 0; i < 10; i++ { c1 <- i // 向通道c1写入数据 time.Sleep(time.Second * 1) // 这里睡眠1秒,是为了方便观察执行效果,要不然程序一下子就全部输出来了 } close(c1) // 写完后, 关闭c1 }() // 计算平方的协程 go func() { for { num, ok := <-c1 // 读取c1数据 if ok { c2 <- num * num // 将平方写入c2 } else { break // 如果ok==false,即:c1关闭了,那么结束循环 } } close(c2) // 写完后, 关闭c2 }() // main最后负责打印(msin本身也属于一个协程) for { for { num, ok := <-c2 // 读取c2数据 if ok { fmt.Println(num) } else { break // 如果ok==false,即:c2关闭了,那么结束循环 } } }}
输出:
0149162536496481
单方向channel
单方向channel
和channel
用法其实是一样的,不同的是:单方向channel通过主动声明读通道(或写通道)的方式,从语法上强制约束了该通道只能读(或只能写),降低了程序员对代码的理解难度,让通道的使用更加明了,让channel更友好了。
单方法channel的声明:
chan_name chan <- chan_type // 只写通道chan_name <- chan chan_type // 只读通道
示例:
下述代码示例为:做一个数字传递的游戏使用3个协程,第一个协程负责将0-9传递给第二个协程,第二个协程将受收到的数据平方后传递给第三个协程,第三个协程负责将收到的数据打印到控制台。
import ( "fmt" "time")func main() { var c1 = make(chan int) var c2 = make(chan int) // 数数的协程 go func(writeChannel chan<- int) { for i := 0; i < 10; i++ { writeChannel <- i // 向通道writeChannel写入数据 time.Sleep(time.Second * 1) // 这里睡眠1秒,是为了方便观察执行效果,要不然程序一下子就全部输出来了 } close(writeChannel) // 写完后, 关闭writeChannel }(c1) // 计算平方的协程 go func(readChannel <-chan int, writeChannel chan<- int) { for { num, ok := <-readChannel // 读取readChannel数据 if ok { writeChannel <- num * num // 将平方写入writeChannel } else { break // 如果ok==false,即:readChannel关闭了,那么结束循环 } } close(writeChannel) // 写完后, 关闭writeChannel }(c1, c2) // main最后负责打印 for { for { num, ok := <-c2 // 读取c2数据 if ok { fmt.Println(num) } else { break // 如果ok==false,即:c2关闭了,那么结束循环 } } }}
输出:
0149162536496481
定时器
定时器是Go语言对channel的一个非常典型的应用;当你声明运行一个定时器后,Go语言会每隔一段时间(这个时间在你声明定时器时由你指定)作为写的一方往channel写数据,应用程序作为读的一方,每当收到一次数据,那么即说明过去了指定的时间长度,即达到了定时的效果
示例:
import ( "fmt" "time")func main() { ticker := time.NewTicker(time.Second) num := 5 for { // 从channel C中读取数据(注:这里虽然读取数据了,但是没有使用变量接收这个数据,这是可以的) <-ticker.C fmt.Println(num) num-- if num == 0 { break } } ticker.Stop() launch() // 倒计时结束,发射}func launch() { fmt.Println("发射!")}
输出:
54321发射!
^_^ 整理自《Go语言区块链应用开发从入门到精通》高野 编著
^_^ 本文已经被收录进《程序员成长笔记》 ,笔者JustryDeng
以上是关于Go基础并发编程的主要内容,如果未能解决你的问题,请参考以下文章