go语言常见问题

本文详细解析了Go语言中的Mutex锁实现原理,利用sync.Mutex和sync.WaitGroup管理并发,并介绍了使用channel进行协程间通信的方法,包括阻塞与非阻塞模式。还讨论了内存泄漏情况和如何优雅退出协程,以及Context在控制协程行为中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. go的锁如何实现,用了什么cpu指令

第一种:mutex

var num int
var mtx sync.Mutex
var wg sync.WaitGroup

func add(){
	mtx.Lock()

	defer mtx.Unlock()
	defer wg.Done()
   num+=1
}

func main(){
	for i:= 0; i < 100; i++{
		wg.Add(1)
		go add()
	}
	wg.Wait()
}

第二种 使用chan实现

var num int
func  add(h chan int, wg *sync.WaitGroup){
	defer wg.Done()
	h <- 1
	num += 1
	<-h
}
func main(){
	ch := make(chan int,1)
	wg := &synv.WaitGroup{}
    for i:=0;i < 100; i++{
		wg.Add(1)
		go add(ch,wg)
	}

	wg.Wait()
	fmt.Println("num",num)
}

2. waitGroup简介

add 增加技术
done:减少一个计数,等价于 add(-1)
wait:卡住,直到计数等于0

每个goroutine启动前,增加一个计数,每一个goroutine结束前,减少计数。
func wgTest(wg *sync.WaitGroup){
//计数减一
defer wg.Done()
}

func main(){
wg := sync.WaitGroup{}
for i:= 0; i < 10; i++{
wg.Add(1)
go wgTest(&wg)
}
//计数为0时,结束
wg.Wait()
}

3.channel

channel用于协程之间的通信,让协程之间可以相互交换数据。像管道一样,一个goroutine_A像channel_A中放数据,另一个goroutine_B从channel_B中取数据。

channel是指针类型的数据类型,通过make来分配内存
ch := make(chan int)

<-ch  //从ch中读取一个值
val := <-ch  //从ch中读取一个值并保存到val变量中
val,ok = <-ch //从ch中读取一个值,判断是否读取成功,如果读取成功,则保存到val变量中

channel的3种操作

  1. send:表示sender端的goroutine向channel中投放数据
  2. receive:表示receiver端的goroutine从channel中读取数据
  3. close:表示关闭channel,关闭channel后,send操作将导致panic;recv操作将返回对应类型的0值以及一个状态码false

channel的两种类型
1.阻塞、同步模式:
sender端向channel中发送一个数据,然后阻塞,知道receiver端将此数据接收
receiver端一直阻塞,知道sender端向channel发送一个数据

2.非阻塞,异步模式:
sender端向channel中send多个数据,容量满之前不会阻塞
receiver端按照队列的方式从buffered channel中按序receive其中数据

3.两个属性
capcity: 表示最多可以缓冲多少个数据make(chan TYPE,CAP)
length:表示当前已经缓冲了多少数据

unbuffered channel可以认为是容量是0的buffered channel,所以每发送一个数据就被阻塞。注意不是容量为1的buffered channel,容量为1的channel,是在channel中已有一个数据,发送第二个数据的时候才被阻塞。

  1. 死锁
    当channel的某一端(sender/receiver)期待另一端的(receiver/sender)操作,另一端正好在期待本端的操作时,也就是说两端都因为对方而使得自己当前处于阻塞状态,这时将会出现死锁问题。
    通俗说,只要所有goroutine都被阻塞,就会出现死锁。
func main(){
	goo(32)
}
func go(s int){
	counter := make(chan int)
	counter <- s
	fmt.Println(<-counter)
}

channel的send和recv操作都是在同一个goroutine中进行的,send会阻塞main goroutine,使得recv操作无法被执行,所以go会报错

修复此问题,只需要将send操作放在另一个goroutine中执行即可:

func main(){
	goo(32)
}
func go(s int){
	counter := make(chan int)
	go func(){
		counter <- s
	}
	fmt.Println(<-counter)
}
或者将counter设置为一个容量为1的buffered channel
counter := make(chan int,1)
这样放完一个数据后 send不会阻塞(被recv之前放第二个数据才会阻塞),可以执行到recv操作
  1. unbuffered channel同步通信示例
