Go面试相关 - goroutine

1、进程、线程和协程的不同

进程:进程是具有一定独立功能的程序,进程是系统资源分配和调度的最小单元。每个进程都有自己独立的内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立内存,所以上下文进程间的切换开销比较大(寄存器、虚拟内存、文件句柄等),但相对比较稳定安全。

线程:线程是进程的一个实体,线程是内核态,而且是CPU调度和分派的基本单位,它是比进程更小的独立单元。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定,容易丢失数据。一个进程可以有多个线程,同一个进程内的线程共享了堆内存,所以经常会引起编发编程问题。

协程:协程是一种用户态的轻量级线程,协程的调度完全是由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文切换非常快。

2、为什么要引入协程?

协程的引入,本质上是为了规避一个问题:我又想有并发,又不想陷入到内核里面去,于是有了协程这个东西。

协程是轻量级的线程,主要体现在两个方面:

1、所需的资源更少。

2、创建、销毁和调度更轻量,并不需要陷入内核。

随着计算机性能提高、业务的发展,要求我们有更高的并发,而大多数并发执行的任务都是短平快,单独一个线程划不来,即时使用线程池,也会带来频繁切换上下文、陷入内核的问题。

因此,我们需要更轻量的东西取代线程。

3、goroutine泄露典型场景

参考煎鱼大佬:

跟读者聊 Goroutine 泄露的 N 种方法,真刺激!_煎鱼(EDDYCJY)的博客-CSDN博客

排场主要用 rumtine.NumGoroutine 或者 pprof 工具,pprof 会返回所有带有堆栈跟踪的 goroutine 列表。

4、怎么避免 goroutine 泄露

如果你不知道 goroutine 什么时候会结束,就不要使用 goroutine , 则是核心原则。

防止 goroutine 泄露可以由以下两个方向:

1、超时控制,使用context.Timeout的特性。

2、信号通知,主动发送信号给goroutine关闭,一般是要使用channel的特性。

这两个方向,都离不开select来配合,要么是业务正常结束,退出 goroutine,要么是超时,或者收到关闭信号,异常退出。

如:

package main

import (
	"context"
	"fmt"
	"time"
)

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	bsChan := make(chan struct{})
	go func() {
		slowBusiness()
		bsChan <- struct{}{}
	}()
	select {
	case <-ctx.Done():
		fmt.Println("timeout")
	case <-bsChan:
		fmt.Println("business end")
	}
}

func slowBusiness() {
	time.Sleep(2 * time.Second)
}

5、Mutext 加锁

mutex加锁大概分成两种模式:

1、正常模式下,goroutine会通过自旋来获得锁;

2、但是如果存在一个 goroutine 等待锁超过 1ms,那么 mutex 就会进入饥饿模式,在饥饿模式下,会遵循 FIFO 原则,将锁交给下一个 goroutine。也就是通过 P 等待队列队里里面唤醒第一个等待者。

6、什么是饥饿模式

mutex有两种模式 — 正常模式和饥饿模式。

在正常模式下,锁的等待者会按照先进先出的顺序获取锁。但是刚被唤起的 Goroutine 与新创建的 Goroutine 竞争时,大概率会获取不到锁,为了减少这种情况的出现,一旦 Goroutine 超过 1ms 没有获取到锁,它就会将当前互斥锁切换饥饿模式,防止部分 Goroutine 被“饿死”。

在饥饿模式中,互斥锁会直接交给等待队列最前面的 Goroutine。新的 Goroutine 在该状态下不能获取锁、也不会进入自旋状态,它们只会在队列的末尾等待。如果一个 Goroutine 获得了互斥锁并且它在队列的末尾或者它等待的时间少于 1ms,那么当前的互斥锁就会切换回正常模式。

与饥饿模式相比,正常模式下的互斥锁能够提供更好地性能,饥饿模式的能避免 Goroutine 由于陷入等待无法获取锁而造成的高尾延时。

以上规则是由1.9更新。

使用Go 1.8 循环了10次进行测试。

package main

import (
	"sync"
	"time"
)

func main() {
	done := make(chan bool, 1)
	var mu sync.Mutex

	// goroutine 1
	go func() {
		for {
			select {
			case <-done:
				return
			default:
				mu.Lock()
				time.Sleep(100 * time.Microsecond)
				mu.Unlock()
			}
		}
	}()

	// goroutine2
	for i := 0; i < 10; i++ {
		time.Sleep(100 * time.Microsecond)
		mu.Lock()
		mu.Unlock()
	}

	done <- true
}

--摘自:《Go语言设计与实现》

7、GMP模型

参考大佬的文章:

GM到GMP,Golang经历了什么? - 知乎

m 为什么要引入协程
z进程、线程和协程的不同
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值