前言

Golang本身实现了线程调度,对于并行来说需要程序运行环境物理设备多核处理器的加持 ,单核只能实现并发。

Goroutine是Go语言中的协程(Coroutine),称为Goroutine。

GPM是Golang的Goroutine调度框架,可以把M个Goroutine映射到N个系统线程中,最终被多核CPU调度执行。

Golang中多个Goroutine并发/行起来之后,通过使用原子操作(atomic)---->锁------>带锁环形队列(Channel)等技术实现并发/行安全;

同步/并发/并行概念

同步:2个/多个独立运行个体之间的执行顺序相互依赖,要么A的执行依赖B的执行结果,要么B的执行依赖A的执行结果。

并发:同一时间段内+交替执行多个任务,并发描述的是1个时间段

并行:同一时刻+同时执行多个任务,不同的线程被不同的CPU执行,并行描述的是1个时间点

真并发=并行:多核处理器的并发

伪并发:单核处理器的并发

由于Python的全局解释器锁(GIL)导致多线程最终被调度到同1个CPU上。称为伪并发

在硬件设备满足多核CPU的前提下,GPM-Goroutine调度框架可以把多个Goroutines同时调度到不同的CPU上执行,称为真并发/并行。

Goroutine

Goroutine本质是协程。

在java/c++中我们要实现并发编程的时候,我们通常需要自己维护一个线程池,并且需要自己去包装一个又一个的任务。

同时需要自己去调度线程执行任务并维护上下文切换,这一切通常会耗费程序员大量的心智。

那么能不能有一种机制,程序员只需要定义很多个任务,让系统去帮助我们把这些任务分配到CPU上实现并发执行呢?

我们原来的实现使用线程实现并发的方案流程是:

  • 程序----》os线程池-----》os调度线程----->cpu

在Golang中

  • 程序---》goroutine------》go's runtime调度goroutines--------》线程池-------》os线程接口-----》os调度线程----->cpu

1.使用goroutine

Goroutine有1个特性,一旦main函数结束,所有Goroutines也会全部消失。

因为mian函数结束相当于进程(资源单位)结束了,皮之不存毛将焉附?

package main

import "fmt"

func hello() {
	fmt.Println("hello")
}
//程序启动之后会主动创建1个main goroutine
func main() {
	go hello() //开启1个独立的goroutine
	fmt.Println("main")
	//main函数结束之后由main函数启动的goroutine也全部结束
}

2.控制goroutine的执行顺序

sync.WaitGroup保证多个goroutine执行顺序
使用sync.WaitGroup可以使main goroutine等待所有子goroutine!
package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

//waitGroup协调gortines顺序
var wg sync.WaitGroup

func f() {
    //在go中生成随机数字(要加seed种子)
    rand.Seed(time.Now().UnixNano())
    for i := 0; i < 5; i++ {
        n1 := rand.Intn(11)
        fmt.Println(n1)
    }

}

func f1(i int) {
    // goroutine结束就登记-1
    defer wg.Done()
    //开启1个goroutine:睡300毫秒
    time.Sleep(time.Millisecond * time.Duration(rand.Intn(300)))
    fmt.Printf("goroutine%d\n",i)
}

func main() {
    for i := 0; i < 10; i++ {
        // 启动一个goroutine就登记+1
        wg.Add(1)
        go f1(i)
    }
    //如何等待10个goroutines全部完成,main函数再结束。
    wg.Wait()//wg.Wait()等待计数器减为0

}
waitGroup使用

4.内核线程和Goroutines的关系

CPU执行的最小单位是OS线程,所有的goroutine最终都需要被runtime调度映射到真正的OS线程上,被CPU执行。

1个OS线程对应用户态N个Goroutine。

1个GO程序可以被Processor处理器使用多个OS线程。

Goroutines和OS线程是多对多的映射关系=M:N。

5.GMP并发模型

GMP的并发模型是Golang可以高效支持大并发的原因。

G:goroutine用户态定义的协程。

M:Macheine的意思,M和内核线程是1对1绑定的,且绑定关系固定不变。

