本文对Golang并发编程进行讲解,源于自己的学习体验。文笔一般,才疏学浅,请多指教!!
Go并发编程
Go协程(Goroutine)
它常常被用于进行多任务,即并发作业 ,Go 的协程却依赖于线程来进行
线程和内核密不可分,协程是在应用层
GMP
在Go中主要是使用GMP调度模型进行调度的。
GMP模型其实指的是:G:Go协程、M:线程、P、逻辑处理器。
它们之间的关系,借用一张图来开是这样的:
它的逻辑为:
G:
- 当一个G创建时,会优先放入P的
runnext - 当runnext放满时,会把当前的G放入本地队列。
- 当本地队列也放满时,会把当前的本地队列的一半和当前的G一起放入全局队列。
M:
- 先从P的本地队列(优先runnext)中取出执行。
- 定期处理全局队列的G,一般每61次一处理,将全局队列的G平均分配给每个P。
- 当当前的P没有G了,会随机挑一个P,拿一半的G执行。
P的结构特点:
- 长度锁定256,但是Go有无限长度的全局队列。
- 本地队列是数组,全局队列是链表。
chan 通道
通道是Golang并发的基础,通道可以使得Go协程之间的数据,状态进行共享。
创建一个通道ch:=make(chan int) 创建了一个int类型的通道
通道分为缓存通道和非缓存通道,它们的区别在于是否定义长度。
a := make(chan int)
b := make(chan int, 10)
如何往通道存放数据呢?
// 创建通道
ch := make(chan int)
a:=1
ch<-a //将a放入通道
b:=<-ch //取出通道的数值放入b
例子1: Go协程的基本实现
work1 和 work2 是两个机器人,它们需要找到数组中的2,我们希望它们比一比谁最快找到。
func work1(arr []int) {
for i := 0; i < len(arr); i++ {
if arr[i] == 2 {
fmt.Println("work1 get the number")
return
} else {
fmt.Println("work1 mine")
}
}
}
func work2(arr []int) {
for i := 0; i < len(arr); i++ {
if arr[i] == 2 {
fmt.Println("work2 get the number")
return
} else {
fmt.Println("work2 mine")
}
}
}
func main() {
arr := []int{1, 3, 5, 6, 7, 1, 3, 5, 7, 2, 3, 4}
go work1(arr)
go work2(arr)
time.Sleep(1 * time.Second)
}
希望你能亲手实现这个例子。
当你运行多次你会发现,它们的顺序是不同的,但是会出现两者都get的情况。这样是比较浪费时间的,如何改善这种情况呢?
采用通道:
func work1(arr []int, ch chan bool) {
for i := 0; i < len(arr); i++ {
select {
case <-ch:
fmt.Println("work1 say 'GG' ")
return
default:
if arr[i] == 2 {
fmt.Println("work1 get the number")
ch <- true
return
} else {
fmt.Println("work1 mine")
}
}
}
}
func work2(arr []int, ch chan bool) {
for i := 0; i < len(arr); i++ {
select {
case <-ch:
fmt.Println("work2 say 'GG'")
return
default:
if arr[i] == 2 {
fmt.Println("work2 get the number")
ch <- true
return
} else {
fmt.Println("work2 mine")
}
}
}
}
func main() {
arr := []int{1, 3, 5, 6, 7, 1, 3, 5, 7, 2, 3, 4}
ch := make(chan bool)
go work1(arr, ch)
go work2(arr, ch)
time.Sleep(1 * time.Second)
}
这里使用的Select是一个特殊的用法,比较类似Swtich。select 随机执行一个可运行的 case。如果没有 case 可运行,它将阻塞,直到有 case 可运行。一个默认(default)的子句应该总是可运行的。
同步协程的方法
根据上面的介绍我们已经得知了第一种同步的方法:通道chan
下面我们介绍第二种方法:WaitGroup
sync.WaitGroup
这是Go提供的等待组方法,通过使用WaitGroup可以有效的同步协程。
func main() {
var wg sync.WaitGroup
wg.Add(2)
go func() {
time.Sleep(1000)
fmt.Println("1👌")
wg.Done()
}()
go func() {
time.Sleep(500)
fmt.Println("2👌")
wg.Done()
}()
wg.Wait()
fmt.Println("ALL Finish")
}
context
context 主要用来在 goroutine 之间传递上下文信息,包括:取消信号、超时时间、截止时间、k-v 等。
创建context:
context.Background、context.Todo可以创建一个context
它有多个方法:
WithCancel函数,传递一个父Context作为参数,返回子Context,以及一个取消函数用来取消ContextWithDeadline函数,和WithCancel差不多,它会多传递一个截止时间参数,意味着到了这个时间点,会自动取消Context,当然我们也可以不等到这个时候,可以提前通过取消函数进行取消WithTimeout和WithDeadline基本上一样,这个表示是超时自动取消,是多少时间后自动取消Context的意思WithValue函数和取消Context无关,它是为了生成一个绑定了一个键值对数据的Context,这个绑定的数据可以通过Context.Value方法访问到
WithCancel
func main() {
ctx, cancel := context.WithCancel(context.Background())
go watch(ctx, "【监控1】")
go watch(ctx, "【监控2】")
go watch(ctx, "【监控3】")
time.Sleep(2 * time.Second)
fmt.Println("可以了,通知监控停止")
cancel()
//为了检测监控过是否停止,如果没有监控输出,就表示停止了
time.Sleep(5 * time.Second)
}
func watch(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Println("监控停止")
return
default:
fmt.Println(name + " 监控中...")
}
}
}
WithDeadline
func main() {
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second*3)) //3秒
go watch(ctx, "监控1")
go watch(ctx, "监控2")
fmt.Println("现在开始等待5秒,time=", time.Now().Unix())
time.Sleep(5 * time.Second)
fmt.Println("等待5秒结束,准备调用cancel()函数,发现两个子协程已经结束了,time=", time.Now().Unix())
cancel()
}
func watch(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Println("监控停止,time=", time.Now().Unix())
return
default:
fmt.Println(name + " 监控中...")
}
}
}
WithTimeout
WithTimeout和WithCancel的区别在于参数的不同,WithTimeout传入的是时间长短,而WithCancel是相对时间。
func main() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) //3秒
fmt.Println("开始监控,time=", time.Now().Unix())
go watch(ctx, "监控1")
go watch(ctx, "监控2")
fmt.Println("现在开始等待5秒,time=", time.Now().Unix())
time.Sleep(5 * time.Second)
fmt.Println("等待5秒结束,准备调用cancel()函数,发现两个子协程已经结束了,time=", time.Now().Unix())
cancel()
}
func watch(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Println("监控停止,time=", time.Now().Unix())
return
default:
fmt.Println(name + " 监控中...")
}
}
}
sync.Mutex 互斥锁
引用自:tour.go-zh.org/concurrency…
sync.Mutex保证每次只有一个 Go 程能够访问一个共享的变量,从而避免冲突?
这里涉及的概念叫做 互斥(mutualexclusion)* ,我们通常使用 互斥锁(Mutex) 这一数据结构来提供这种机制。
Go 标准库中提供了 sync.Mutex 互斥锁类型及其两个方法:
LockUnlock
type SafeCounter struct {
v map[string]int
mux sync.Mutex
}
// Inc 增加给定 key 的计数器的值。
func (c *SafeCounter) Inc(key string) {
c.mux.Lock()
// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
c.v[key]++
c.mux.Unlock()
}
// Value 返回给定 key 的计数器的当前值。
func (c *SafeCounter) Value(key string) int {
c.mux.Lock()
// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
defer c.mux.Unlock()
return c.v[key]
}
func main() {
c := SafeCounter{v: make(map[string]int)}
for i := 0; i < 1000; i++ {
go c.Inc("some-key")
}
time.Sleep(time.Second)
fmt.Println(c.Value("some-key"))
}
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。