文章目录
1、并发和并行
go语言协程依赖于线程,即使处理器运行的是统一线程,在线程内部go语言调度器也会切换多个协程执行,这个时候协程是并发的。如果多个协程被分配给了不同的线程,这些线程被不同的CPU核心处理,协程就是并行处理的。
故多核处理场景下,go语言的协程是并发
并发
并行
2、线程和协程的区别
协程是轻量级的线程
1、调度方式
协程是用户态的,协程和线程的对应关系是M:N。Go语言调度器可以将多个协程调度到同一个线程中,一个协程也可能切换到多个协程中执行
2、上线文切换速度
协程的速度要快于线程,是因为协程切换不用经过操作系统用户态和内核太的切换,并且协程切换的时候保留的寄存器要少于线程,线程切换大约需要1~2微秒,协程约为0.2微秒
3、调度策略
线程的调度大多数时间是抢占式的,操作系统调度器为了均衡每个线程的执行周期,会定时发出中断信号轻质执行线程上下文切换。而Go语言中的协程一般情况下是协作式调度,当一个协程处理完自己的任务后,可以主动将执行权限让给其他协程。这意味着协程可以更好的在规定时间内完成自己的工作,不会被轻易抢占。当一个协程运行了过长的时间,Go语言调度器才会抢占其执行
4、栈大小
线程的栈大小为2MB(避免栈一处),协程的栈默认为2KB。同时协程的栈在运行的时候是不能更改的,运行时会动态检测栈大小,动态扩容
3、golang 并发实现
goroutine 不是os线程、不是绿色线程(由语言运行时管理的线程),是协程。协程是一种非抢占式的简单并发子goroutine(函数、闭包或方法),也就是说,它们不能被中断。取而代之的是,协程有多个点,允许暂停或重新进入 —Go语言并发之道
goroutine
是go的并发体channels
是goroutine
的通信机制(实际就是发送数据)
基础知识
goroutine
goroutine是
一种轻量级的实现,可以在单个进程中执行成千上万的并发任务,是go语言并发设计的核心.
将关键字go
放在一个函数的前面,这个函数执行时就会成为一个独立的并发线程,这线程就被称为goroutine
channel 通道
通道
(chan)是一种特殊的类型,是一种引用类型。
主要分类有单向通道
、无缓冲的通道
、带缓冲的通道
主要作用是同步信号(shutdown/close/finish)
消息传递(queue/stream)
互斥(mutex)
//go中对chan的定义 /runtime/chan.go
type hchan struct {
qcount uint // total data in the queue 数据总量
dataqsiz uint // size of the circular queue
buf unsafe.Pointer // points to an array of dataqsiz elements
elemsize uint16
closed uint32 //标记channel已经关闭
elemtype *_type // element type
sendx uint // send index
recvx uint // receive index
recvq waitq // list of recv waiters 和ring buffer相关
sendq waitq // list of send waiters 和ring buffer相关
// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex //保护所有数据结构,但在某些quick path场景下不必加锁
}
type waitq struct {
first *sudog
last *sudog //sudog dequeue中等待的goroutine以及他的数据存储
}
- 任何时间只能有一个goroutine访问chan
- chan 里面的数据满足FIFO规则
//声明通道
var 通道变量 chan 通道类型
//创建通道
通道实例 := make(chan 通道类型)
//给通道发送数据
通道变量 <-值
//读取通道数据
data, ok := <-通道变量
defer关键字
注意:
defer
会对表达式进行提前求值
go调度模型
G:goroutine
M:系统线程
-> 执行代码的实体
P:Processor调度实体
实现方式
syn包
sync.WaitGroup
等待组讲解比较好的文章
进行多个任务的同步,主要操作有Add()、Done()、Wait()等操作
- 互斥锁和读写锁
互斥锁
sync.Mutex
:[互斥锁原理] (https://www.jb51.net/article/258684.htm)一个goroutine独占资源 互斥性、公平调度,饥饿处理
读写锁sync.RWMutex
:不可递归调用,读写互斥,多读之间并发
使用注意事项:
1、配套使用Lock、UnLock
2、运行时离开当前逻辑就释放锁
3、锁的粒度越小越好,加锁后尽快释放锁
4、没有特殊原因,尽量不要defer释放锁
5、RWMutex的读锁不要嵌套使用
- List item
- cond
sync.Cond
主要用于goroutine之间的协作,主要有三个函数Broadcast()
,Signal()
,Wait()
, 一个成员变量,L Lock
Broadcast()
:唤醒在本cond上等待的所有的goroutine
Signal()
:选择一个goroutine进行唤醒
Wait()
:让goroutine进行等待
- once
sync.Once
中有一个Do()方法,无论是否更换Do()里面的东西,这个方法只会执行一次
func main() {
var count int
increment := func() {
count++
}
var once sync.Once
var increments sync.WaitGroup
increments.Add(100)
for i:=0;i<100;i++{
go func() {
defer increments.Done()
once.Do(increment)
}()
}
increments.Wait()
fmt.Printf("count is %d\n", count)
}
//最终输出为 1
- 池
池(pool)是Pool模式的并发安全实现,需要参照pool设计模式
channel
select 语句
GOMAXPROCS控制
runtime.GOMAXPROCS(runtime.NumCPU()) //充分利用cpu资源
Go语言的并发通过
goroutine`实现。goroutine类似于线程,属于用户态的线程,我们可以根据需要创建成千上万个goroutine并发工作。goroutine是由Go语言的运行时(runtime)调度完成,而线程是由操作系统调度完成。
Go语言还提供
channel
在多个goroutine
间进行通信。goroutine和channel是 Go 语言秉承的CSP(Communicating Sequential Process)
并发模式的重要实现基础。
经典例子分析
case1
func handler(){
ch := make(chan string)
go func() {
time.Sleep(3*time.Second)
ch <- "job result"
}()
select {
case result := <- ch:
fmt.Println(result)
case <-time.After(time.Second):
return
}
}
上述代码会造成goroutine泄漏,原因在于channel没有缓存
可能造成goroutine泄漏的原因有:
1、channel没有缓存
2、select命中timeout逻辑
3、导致channel没有消费者
4、最终导致anonymous goroutine泄漏
case2
var mu sync.RWMutex
func main() {
go A()
time.Sleep(2*time.Second)
fmt.Println("main call lock")
mu.Lock()
defer mu.Unlock()
}
func A() {
fmt.Println("A call rlock")
mu.RLock()
fmt.Println("A rlocked")
defer mu.RUnlock()
B()
}
func B(){
time.Sleep(5*time.Second)
C()
}
func C() {
fmt.Println("C call rlock")
mu.RLock()
fmt.Println("C rlocked")
mu.RUnlock()
}
运行结果如下:
谨慎使用锁的递归调用,上面的还需再看一看
case3
go的
map
不可以并发
go
内置map
的几个要点:参考
- hash算法:AES
- 冲突解决:链地址法(和java类似) Python采用开放定址法
- range go每次rangemap的时候顺序都会不同,因为go故意实现了random 【需要代码论据】
- 装填因子 6.5
- rehash 渐进式rehash (和redis类似)