P:Processor虚拟处理器的意思,P的数量不会多余M,把goroutine队列中等待执行的goroutine调度到M上执行,当goroutine执行遇到系统调用,P和会G接触绑定关系,当P空闲时,P会去其他P的goroutine队列中获取goroutine来执行,以分摊其他P的工作。

Runqueue:当所有的Processor都挂载了goroutine队列,没有可以的P使goroutine会被保存到该全局队列,等待空闲的P来获取和挂载到自己的goroutine队列中。

线程缓存:当M空闲时会被存放到线程缓存中,无需重新创建M。

6.Goroutine池

在Golang中可以轻松启动多个goroutine,但物极必反,无论我们启动多少个goroutine最终干活的还是os线程。

Goroutine池可以限制Goroutine的数量。

1个8核的服务器可以同时启动16个线程,但是golang中启动了1000个goritine。

16个os线程划分1000个goroutine无疑是增加了go runtime调度频率。并没有加速程序执行速度。

Goroutine池

Channel

Channel的本质是1个加锁的环形队列,环形队列的本质是1个固定长度、固定大小的数组;

环形队列的特点是长度固定,不断地记录rear和front指针(数组的索引),通过rear和front数组索引,在1个定长数组中读写数据;

  • 环形队列写满时,写端阻塞!
  • 环形队列读完时,读端阻塞!

Channel完全具备环形队列的所有特点

  • 通道channel分为2端,即读端和写端。
  • 写端往channel发送数据时,没有读端来读,写端阻塞。
  • 写端从channel接收数据时,没有写端来写,读端阻塞。
  • 因为channel具备以上2个特点,读端和写端必须是2个不同的goroutine,否则造成阻塞和死锁。
  • Channel本身具有锁机制,是同步的,当多个Goroutines争抢1个channel的读/写操作权限时,同1时刻只能有1个Goroutine抢到。

1.Channel创建

每1个Channel都是1个具体类型的导管,叫作Channel的元素类型。

例如:1个有int类型元素的通道,写为

chan int

像map类型一样channel类型是1个通过make创建的引用类型。

当Channel作为参数传递到另1个函数时,复制的是channel引用,这样调用者和被调用者都会引用同1份数据结构。

和其他引用类型一样channel的零值为nil。

2.Channel比较运算

2个同类型的channel可以使用==符合进行比较运算。

var chan1 = make(chan struct{}, 1)
var chan2 = make(chan struct{}, 1)
func main() {
    fmt.Println(chan1 == chan2)
}

但是只有2个同类型的channel都是同1个Channel的引用时,比较值=true。

var chan1 = make(chan struct{}, 1)
var chan2 = make(chan struct{}, 1)
//比较2个同类型的channel
func isEqual(ch1, ch2 chan struct{}) (res bool) {
    res = ch1 == ch2
    return
}
func main() {
    res1 := isEqual(chan2, chan1)
    fmt.Println(res1) //false
    //只有2个同类型的channel都是同1个Channel的引用时,比较值=true。
    res2 := isEqual(chan1, chan1)
    fmt.Println(res2) //true
    res3 := isEqual(chan2, chan2)
    fmt.Println(res3) //true
}

Channel也可以和nil进行比较。

var chan1 = make(chan struct{}, 1)
func main() {
    fmt.Println(chan1 == nil)
} 

3.Channel操作

1.两个主要操作

channel有2个主要操作:发送(send)和接收(receive)两者结合在一起统称为1次通信。

var chan1 = make(chan struct{}, 1)
​
func main() {
    //发送
    chan1 <- struct{}{}
    //接收
    <-chan1
}

2.关闭操作

Channel关闭代表着该通道写入完成

  • channel关闭之后, 读取到的是通道元素类型的零值,不会引发异常。
  • channel关闭之后,写入会引发 panic: send on closed channel异常。
  • channel关闭之后,再次关闭channel会引发panic: close of closed channel异常。
close(chan1)

3.for range读操作

Channel关闭代表着该通道的写入完成

package main

import (
    "fmt"
    "golang.org/x/sys/windows"
    "time"
)

var naturalNumberCh = make(chan int, 100)

func write() {
    threadID := windows.GetCurrentThreadId()
    defer fmt.Printf("----write-%d结束\n", threadID)
    for i := 0; i <= 100; i++ {
        naturalNumberCh <- i
    }

}