// wg用于等待程序执行完成
var wg sync.WaitGroup

func main() {
	count := make(chan int)

	// 增加两个待等待的goroutines
	wg.Add(2)
	fmt.Println("Start Goroutines")

	// 激活一个goroutine,label:"Goroutine-1"
	go printCounts("Goroutine-1", count)
	// 激活另一个goroutine,label:"Goroutine-2"
	go printCounts("Goroutine-2", count)

	fmt.Println("Communication of channel begins")
	// 向channel中发送初始数据
	count <- 1

	// 等待goroutines都执行完成
	fmt.Println("Waiting To Finish")
	wg.Wait()
	fmt.Println("\nTerminating the Program")
}
func printCounts(label string, count chan int) {
	// goroutine执行完成时,wg的计数器减1
	defer wg.Done()
	for {
		// 从channel中接收数据
		// 如果无数据可recv,则goroutine阻塞在此
		val, ok := <-count
		if !ok {
			fmt.Println("Channel was closed:",label)
			return
		}
		fmt.Printf("Count: %d received from %s \n", val, label)
		if val == 10 {
			fmt.Printf("Channel Closed from %s \n", label)
			// Close the channel
			close(count)
			return
		}
		// 输出接收到的数据后,加1,并重新将其send到channel中
		val++
		count <- val
	}
}

上面的程序,激活了两个goroutine,激活这两个goroutine后,向channel中发送一个初始数据值1,然后main goroutine将因为wg.Wait等待两个goroutine都执行完而被阻塞。
在看这两个goroutine,执行完全一样的代码。都接收count这个channel的数据,但可能goroutine1先接收到channel中的初始值1,也可能是goroutine2先接收到初始值1.接收到数据后输出值,并在输出后对数据加1,然后将加1后的数据再次send到channel,每次send都会将自己这个goroutine阻塞,此时,另一个goroutine因为等待recv而执行。当加1后发送给channel的数据为10之后,某goroutine将关闭count channel,该goroutine将退出。wg的计数器-1,另一个goroutine因为等待recv而阻塞的状态将因为channel的关闭而失败,ok状态码将让该goroutine退出,于是wg的计数器减为0,main goroutine因为go.Wait()而继续执行后面的代码。

5.go的runtime如何实现

goland的程序都是在runtime的基础上运行的,除了与底层直接交互的syscall
runtime.h中有很多数据结构和接口:
1.G,代表的是goroutine,开启一个goroutine实际就是实例化一个G
2.M,代表Machine. M中存放go程序和机器CPU交互的数据结构
一个go程序都附带一个runtime,负责与底层操作系统做交互
go程序的启动流程:
1.调用osinit,操作系统级别的初始化
2.调用runtime.schedinit
3.调用runtime.mstart启动M
4.调用runtime.main

5.go的数据库连接池实现

6.ctx包有什么用?

1.每一个长请求都应该有个超时限制
2.需要在调用中传递这个超时,通过channel来通知请求结束了。
3.优雅解决goroutine启动后不可控问题。

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	go watch(ctx,"【监控1】")
	go watch(ctx,"【监控2】")
	go watch(ctx,"【监控3】")

	time.Sleep(10 * time.Second)
	fmt.Println("可以了,通知监控停止")
	cancel()
	//为了检测监控过是否停止,如果没有监控输出,就表示停止了
	time.Sleep(5 * time.Second)
}

func watch(ctx context.Context, name string) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println(name,"监控退出,停止了...")
			return
		default:
			fmt.Println(name,"goroutine监控中...")
			time.Sleep(2 * time.Second)
		}
	}
}
示例中启动了3个监控goroutine进行不断的监控,每一个都使用了Context进行跟踪,当我们使用cancel函数通知取消时,这3个goroutine都会被结束。这就是Context的控制能力,它就像一个控制器一样,按下开关后,所有基于这个Context或者衍生的子Context都会收到通知,这时就可以进行清理操作了,最终释放goroutine,这就优雅的解决了goroutine启动后不可控的问题。

7. go什么情况下会发生内存泄漏?

ctx没有cancel的时候

8. 怎么实现协程完美退出?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

叫我峰兄

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值