文章15 | 阅读 7616 | 点赞0
提及Go,大家都会人云亦云一句“Go支持高并发,适合使用高并发的场景”,事实也确实如此,Go学习笔记系列也终于到了该介绍下最著名的Go并发的时候了,没有介绍并发的Go文章是没有灵魂的哈哈^^
提及并发,很容易联想到另外一个概念:并行。它们两个的区别是:
一个并发程序可以只在一个处理器或内核上运行多个任务,但是某一时间点只有一个任务在运行;如果运行在多处理器或多核上,就可以在同一时间点有多个任务在运行,才能实现真正的并行,因此,并发程序可以是运行在单处理器(单核)上也可以是运行在多处理器(多核)上。而在Go中,可以设置核数,让并发程序在多核心上真正并行运行,充分发挥多核计算机的能力
在其它编程语言中,实现并发程序往往是使用多线程的技术。在一个进程中有多个线程,它们共享同一个内存地址空间。然而使用多线程难以做到准确,尤其是内存中数据共享的问题,它们会被多线程以无法预知的方式进行操作,导致一些无法重现或者随机的结果。多线程解决这个问题的方式是同步不同的线程,对数据加锁
但是,这会带来更高的复杂度,更容易使代码出错以及更低的性能,所以这个经典的方法明显不再适合现代多核/多处理器编程
在Go中,使用协程(goroutines)来实现并发程序:
sync
包可以进行传统的加锁同步操作,但并不推荐使用在Go中使用协程是通过关键字go调用一个函数或者方法来实现的:
import (
"fmt"
"time"
)
func main(){
go Goroutine()
time.Sleep(3 * time.Second)
}
func Goroutine(){
fmt.Println("start a goroutine")
}
根据上面的代码,我们定义了一个Goroutine
的函数,并在主程序中使用go
关键字去调用该函数,从而启动一个协程去执行Goroutine
这个函数。在程序中使用time.Sleep(3*time.Second)
是为了让主程序延时3s再结束,否则主程序启动完一个协程后立即退出,我们将没法看到协程函数中打印的信息
新版本的Go(应该是1.8之后)当我们启动多个协程时,Go将会自动启动多个核心来并行运行,而在老版本的Go里需要我们手动设置,手动设置多核心的操作如下:
import (
"fmt"
"runtime"
"time"
)
func main(){
num := runtime.NumCPU()
runtime.GOMAXPROCS(num) //新版本会自动设置
for i := 0; i < 10; i++ {
go Goroutine(i)
}
time.Sleep(3 * time.Second)
}
func Goroutine(){
fmt.Println("start a goroutine")
}
通道(channel)是一种特殊的类型,可以理解为发送某种其它类型数据的管道,用于在协程之间通信。数据利用通道进行传递,在任何时间,通道中的数据只能被一个协程进行访问,因此不会发生数据竞争
make
进行创建,使用close
来关闭var ch1 chan string
ch1 = make(chan string) //创建一个用于传递string类型的通道
如果只是声明channel,未初始化,它的值为nil
上面两行代码也可以简写为ch1 := make(chan string)
通道的操作符<-
往通道发送数据:ch <- int1
表示把变量int1
发送到通道ch
中
从通道接收数据:int2 <- ch
表示变量int2
从通道ch
中接收数据(如果int2
没有事先声明过,则要用int2 := <- ch
)。直接使用<-ch
也可以,也表示从通道中取值,然后该值会被丢弃
func main(){
c := make(chan bool)
go func() { //使用匿名函数,闭包,所以可以获取到外层的channel变量
fmt.Println("go go go")
c <- true
}()
<-c //阻塞,直到从通道取出数据
}
上面的代码创建了一个布尔型的channel,主程序启动协程后就一直阻塞在<-c
那里等待从通道中取出数据,协程中当打印完数据后,就往通道中发送true
。主程序此时方从通道中取出数据,退出程序。从而不需要手动让主程序睡眠等待协程完成
func main(){
c := make(chan bool)
go func() { //使用匿名函数,闭包,所以可以获取到外层的channel变量
fmt.Println("go go go")
<-c
}()
c <- true //这里把数据传入通道后也会阻塞知道通道中数据被取出
}
func main(){
c := make(chan bool, 1)
go func() { //使用匿名函数,闭包,所以可以获取到外层的channel变量
fmt.Println("go go go")
<-c
}()
c <- true //这里往通道发完数据就直接退出了
}
make(chan type, buf)
这里buf
是通道可以同时容纳的元素的个数,如果容量大于 0,通道就是异步的了:缓冲满载(发送)或变空(接收)之前通信不会阻塞,元素会按照发送的顺序被接收
for-range
来操作channelfor-range
可以用在通道上,以便从通道中获取值:for v := range ch {
fmt.Println(v)
}
它从指定的通道中读取数据直到通道关闭才能执行下面的代码,因此程序必须在某个地方close
该通道,否则程序将死锁
func main(){
c:=make(chan bool)
go func(){
fmt.Println("gogogo")
c <- true
close(c)
}()
for v := range c{
fmt.Println(v)
}
}
此外,关于channel还需要注意:
使用select
可以从不同的并发执行的协程中获取值,它和switch
语句很类似。select
可以用来监听进入通道的数据,也可以向通道发送数据
select {
case u:= <- ch1:
...
case v:= <- ch2:
...
...
default: // no value ready to be received
...
}
select
的功能其实就是处理列出的多个通信中的一个
default
语句是可选的,fallthrough
是不允许的,任何一个case
中执行了break
或者return
语句,select
就结束了case
的通道都阻塞了,会等待直到其中一个可以处理case
的通道可以处理,会随机
选择一个处理default
语句,它就会执行default
语句select
中使用发送操作并且有default
可以确保发送不被阻塞!如果没有default
,select
就会一直阻塞select
也可以设置超时处理下面的代码是一个类似生产者-消费者的模式,包括了两个通道和三个协程,其中协程goroutine1
和goroutine2
分别往通道ch1
和ch2
中写入数据,协程goroutine3
则通过select
分别从两个通道中读出数据并输出
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go goroutine1(ch1)
go goroutine2(ch2)
go goroutine3(ch1, ch2)
time.Sleep(1e9)
}
func goroutine1(ch chan int) {
for i := 0; ; i++ {
ch <- i * 2
}
}
func goroutine2(ch chan int) {
for i := 0; ; i++ {
ch <- i + 5
}
}
func goroutine3(ch1, ch2 chan int) {
for {
select {
case v := <-ch1:
fmt.Printf("Received on channel 1: %d\n", v)
case v := <-ch2:
fmt.Printf("Received on channel 2: %d\n", v)
}
}
}
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/xah100147/article/details/106439497
内容来源于网络,如有侵权,请联系作者删除!