func read() {
    threadID := windows.GetCurrentThreadId()
    defer fmt.Printf("----read-%d结束\n", threadID)
    for n := range naturalNumberCh {
        fmt.Printf("----read-%d读取到值%d\n", threadID, n)
    }

}
func main() {
    go write()
    go read()
    go read()
    go read()
    time.Sleep(10 * time.Second)
    fmt.Println("main关闭channel")
    //channel一旦关闭读取当前channel的3个read goroutie完成读取之后,立即结束阻塞,退出!
    close(naturalNumberCh)
    time.Sleep(10 * time.Second)

}
channel关闭

Channel关闭后,使用for range读当前channel的全部Goroutines,完成读取---->结束阻塞----->退出for range循环。

  • 在循环遍历channel时,如果channel已关闭,for循环正常遍历,正常退出!
  • 在循环遍历channel时,如果channel未关闭,for range循环读完channel中的值之后还会继续读,for range循环不结束,导致当前Goroutine一直阻塞,无法正常退出,最终可能会造成死锁!

for range循环遍历channel引发的死锁问题

package main

import (
    "fmt"
    "sync"
)

var wg sync.WaitGroup
var naturalCh = make(chan int)
var squareCh = make(chan int)

func counter() {
    for i := 0; i < 100; i++ {
        naturalCh <- i
    }
    fmt.Println("counter协程结束")
    wg.Done()
}
func squarer() {
    for n := range naturalCh {
        squareCh <- n * n
    }
    //Channel不关闭,for range循环会一直读channel造成当前Goroutine一直阻塞,一直不结束
    fmt.Println("squarer协程结束")
    wg.Done()
}
func printer() {
    for n := range squareCh {
        fmt.Println(n)
    }
    //Channel不关闭,for range循环会一直读channel造成当前Goroutine一直阻塞,一直不结束
    fmt.Println("printer协程结束")
    wg.Done()
}

func main() {
    wg.Add(3)
    go counter()
    go squarer()
    go printer()
    fmt.Println("main协程结束")
    //wg一直等待squarer和sprinter结束,但是这2个Goroutine一直不结束!!!
    wg.Wait()
}
for range死锁问题

5.循环监听读取

select可以同时监听多个channel是否可读

监听不能只监听1次,需要配合死循环进行循环监听

循环监听需要有死循环的结束条件,需要配合context

package main

import (
    "context"
    "fmt"
    "golang.org/x/sys/windows"
    "time"
)

var naturalNumberCh = make(chan int, 100)

func write() {
    //记得channel写入完成关闭,否则for range循环读一直不结束!
    defer close(naturalNumberCh)
    threadID := windows.GetCurrentThreadId()
    defer fmt.Printf("----write-%d结束\n", threadID)
    for i := 0; i <= 3; i++ {
        naturalNumberCh <- i
    }

}

func read(ctx context.Context) {
    threadID := windows.GetCurrentThreadId()
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("----read-%d结束------\n", threadID)
            return
        default:
            for n := range naturalNumberCh {
                fmt.Printf("----read-%d读取到值%d\n", threadID, n)
            }
        }
    }

}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    go write()
    go read(ctx)
    go read(ctx)
    go read(ctx)
    time.Sleep(3 * time.Second)
    fmt.Println("main发送关闭信号给读取gorutine")
    cancel()
    time.Sleep(10 * time.Second)
    fmt.Println("mian结束")

}
for{select{}}

 

6.操作总结

Channel的读、写操作遵循供需、守恒原则,生产方Goroutine生产值的数量与消费方Goroutine消费值的数量比例为1:1

Channel的读写操作自带锁功能,同1个时刻只能有1个Goroutine执行channel的读/写操作。

使用for range循环读channel一定记得手动关闭channel,否则for range循环一直不结束,goroutine就会一直阻塞,进而导致死锁。

操作/状态channel=nil正常channel已关闭的channel
阻塞 成功或阻塞 读到零值
阻塞 成功或阻塞 panic
关闭 close(ch) panic 成功 panic

4.Channel分类

