有人把Go比作21世纪的C语言,首先是因为Go语言设计简单,第二,21世纪最重要的就是并行程序设计,而Go从语言层面就支持了并行。
一、Goroutine简介
Goroutine是Go并发设计的核心。**Goroutine是Go运行时(runtime)管理的一种轻量级”线程”,。
Goroutine并不是一种真正意义上的协程,但我们可以认为它很像协程。
Goroutine
在一个需要调用的函数之前,通过go关键字就启动了一个Goroutine执行任务。
go f(x,y)
我们来看一个例子
package main
import (
"fmt"
"time"
)
func Say(name string) {
for i:=1;i<=5;i++ {
fmt.Println("I am "+name)
time.Sleep(time.Second)
}
}
func main() {
//demo.ChannelDemo()
go Say("goroutine2") //开启新的goroutine执行
Say("goroutine1")
}
/*
以上程序执行后将输出:
I am goroutine1
I am goroutine2
I am goroutine2
I am goroutine1
I am goroutine1
I am goroutine2
I am goroutine2
I am goroutine1
I am goroutine1
I am goroutine2
*/
我们通过go关键字很方便的就实现了并发编程。上面的例子中,多个goroutine运行在同一个进程中共享着内存,所以在访问共享内存的时候一定要注意同步的问题。
package main
import (
"fmt"
"sync"
"time"
)
var counter int = 0
func Count(lock *sync.Mutex) {
lock.Lock() //互斥锁
counter++
fmt.Println(counter)
lock.Unlock()
}
func main() {
lock := &sync.Mutex{}
for i := 0; i < 5; i++ { //创建5个goroutine跑子函数
go Count(lock)
}
time.Sleep(time.Second * 5)
}
输出结果:1 2 3 4 5
此时,多个线程共享数据counter,实际上当业务逻辑比较复杂并且共享数据比较多的情况下,使用这种方式是一件十分头疼的事情。我们这里暂且不提这些。
goroutine调度机制
MPG模型
Goroutine总结
- 轻量级”线程”
作用和线程一样用来并发执行任务,轻量级可以开一千个协程,但是开一千个线程很难
- 非抢占式多任务处理,由协程主动交出控制权
线程是抢占式多任务处理。线程在任何时候都会被操作系统切换掐掉,转到其他人执行,回来以后在继续执行轮流使用CPU时间片,线程被切换需要存储更多的上下文,而coroutine正因为轻量级资源消耗更少。
- 编译器/解释器/虚拟机层面的多任务
Go语言编译器层面会把go func解释成一个goroutine来执行,操作系统有调度器来调度线程,而Go语言也有自己的调度器来调度goroutine。
多个协程可能在一个或者多个线程上运行
独立的栈空间
- 共享程序堆空间
- 调度由用户控制(区别于进程和线程是操作系统调度启动的)
- 轻量级的”线程”(几十个goroutine 可能体现在底层就是五六个线程)
而且Go语言内部也实现了 goroutine 之间的内存共享。
**
二、通道(Channel)
Do not communicate by sharing memory; instead, share memory by communicating. 不要以共享内存的方式来通信,相反,要通过通信来共享内存。
多个Goroutine运行在相同的地址空间,因此访问共享内存必须做好同步。通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。
通道(channels)
声明channel
通道是带有类型的管道。这意味着一个channel只能传递一种类型的值,这个类型需要在声明channel时指定。
声明一个通道类型语法如下:
var variableName chan deliveryType
举几个例子
var intChan chan int //传递整型的通道
var silceChan chan []string //传递string切片的通道
var anyChan chan interface{} //传递interface{}类型的通道
var personChan chan Person //传递Person类型的通道
创建channel
通道和切片、映射一样都是引用类型,类型默认值为
创建一个channel语法如下:
ch:=make(chan deliveryType, bufferSize) //缓冲区大小是可选的
举几个例子
intChan:=make(chan int)
anyTypeChan:=make(chan interface{})
personChan:=make(chan *person)
channel的发收操作
一旦创建了通道,就可以通过通道操作符<-来发送(send)或者接收(receive)数据,<-表示数据流的方向。**
通道操作语法如下:
ch<-v //把v发送到ch中
v:=<-ch //从ch中接收值,并赋给变量v
<-ch //从ch中接收值,忽略结果
使用goroutine和channel的例子
关闭通道后,其它goroutine访问通道获取数据时,得到零值和false
有条件结束死循环:
通道的关闭和遍历
如果数据有明确的结尾,发送方可以使用close()函数显式的关闭channel,通知接收方已经没有数据了。
在接收方可以使用多重返回值的方式判断channel是否被关闭。
for{
v ,ok := <- chan
if !ok{
break //通道被关闭,退出循环
}
}
for-range语句能够不断的读取channel里面的数据,直到该channel被显式的关闭
for data := range ch {
fmt.Println(data)
}
- 通道与文件不同,通常情况下无需关闭
- 只能发送方关闭通道,向一个已经关闭的通道发送数据会报panic
- 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值(无阻塞的成功)。
默认情况下,发送和接收的操作会等待另一端准备好情况下进行,这样就使得Goroutines同步变的更加的简单,而不需要显式的lock。
Go语言通道的特性
- 通道是引用类型(初始值是nil,必须配合使用make函数分配内存区域)
- 数据是先进先出(FIFO)
- 数据安全(多个goroutine访问时,不需要加锁,意味着channel本身就是线程安全的)
- channel的本质就是一个数据结构—队列
- 通道是带有类型的。(channel只能存放指定类型的数据)
- 通道是带有大小的。(数据放满就不能在放了)
- channel取出数据,可以在放(形成管道,一边取一边放)
channel是多个goroutine之间的通信桥梁,把数据往通道发送时,如果没有接收方接收数据,那么会报deadlock阻塞错误!
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.channelDemo()
C:/Users/Administrator/IdeaProjects/channel/src/main/main.go:20 +0x9f
无缓冲通道(Buffered channels)
select多路复用
协程通信
协程同步
互斥锁
package main
import (
"fmt"
"sync"
)
var (
//临界资源
balance int =1000
//同步对象
guard sync.Mutex=sync.Mutex{}
wg sync.WaitGroup
)
func setMoney() {
defer wg.Done()//减少等待的数量1
defer guard.Unlock()
guard.Lock()
balance+=1
}
func getMoney() {
defer wg.Done() //减少等待的数量1
defer guard.Unlock()
guard.Lock()
balance-=1
}
func main() {
wg.Add(20) //需要等待的goroutine数量
for i:=0;i<10;i++ {
go setMoney()
go getMoney()
}
wg.Wait() // 执行main阻塞,等待其他goroutine完成
fmt.Println("balance=",balance)
}
等待组
使用等待组进行多个任务的同步,,等待组可以保证在并发环境中完成指定数量的任务