GMP模型
是什么:
GMP模型是go语言中管理协程调度的模型,因为协程是用户态的线程,不受内核调度,而是受程序控制。
为什么:
G:代表协程,协程对象,有着协程的信息,协程栈,程序计数器等
M:代表执行协程的线程
P:代表处理器,本地队列存放多个协程,管理G
怎么做:
- 创建一个协程G后,将其放到P的本地队列里,如果本地队列都满了,就放到全局队列里。
- P找空闲的M绑定,并执行P本地队列中的G,如果没有空闲M就创建一个M。
- 当M执行P中的G遇到阻塞事件,或者执行时间过长时,将G状态变为等待,放到阻塞事件的等待队列里,或者放到P末尾,M继续执行P中的下一个G。
- 如果P中为空,则去全局队列中找,全局队列也没有,则从其余P的本地队列偷取一半来执行。
- 阻塞事件结束后,将G放到原来的P队列末尾等待执行。
- M如果没有绑定的P来执行,就休眠状态。
p的数量,怎么设置,最高是多少个?
P的数量默认是CPU个数,理论可以设置任意正整数,但是过高会导致CPU频繁切换,浪费CPU时间。
可以通过环境变量或者程序内调用函数设置。
环境变量:export GOMAXPROCS=4
函数:num := runtime.GOMAXPROCS(4)
num是返回之前设置的数量。
本地队列、全局队列的长度,偷取时的偷取数量是多少?
**本地队列长度:**每个P最多存放256个G。
**全局队列长度:**动态大小的队列,切片类型。
**偷取数量:**随机一个队列偷取一半的G。
GMP模型协程最长的运行时间?
10ms。
channel在项目中的使用?
节点在etcd中注册服务时,etcd服务器会向客户端定时发送心跳消息,表明服务注册有效。
如果通道返回值为false,表明通道已经关闭,可能因为etcd的问题或者租约到期由于网络原因超过了租约时间,客户端这里没有收到通道ch发来的信号,就不会继续续约。
ch, err := cli.KeepAlive(context.Background(), leaseId)
if err != nil {
return fmt.Errorf("set keepalive failed: %v", err)
}
log.Printf("[%s] register service ok\n", addr)
for {
select {
// 停止服务注册
case err := <-stop:
if err != nil {
log.Println(err)
}
return err
//客户端关闭,调用了cancel函数或者超时时间到
case <-cli.Ctx().Done():
log.Println("service closed")
return nil
// 返回false,租约撤销,
case _, ok := <-ch:
// 监听租约
if !ok {
log.Println("keep alive channel closed")
_, err := cli.Revoke(context.Background(), leaseId)
return err
}
//log.Printf("Recv reply from service: %s/%s, ttl:%d", service, addr, resp.TTL)
}
}
在定时LRU中设置一个定时器协程,ticker在间隔时间每一分钟发送一次信号,表明到了定期时间,定期清理缓存中已经过期的元素。
和一个停止定时器的通道,传递信号。
func (c *Lru) startCleanupTimer() {
// 定义ticker时告诉间隔时间
ticker := time.NewTicker(c.interval)
defer ticker.Stop()
// 无限循环
for {
select {
// 每一分钟接收一个当前时间值
case <-ticker.C:
for e := c.l.Back(); e != nil; e = e.Prev() {
kv := e.Value.(*entry)
// 过期时间!=0,并且现在时间在过期时间之后
if !kv.expire.IsZero() && time.Now().After(kv.expire) {
c.removeElement(e)
} else {
break
}
}
case <-c.stopChan:
return
}
}
}
channel的底层结构体有没有了解?
channel是golang中用来实现多个goroutine通信的管道,它的底层是一个叫做hchan的结构体。在go的runtime包下。
主要包含:
- 一个指向底层循环队列的指针buf,用于保存协程之间传递的数据。
- 记录这个数组buf,当前发送和接收数据的下标值。
- 数组当前数量和总数
- 从该通道向那个协程发送,和从哪个协程接收的两个队列
- 保证向通道写入和读取安全的锁
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
timer *timer // timer feeding this chan
elemtype *_type // element type
sendx uint // send index
recvx uint // receive index
recvq waitq // list of recv waiters
sendq waitq // list of send waiters
// 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
}
例如:
G1协程中向G2通过通道发送数据。
//G1
func sendTask(taskList []Task) {
...
ch:=make(chan Task, 4) // 初始化长度为4的channel
for _,task:=range taskList {
ch <- task //发送任务到channel
}
...
}
//G2
func handleTask(ch chan Task) {
for {
task:= <-ch //接收任务
process(task) //处理任务
}
}
初始通道中buf为空,sendx和recvx都是0。G1向ch里添加数据是,先对buf加锁,将数据copy一份放到buf里,sendx++,解锁,G2接收数据,也对buf加锁,将数据copy一份到内存里,recvx++,解锁。
对数据的拷贝可以防止某个协程在发送过程中对数据修改,这样接收可能是修改后的值。
channel使用过程有哪些注意事项?
- var只是声明了一个通道,没有初始化底层缓冲区,使用没有初始化的通道会panic。必须用make初始化,无缓冲通道或者有缓冲通道。
- 确保发送和接收端都有,避免阻塞。确保select中所有case都能执行,可以添加default分支,不然select可能一直阻塞。
- 不再发送数据时要关闭通道,对关闭的通道发送数据会引起panic,但是可以从中读取数据,直到数据为空。
- 重复关闭也会panic。
map有看过么,哈希冲突的解决方案?
map是一种kv键值对的存储结构,底层用哈希表实现,key不能重复。
解决方案:
- 开放寻址法:插入元素时,目标桶已经被占用,会探测下一个桶,直到找到空桶。
- 链表法:每个存储桶包含一个指针,指向存储冲突元素的链表,发生冲突添加到链表末尾。
map的扩容
如果桶的链表长度过长超过桶的数量时,或者负载因子(map中的元素个数/桶数)大于6.5时,即元素个数超过桶个数6.5倍触发扩容。
双倍扩容:通常情况下进行双倍扩容。
渐进式扩容:在扩容时,新的插入操作,会同时插入原桶和新桶。新的读取操作会从原桶中读取。
map是线程不安全的
同一时刻,两个协程对同一map操作是不安全的。
sync包中的sync.map类型是一种并发安全的map,或者利用互斥锁保护map。