根据Channel容量,可以把Channel划分为有缓冲Channel和无缓冲channel。

  • 有缓冲通道(BufferdChannel) :不能缓冲数据,容量=0
  • 无缓冲通道:(unbufferdChannel):可以缓冲一定数量的数据,容量>0

根据Channel支持的读、写功能,可以把Channel划分为单向Channel和双向Channel。

  • 单向channel:仅支持读或写,1种功能。
  • 双向channel:同时支持读和写,2种功能。

1.无缓冲Channel

无缓冲channel:不能缓冲数据,容量=0

var unbufferdCh1 = make(chan struct{})    //无缓冲channel
var unbufferdCh2 = make(chan struct{},0)  //无缓冲channel
var bufferdCh = make(chan struct{}, 1)    //容量=1的有缓冲channel

当1个goroutine1向无缓冲channelA发送数据时,goroutine1会进行阻塞状态,直到另1个goroutine2从读无缓冲channelA读取数据。

此时1次通信操作完成,goroutine1和goroutine2都同时处于运行状态

相反如果无缓冲ChannelA的读操作先执行,goroutine2会进行阻塞状态,直到goroutine1向无缓冲channelA写入数据。

此时1次通信操作完成,goroutine2和goroutine1都同时处于运行状态。

2个goroutine使用无缓冲通道通信会导致goroutine同步化,因此无缓冲通道也称为同步通道。

经典案例:

基于1个无缓冲channel特性,使用2个Goroutine交替打印奇偶数。

package main

import (
    "fmt"
    "sync"
)

var wg sync.WaitGroup

//无缓冲Channel
var ch = make(chan struct{})

//Goroutine写
func workerW(ch chan struct{}) {
    for i := 1; i <= 10; i++ {
        fmt.Println("workerW开始", i)
        ch <- struct{}{}
        fmt.Println("workerW结束", i)
    }
    wg.Done()
}

//Goroutine读
func workerR(ch chan struct{}) {
    for i := 1; i <= 10; i++ {
        fmt.Println("workerR开始", i)
        <-ch
        fmt.Println("workerR结束", i)
    }
    wg.Done()
}

func main() {
    wg.Add(2)
    go workerW(ch)
    go workerR(ch)
    wg.Wait()

}

/*
执行结果:假设workerR Goroutine先开始执行
-----------------------------------------
1. workerR开始 i=1, 然后workerR读无缓冲Channel进入阻塞,workerR阻塞,workerW执行 ---workerR读阻塞
-----------------------------------------
2. workerW开始 i=1  然后workerW写入无缓冲Channel,结束workerR的阻塞              ---workerW写不阻塞
3. workerW结束 i=1  workerW继续执行
4. workerW开始 i=2,然后workerW写无缓冲Channel进入阻塞,workerR执行         ---workerW写阻塞
-----------------------------------------
5. workerR结束 i=1  workerR执行
6. workerR开始 i=2,然后workerR读无缓冲Channel,结束workerW的阻塞             ---workerR读不阻塞
7. workerR结束 i=2  workerR继续执行
8. workerR开始 i=3,然后workerR读无缓冲Channel进入阻塞,workerR阻塞,workerW执行 ---workerR读阻塞
-----------------------------------------
9. workerW结束 i=2  workerW继续执行
10.workerW开始 i=3  然后workerW写无缓冲Channel,结束workerR的阻塞       ---workerW写不阻塞
11.workerW结束 i=3  workerW继续执行
12.workerW开始 i=4,然后workerW写无缓冲Channel进入阻塞,workerR执行     ---workerW写阻塞
-----------------------------------------
13.workerR结束 i=3  workerR继续执行
14.workerR开始 i=4  然后workerR读无缓冲Channel,结束workerW的阻塞           ---workerR读不阻塞
15.workerR结束 i=4  workerR执行
16.workerR开始 i=5,然后workerR读无缓冲Channel进入阻塞,workerR阻塞,workerW执行 ---workerR读阻塞
-----------------------------------------
*/
使用1个无缓冲Channel交替打印奇偶数

无缓冲channel控制多个Goroutine的执行顺序

package main

import (
    "fmt"
)

