Golang 学习之路十:并发
来源:互联网 发布:seo 衡水培训班 编辑:程序博客网 时间:2024/06/06 02:06
Golang 学习:并发
一、前言
Go语言经常被称为21世纪的C语言,原因一是Go语言设计的简洁优雅,原因二就是Go语言从语言层面原生支持并发。并发的意义,简单通俗来说就是并发的意义就是:你可以同时做多件事!
二、Goroutine
goroutine是Go并行设计的核心。goroutine是通过Go的runtime管理的一个线程管理器。goroutine说到底其实就是线程,但是他比线程更小,十几个goroutine可能体现在底层就是五六个线程,Go语言内部帮你实现了这些goroutine之间的内存共享。执行goroutine只需极少的栈内存(大概是4~5KB),当然会根据相应的数据伸缩。也正因为如此,可同时运行成千上万个并发任务。goroutine比thread更易用、更高效、更轻便。
- 在函数调用语句前添加
go
关键字就可以创建并发单元了。
package mainimport ( "fmt" "runtime")func say(s string) { for i := 0; i < 5; i++ { runtime.Gosched() fmt.Println(s) }}func main() { go say("world") //开一个新的Goroutines执行 say("hello") //当前Goroutines执行}
输出结果:
helloworldhelloworldhelloworldhelloworldhelloworld
- 上面的多个goroutine运行在同一个进程里面,共享内存数据,设计要遵循:不要通过共享来通信,而要通过通信来共享。
- runtime.Gosched()表示让CPU把时间片让给别人,下次某时继续恢复执行该goroutine。
- 默认情况下,调度器仅使用单线程,也就是说只实现了并发。想要发挥多核处理器的并行,需要在我们的程序中显示的调用 runtime.GOMAXPROCS(n) 告诉调度器同时使用多个线程。GOMAXPROCS 设置了同时运行逻辑代码的系统线程的最大数量,并返回之前的设置。
- 调用 runtime.Goexit 将立即终止当前 goroutine 执行,调度器确保所有已注册 defer延迟调用被执行。
package mainimport ( "runtime" "sync")func main() { wg := new(sync.WaitGroup) wg.Add(1) go func() { defer wg.Done() defer println("A.defer") func() { defer println("B.defer") runtime.Goexit() // 终止当前 goroutine println("B") // 不会执行 }() println("A") // 不会执行 }() wg.Wait()}
输出结果:
B.deferA.defe
- 和协程 yield 作用类似,Gosched 让出底层线程,将当前 goroutine 暂停,放回队列等待下次被调度执行。
package mainimport ( "fmt" "runtime")func say(s string) { for i := 0; i < 2; i++ { runtime.Gosched() fmt.Println(s) }}func main() { go say("world") say("hello")}
输出结果:
helloworldhelloworld
三、Chanel
goroutine是运行在相同的地址空间(即CSP模式),Go提供了通信机制channel来进行同步。channel可以与Unix shell 中的双向管道做类比:可以通过它发送或者接收值。这些值只能是特定的类型:channel类型。定义一个channel时,也需要定义发送到channel的值的类型。因为默认同步模式,需要发送与接收配对,否则会被阻塞,知道另一方准备好后被唤醒。
- 使用make
创建channel
ci := make(chan int)cs := make(chan string)cf := make(chan interface{})
- channel通过操作符
<-
来接收和发送数据
ch <- v // 发送v到channel ch.v := <-ch // 从ch中接收数据,并赋值给v
- 默认情况下,channel接收和发送数据都是阻塞的,除非另一端已经准备好,这样就使得Goroutines同步变的更加的简单,而不需要显式的lock。
package mainimport "fmt"func Afun(ch chan int) { ch <- 1 ch <- 2}func main() { c := make(chan int) go Afun(c) <-c <-c}
上面的程序虽无输出但可以运行,而如果是下面的写法是无法通过编译运行的。
// 1package mainimport "fmt"func Afun(ch chan int) { ch <- 1 ch <- 2}func main() { c := make(chan int) <-c <-c go Afun(c)}
// 2package mainfunc main() { c := make(chan int) c <- 1 c <- 2 <-c <-c}
前一段代码最终会正常结束,但是后一段代码会发生死锁。为什么会出现这种现象呢,咱们把上面两段代码的逻辑跑一下。
第一段代码:
- 创建了一个无缓冲channel
- 启动了一个goroutine,这个routine中对channel执行放入数据操作,但是因为这时候channel为空,所以这个取出操作发生阻塞,但是主routine可没有发生阻塞,它还在继续运行?
- 主goroutine这时候继续执行下一行,在channel中取出了数据
- 这时阻塞的那个routine检测到了channel没有数据了,所以解除阻塞,放入数据到channel,程序就此完毕
第二、三段代码:
- 创建了一个无缓冲的channel
- 主routine要从channel中取出一个数据,但是因为channel没有缓冲,相当于channel一直都是空的,所以这里会发生阻塞。可是下面的那个goroutine还没有创建呢,主routine在这里一阻塞,整个程序就只能这么一直阻塞下去了,就会发生死锁!
※从这里可以看出,对于无缓冲的channel,放入操作和取出操作不能再同一个routine中,而且应该是先确保有某个routine对它执行取出操作,然后才能在另一个routine中执行放入操作。
- 异步方式通过判断缓冲区来决定是否阻塞。如果缓冲区已满,发送被阻塞;缓冲区为空,接收被阻塞。
ch := make(chan type, value)value == 0 ! 无缓冲(阻塞)value > 0 ! 缓冲(非阻塞,直到value 个元素)
用简单例子证明:
package mainimport "fmt"func main() { c := make(chan int, 2)//修改2为1就报错,修改2为3可以正常运行 c <- 1 c <- 2 fmt.Println(<-c) fmt.Println(<-c)}
- 缓存区是内部属性,并非构成要素
var a, b chan int = make(chan int), make(chan int, 3)
- 用
range
可以判断channel
是否关闭,我们还可以用ok-idiom
模式来判断channel
是否关闭。
package mainimport "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}
ok-idiom
模式
for { if d, ok := <-data; ok { fmt.Println(d) } else { break }}
- 关闭后的channel可以取数据,但是不能放数据。而且,channel在执行了close()后并没有真的关闭,channel中的数据全部取走之后才会真正关闭。
package mainfunc main() { ch := make(chan int, 5) ch <- 1 ch <- 1 close(ch) ch <- 1 //不能对关闭的channel执行放入操作 // 会触发panic}//// panic: send on closed channel
package mainfunc main() { ch := make(chan int, 5) ch <- 1 ch <- 1 close(ch) <-ch //只要channel还有数据,就可能执行取出操作 //正常结束}
再看:
package mainimport "fmt"func main() { ch := make(chan int, 5) ch <- 1 ch <- 1 ch <- 1 ch <- 1 close(ch) //如果执行了close()就立即关闭channel的话,下面的循环就不会有任何输出了 for { data, ok := <-ch if !ok { break } fmt.Println(data) }}
输出结果:
1111
四、channel 单向
我们还可以以数据在通道中的传输方向为依据来划分通道。默认情况下,通道都是双向的,即双向通道。如果数据只能在通道中单向传输,那么该通道就被称作单向通道。
- 我们在初始化一个通道值的时候不能指定它为单向。但是,在编写类型声明的时候,我们却是可以这样做:
type Receiver <-chan int //代表了一个只可从中接收数据的单向通道类型。type Sender chan<- int //代表了一个只可从中发送数据的单向通道类型。var myChannel = make(chan int, 3)var sender Sender = myChannelvar receiver Receiver = myChannel
单向通道的主要作用是约束程序对通道值的使用方式。比如,我们调用一个函数时给予它一个发送通道作为参数,以此来约束它只能向该通道发送数据。又比如,一个函数将一个接收通道作为结果返回,以此来约束调用该函数的代码只能从这个通道中接收数据。
package mainimport ( "fmt" "time")type Sender chan<- inttype Receiver <-chan intfunc main() { var myChannel = make(chan int) var number = 6 go func() { var sender Sender = myChannel sender <- number fmt.Println("Sent!") }() go func() { var receiver Receiver = myChannel fmt.Println("Received!", <-receiver) }() // 让main函数执行结束的时间延迟1秒, // 以使上面两个代码块有机会被执行。 time.Sleep(time.Second)}
输出结果:
Received! 6Sent!
- 不能将单向 channel 转换为普通 channel
五、select
如果需要同时处理多个channel,可使用select
语句监听channel
上的数据流动。它随机选择一个可用channel
做收发操作,或执行 default case。select
是默认阻塞的。
package mainimport ( "fmt" "os")func main() { a, b := make(chan int, 3), make(chan int) go func() { v, ok, s := 0, false, "" for { select { // 随机选择可用 channel,接收数据。 case v, ok = <-a: s = "a" case v, ok = <-b: s = "b" } if ok { fmt.Println(s, v) } else { os.Exit(0) } } }() for i := 0; i < 5; i++ { select { // 随机选择可用 channel,发送数据。 case a <- i: case b <- i: } } close(a) select {} // 没有可用 channel,阻塞 main goroutine。}
输出结果:
// 随机,结果可能不同b 3b 4a 0a 1a 2
- 在select里面还有default语法,select其实就是类似switch的功能,default就是当监听的channel都没有准备好的时候,默认执行的(select不再阻塞等待channel)。
select { case i := <-c: // use i default: // 当c阻塞的时候执行这里}
- 用
select
设置超时,避免整个程序进入阻塞
package mainimport ( "fmt" "time")func main() { c := make(chan int) o := make(chan bool) go func() { for { select { case v := <-c: println(v) case <-time.After(5 * time.Second): println("timeout") o <- true break } } }() c <- 1 fmt.Println(<-o)}
输出结果:
1timeouttrue
六、模式
- 用简单工厂模式打包并发任务和
channel
。
package mainimport ( "math/rand" "time")func NewTest() chan int { c := make(chan int) rand.Seed(time.Now().UnixNano()) go func() { time.Sleep(time.Second) c <- rand.Int() }() return c}func main() { t := NewTest() println(<-t) // 等待 goroutine 结束返回。}
- 用 channel 实现信号量 (semaphore)
package mainimport ( "fmt" "sync")func main() { wg := sync.WaitGroup{} wg.Add(3) sem := make(chan int, 1) for i := 0; i < 3; i++ { go func(id int) { defer wg.Done() sem <- 1 // 向 sem 发送数据,阻塞或者成功。 for x := 0; x < 3; x++ { fmt.Println(id, x) } <-sem // 接收数据,使得其他阻塞 goroutine 可以发送数据。 }(i) } wg.Wait()}
输出结果:
0 00 10 21 01 11 22 02 12 2
- 用 closed channel 发出退出通知。
package mainimport ( "sync" "time")func main() { var wg sync.WaitGroup quit := make(chan bool) for i := 0; i < 2; i++ { wg.Add(1) go func(id int) { defer wg.Done() task := func() { println(id, time.Now().Nanosecond()) time.Sleep(time.Second) } for { select { case <-quit: // closed channel 不会阻塞,因此可用作退出通知。 return default: // 执行正常任务。 task() } } }(i) } time.Sleep(time.Second * 5) // 让测试 goroutine 运行一会。 close(quit) // 发出退出通知。 wg.Wait()}
- channel 是第一类对象,可传参 (内部实现为指针) 或者作为结构成员。
import "fmt"type Request struct { data []int ret chan int}func NewRequest(data ...int) *Request { return &Request{data, make(chan int, 1)}}func Process(req *Request) { x := 0 for _, i := range req.data { x += i } req.ret <- x}func main() { req := NewRequest(10, 20, 30) Process(req) fmt.Println(<-req.ret)}
输出结果:
60
七、补充 runtime
包处理goroutine
的函数
Goexit
退出当前执行的goroutine,但是defer函数还会继续调用
Gosched
让出当前goroutine的执行权限,调度器安排其他等待的任务运行,并在下次某个时候从该位置恢复执行。
NumCPU
返回 CPU 核数量
NumGoroutine
返回正在执行和排队的任务总数
GOMAXPROCS
用来设置可以运行的CPU核数
八、参考
- Go 学习笔记(雨痕)
- Go Web 编程
- Go 语言第一课
- 博文:进一步认识golang中的并发
http://blog.csdn.net/gophers/article/details/24665419
- Golang 学习之路十:并发
- golang并发学习笔记
- golang学习笔记之并发优化(一)
- golang学习笔记之并发优化(二)
- Golang教程:(二十)并发介绍
- Golang 学习之路三:基础命令
- Golang 学习之路四:类型
- Golang 学习之路五:表达式
- Golang 学习之路六:函数
- golang学习笔记[3] 并发编程
- golang并发
- golang 并发
- JavaScript学习之路<十>
- golang学习(一)之安装
- golang学习之--Hello GO
- golang学习之--struct类型
- golang学习之安装环境
- golang学习笔记之---waitgoup
- java中的匿名内部类总结
- 【前端面试】清除二维数组中元素为0所在的行和列
- IM 与 LVB 集成方案:
- 论文阅读——YouTube推荐中的深层神经网络
- java获取完整路径
- Golang 学习之路十:并发
- 3D max 渲染lightingmap的时候出现的小问题
- [李景山php] ddos 攻击 之 HTTP Flood 别名 cc攻击
- PHP学习笔记——数组下标的细节问题
- Spring中注入service时property报错 notfind dao
- gitlab上克隆远程分支到本地
- 解决:Linux中文乱码问题
- Java同步与Lock接口
- 最强开发资源平台大合集