day 13

发布于:2025-02-10 ⋅ 阅读:(62) ⋅ 点赞:(0)

GMP模型

是什么:
GMP模型是go语言中管理协程调度的模型,因为协程是用户态的线程,不受内核调度,而是受程序控制。
为什么:
G:代表协程,协程对象,有着协程的信息,协程栈,程序计数器等
M:代表执行协程的线程
P:代表处理器,本地队列存放多个协程,管理G
怎么做:

  1. 创建一个协程G后,将其放到P的本地队列里,如果本地队列都满了,就放到全局队列里。
  2. P找空闲的M绑定,并执行P本地队列中的G,如果没有空闲M就创建一个M。
  3. 当M执行P中的G遇到阻塞事件,或者执行时间过长时,将G状态变为等待,放到阻塞事件的等待队列里,或者放到P末尾,M继续执行P中的下一个G。
  4. 如果P中为空,则去全局队列中找,全局队列也没有,则从其余P的本地队列偷取一半来执行。
  5. 阻塞事件结束后,将G放到原来的P队列末尾等待执行。
  6. 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。


网站公告

今日签到

点亮在社区的每一天
去签到