func main() {
    ch1 := make(chan bool)
    ch2 := make(chan bool)
    go func() {
        fmt.Println("step1")
        <-ch1
    }()
    go func() {
        ch1 <- true
        fmt.Println("step2")
        ch2 <- true
    }()
    <-ch2
    fmt.Println("step3")
}
依赖链

2.单向Channel

当1个Channel用作函数的形参时,它几乎被有意地限制不能发送或者不能接收。

限制函数操作Channel的权限可以避免Channel被误用。

Go提供了单向Channel类型,仅支持发送 or 读取1种操作。

sync包

Golang除了提供channel 这种CSP机制达到goroutines之间共性数据目的之外,还提供了1个sync包实现并发安全。

sync包中提供了Mutex(互斥锁)、once(一次性操作)、waigroup(主线程等待所有goroutine结束再推出)、RWMutex(读写相互斥锁)等功能,帮助我们实现并发安全

package main

import (
    "context"
    "fmt"
    "golang.org/x/sys/windows"
    "sync"
)

var naturalNumberCh = make(chan int, 100)
var wg sync.WaitGroup
var donech = make(chan uint32, 3)

func write() {
    threadID := windows.GetCurrentThreadId()
    defer func() {
        wg.Done()
        //记得channel写入完成关闭,否则for range循环读一直不结束!
        close(naturalNumberCh)
        fmt.Printf("----write-%d结束\n", threadID)
    }()
    for i := 0; i <= 100; i++ {
        naturalNumberCh <- i
    }

}

func read(ctx context.Context, once *sync.Once) {
    defer wg.Done()
    threadID := windows.GetCurrentThreadId()
    //循环监听
    for {
        //监听多个channel
        select {
        //1.监听结束信号
        case <-ctx.Done():
            fmt.Printf("----read-goroutine-%d结束------\n", threadID)
            return
        //2.不结束即执行
        default:
            //记得写完了关闭Channel,否则for range循环不结束!
            for n := range naturalNumberCh {
                fmt.Printf("----read-goroutine-%d读取到值%d\n", threadID, n)
            }
            once.Do(func() {
                donech <- threadID
            })
        }
    }
}

func main() {
    defer fmt.Println("mian函数结束")
    ctx, cancel := context.WithCancel(context.Background())
    readerCount := 3
    writeCount := 1
    //开1个写go程
    for i := 0; i < writeCount; i++ {
        go write()

    }
    //开3个读go程执行结束后主动通知main函数,发请求结束的请求!
    for i := 0; i < readerCount; i++ {
        go read(ctx, &sync.Once{})
    }
    gocount := readerCount + writeCount
    wg.Add(gocount)
    //mian函数收到了3个读goroutine发送的请求结束请求,调用cancel主动结束它们!
    for i := 0; i < readerCount; i++ {
        fmt.Printf("----read-goroutine-%d请求结束!\n", <-donech)
    }
    cancel()
    wg.Wait()
}
Goroutines控制案例

1.goroutine资源争用现象

我们知道MySQL客户端用到的数据放在mysqld服务端的数据库中当多个客户端连接数据库时有事会需要加锁操作保证数据安全。

程序中用到变量数据在内存里,我开多个goroutine去同时对同1个全局变量进行修改,相当于多个MySQL的客户端同时对数据库同1条数据进行修改。

var wg sync.WaitGroup

//定义1个全局变量
var number int64

//对全局变量进行+1操作
func add1() {
    for i := 0; i < 5000; i++ {
        //1.从内存中找到number变量对应的值
        //2.进行+1操作
        //3.把结果赋值给number写到内存
        number++
    }
    wg.Done()
}
func main() {
    wg.Add(2)
    go add1()
    go add1()
    //fmt.Println(number)
    wg.Wait()
    fmt.Println(number) //每次执行结果都不一致
}

2.sync.Mutex互斥锁

Mutex可以防止同1时刻,同1资源(全局变量)被多个goroutine操作。

互斥锁不区分是读、写操作,只要有1个goruitne拿到Mutax,其余的所有goroutines,无论是读还写,只能等待。

Mextex是使用struct实现的而在golang中struct属于value类型。

需要注意的是在使用sync.Mutex时如果把它当成参数传入到函数里面,mutax就会被copy生成2把不同的mutex。

var lock sync.Mutex
lock.Lock()   //加锁
lock.Lock()   //加锁

1个公共资源被N个goroutines 操作引发的问题 

package main

import (
	"fmt"
	"sync"
)

//锁
var x = 0
var wg sync.WaitGroup

//每次执行add增加5000
func add() {
	defer wg.Done()
	for i := 0; i < 5000; i++ {
		x++
	}

}

func main() {
	wg.Add(2)
	//开启2个goroutines同时对x+1
	go add()
	go add()
	/*2个goroutines如果同1时刻都去获取公共变量x=50,
	然后在独自的栈中对x+1改变了x都=51
	就少+了1次,导致结果计算不准!
	*/
	wg.Wait()
	fmt.Println(x)
}

3.使用互斥锁

A Mutex must not be copied after first use.
在使用互斥锁一定要确保该锁不是复制品(作为参数传递时一定要传指针
package main

import (
	"fmt"
	"sync"
)

//锁
var x = 0
var wg sync.WaitGroup

/*
A Mutex must not be copied after first use.
使用互斥锁一定要确保该锁不是复制品(作为参数传递时一定要传指针)
*/

//互斥锁
var lock sync.Mutex

//每次执行add增加5000
func add() {
	defer wg.Done()
	for i := 0; i < 5000; i++ {
		lock.Lock()   //加锁
		x++           //操作同1资源
		lock.Unlock() //释放锁
	}

}

func main() {
	wg.Add(2)
	//开启2个goroutines同时对x+1
	go add()
	go add()
	wg.Wait()
	fmt.Println(x)
}

  

4.RWMutex(读/写互斥锁)

使用数据库时我们大部分的场景都是读的频率高于写的频率,所以我们可以使用2个数据库,1个叫主库另1个叫从库,主库支持写操作,从库支持度操作,主从之间通过bin log同步数据。

如果现在数据在内存中放着也是读变量的频率远远高于修改变量的频率。我们可以使用RWmutex

互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的,当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景下使用读写锁是更好的一种选择。读写锁在Go语言中使用sync包中的RWMutex类型。

读写锁分为两种:

读锁:当一个goroutine获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待;

写锁:当一个goroutine获取写锁之后,其他的goroutine无论是获取读锁还是写锁都会等待;

var rwlock sync.RWMutex
//读锁
rwlock.RLock()
rwlock.RUnlock()
//写锁
rwlock.Lock()
rwlock.Unlock()

Rwmutex区分goroutine读、写操作,仅在写时资源被lock,读的goroutines等(读并发、写串行)

应用场景:所以使用RWMutex之后,在读操作大于写操作次数的场景下并发执行效率会比Mutex更快。

如果读和写的操作差别不大,读写锁的优势就发挥不出来。

package main

import (
	"fmt"
	"sync"
	"time"
)

var x = 0
var lock sync.Mutex
var rwlock sync.RWMutex
var wg sync.WaitGroup

//rwlock
func read() {
	defer wg.Done()
	//加普通互斥锁
	// lock.Lock()
	//加读锁
	rwlock.RLock()
	fmt.Println(x)
	time.Sleep(time.Millisecond)
	//释放普通互斥锁
	// lock.Unlock()
	//释放读锁
	rwlock.RUnlock()
}

func write() {
	defer wg.Done()
	// lock.Lock()
	//加写锁
	rwlock.Lock()
	x++
	time.Sleep(10 * time.Millisecond)
	// lock.Unlock()
	//释放写锁
	rwlock.Unlock()
}

func main() {

	start := time.Now()

	for i := 0; i < 10; i++ {
		go write()
		wg.Add(1)
	}
	//读的次数一定要大于写的次数
	for i := 0; i < 1000; i++ {
		go read()
		wg.Add(1)
	}
	wg.Wait()
	fmt.Println(time.Now().Sub(start))

	//Mutex:1.205s
	//RWMutex 194ms
}

6.sync.WaitGroup

var wg sync.WaitGroup
wg.Add(2)
wg.Done(2)
wg.Wait()

主goroutine结束之后,又它开启的其他goroutines会自动结束!!     

如何做到让main goroutine等待它开启的goroutines结束之后,再结束呢?

main goroutine执行time.Sleep(duration)肯定是不合适,因为我们无法精确预测出 goroutines到底会执行多久?

方法名功能
(wg * WaitGroup) Add(delta int) 计数器+delta
(wg *WaitGroup) Done() 计数器-1
(wg *WaitGroup) Wait() 阻塞直到计数器变为0

 

var wg sync.WaitGroup

func hello() {
	defer wg.Done()
	fmt.Println("Hello Goroutine!")
}
func main() {
	wg.Add(1)
	go hello() // 启动另外一个goroutine去执行hello函数
	fmt.Println("main goroutine done!")
	wg.Wait()
}

7.sync.Once

 如何确保某些操作在并发的场景下只执行1次,例如只加载一次配置文件、只执行1次close(channel)等。

func (o *Once) Do(f func()) {}

Onece的Do方法只能接受1个没有参数的函数作为它的参数,  如果要传递的func参数是有参数的func, 就需要搭配闭包来使用。  

 

下面是借助sync.Once实现的并发安全的单例模式:

package singleton

import (
    "sync"
)

type singleton struct {}

var instance *singleton
var once sync.Once

func GetInstance() *singleton {
    once.Do(func() {
        instance = &singleton{}
    })
    return instance
} 

8.sync.Map

Golang内置的Map数据类型是非线程安全的Map;

sync包提供了1个线程安全的Map即sync.map。

sync.map内部使用2个map即read和dirty,实现读写分离的机制;

type Map struct {
 mu Mutex
 // 把read看成一个安全的只读快照表,实际对应的是readOnly, 
 read atomic.Value // readOnly
 // dirty需要使用上面的mu加锁才能访问里面的元素,
 //dirty中包含所有在read字段中但未被expunged(删除)的元素,
 //重点包含最新的 KV 对,等时机成熟,dirty 会被转换为 read, 然后该字段会被置为空
 dirty map[interface{}]*entry
 // misses是一个计数器,记录在从read中读取数据的时候,没有命中的次数,
 //每次从 read 中没找到回到 dirty 中查询都会导致 misses 自增一,
 //当misses > len(dirty) 时,就会触发dirty转换
 misses int
}

在读取时不需要加锁,在写入时则会进行细粒度的锁定,以保证数据的一致性和并发安全性;

 

var syncMap sync.Map
//新增
syncMap.Store(key, n)
//删除
syncMap.Delete(key)
//改
syncMap.LoadOrStore(key)
//遍历
syncMap.Range(walk)

golang中的map在并发情况下: 只读是线程安全的,但是写线程不安全,所以为了并发安全 & 高效,官方帮我们实现了另1个sync.map。

fatal error: concurrent map writes  //go内置的map只能支持20个并发写!
package main

import (
	"fmt"
	"strconv"
	"sync"
)

var m = make(map[string]int)

func get(key string) int {
	return m[key]
}

func set(key string, value int) {
	m[key] = value
}

func main() {
	wg := sync.WaitGroup{}
	for i := 0; i < 20; i++ {
		wg.Add(1)
		go func(n int) {
			key := strconv.Itoa(n)
			//设置1个值
			set(key, n)
			//获取1个值
			fmt.Printf("k=:%v,v:=%v\n", key, get(key))
			wg.Done()
		}(i)
	}
	wg.Wait()
}

就支持20个并发也太少了!

Go语言的sync包中提供了一个开箱即用的并发安全版map–sync.Map。开箱即用表示不用像内置的map一样使用make函数初始化就能直接使用。

同时sync.Map内置了诸如StoreLoadLoadOrStoreDeleteRange等操作方法。

package main

import (
	"fmt"
	"strconv"
	"sync"
)

var syncMap sync.Map
var wg sync.WaitGroup



func walk(key, value interface{}) bool {
	fmt.Println("即将删除Key =", key, "Value =", value)
	syncMap.Delete(key)
	return true
}

func main() {
	for i := 0; i < 200; i++ {
		//开启20个协程去syncMap并发写操作,也是可以顺利写进去的的!
		key := strconv.Itoa(i)
		wg.Add(1)
		go func(n int) {
			//设置key
			syncMap.Store(key, n)
			//通过key获取value
			value, ok := syncMap.Load(key)
			if !ok {
				fmt.Println("没有该key", key)
			}
			fmt.Println(value)
			wg.Done()
		}(i)

	}
	//使用for 循环或者 for range 循环无法遍历所有syncMap只能使用syncMap.Range()
	//不幸运的Go没有提供sync.Map的Length的方法,需要自己实现!!
	syncMap.Range(walk)
	wg.Wait()
}

atomic包

Golang的sync.Mutex锁的底层都是通过atomic来实现的;

原子操作概念

在程序中执行1行内容为a=a+1的代码时,计算机底层的CPU其实是分了多个步骤去处理它;

  • 从内存中获取a变量原值
  • 对a变量原值进行+1操作
  • +1操作结果赋值给变量a

以上多个步骤处理,那么就意味着有中间状态(操作中、没操作完的状态);

而原子操作,它是一个不可分割的整体,没有中间状态,要么成功了、要么失败了。

子操作需要通过给底层的CPU发送原子操作指令去实现

在并发读写模式下,在变量级别使用原子操作的好处是在多Goroutine并发操作的同1个变量时

  • 可以保证当前变量值一致性
  • 比sync.Mutex锁的粒度更小,性能更快

原子操作函数

atimic包提供了一组原子操作函数,用于对值类型的变量进行原子操作

方法解释
func LoadInt32(addr *int32) (val int32)
func LoadInt64(addr *int64) (val int64)
func LoadUint32(addr *uint32) (val uint32)
func LoadUint64(addr *uint64) (val uint64)
func LoadUintptr(addr *uintptr) (val uintptr)
func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)
读取操作
func StoreInt32(addr *int32, val int32)
func StoreInt64(addr *int64, val int64)
func StoreUint32(addr *uint32, val uint32)
func StoreUint64(addr *uint64, val uint64)
func StoreUintptr(addr *uintptr, val uintptr)
func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)
写入操作
func AddInt32(addr *int32, delta int32) (new int32)
func AddInt64(addr *int64, delta int64) (new int64)
func AddUint32(addr *uint32, delta uint32) (new uint32)
func AddUint64(addr *uint64, delta uint64) (new uint64)
func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)
修改操作
func SwapInt32(addr *int32, new int32) (old int32)
func SwapInt64(addr *int64, new int64) (old int64)
func SwapUint32(addr *uint32, new uint32) (old uint32)
func SwapUint64(addr *uint64, new uint64) (old uint64)
func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)
func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)
交换操作
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)
func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)
func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)
func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)
比较并交换操作

 

ackage main

import (
	"fmt"
	"sync"
	"sync/atomic"
	"time"
)

type Counter interface {
	Inc()
	Load() int64
}

// 普通版
type CommonCounter struct {
	counter int64
}

func (c CommonCounter) Inc() {
	c.counter++
}

func (c CommonCounter) Load() int64 {
	return c.counter
}

// 互斥锁版
type MutexCounter struct {
	counter int64
	lock    sync.Mutex
}

func (m *MutexCounter) Inc() {
	m.lock.Lock()
	defer m.lock.Unlock()
	m.counter++
}

func (m *MutexCounter) Load() int64 {
	m.lock.Lock()
	defer m.lock.Unlock()
	return m.counter
}

// 原子操作版
type AtomicCounter struct {
	counter int64
}

func (a *AtomicCounter) Inc() {
	atomic.AddInt64(&a.counter, 1)
}

func (a *AtomicCounter) Load() int64 {
	return atomic.LoadInt64(&a.counter)
}

func test(c Counter) {
	var wg sync.WaitGroup
	start := time.Now()
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			c.Inc()
			wg.Done()
		}()
	}
	wg.Wait()
	end := time.Now()
	fmt.Println(c.Load(), end.Sub(start))
}

func main() {
	c1 := CommonCounter{} // 非并发安全
	test(c1)
	c2 := MutexCounter{} // 使用互斥锁实现并发安全
	test(&c2)
	c3 := AtomicCounter{} // 并发安全且比互斥锁效率更高
	test(&c3)
}

 

https://morsmachine.dk/go-scheduler

posted on 2020-04-22 19:19  Martin8866  阅读(1598)  评论(0编辑  收藏  举报