Go 阻塞

阻塞 

在Go语言中,阻塞通常指的是一个goroutine(轻量级线程)在等待另一个goroutine完成操作(如I/O操作、channel通信等)时,暂时停止执行的现象。Go语言提供了多种同步和通信机制,可以用于实现阻塞的效果。

使用 Channel 实现阻塞

Channel 是Go语言中的一个核心特性,用于在goroutines之间进行通信。通过channel,你可以实现阻塞等待数据或命令。

package main

import (
	"fmt"
	"time"
)

func main() {
	c := make(chan struct{})
	go func() {
		fmt.Println("业务处理~~~")
		time.Sleep(2 * time.Second)
		fmt.Println("业务处理完成~~~")
		close(c) // 关闭channel,通知工作完成
	}()

	<-c // 阻塞等待channel关闭
	fmt.Println("处理其他业务~~~")
}

使用 WaitGroup 实现阻塞

WaitGroup 是Go语言中用于同步一组并发操作的另一个工具。它通过计数器来跟踪完成的操作数量。

package main

import (
	"fmt"
	"strconv"
	"sync"
	"time"
)

func main() {
	var wg sync.WaitGroup //控制并发组

	doWork := func(i int) {
        // wg.Done(): 表示一个事件已经完成。它等价于 wg.Add(-1),但更明确地表达了“完成一个任务”的意图,并且在使用上更安全,因为它不会导致计数变为负数(如果已经到达零,则会panic)。
		defer wg.Done() // 当函数返回时,通知WaitGroup一个操作已完成相当于wg.Add(-1)
		fmt.Println("处理业务~~~" + strconv.Itoa(i))
		time.Sleep(2 * time.Second)
		fmt.Println("业务处理完成~~~" + strconv.Itoa(i))
	}

	for i := 0; i < 5; i++ {
		wg.Add(1)    // 增加WaitGroup的计数器
		go doWork(i) // 启动一个goroutine做工作
	}
	//主goroutine调用wg.Wait(),直到所有启动的goroutines都通过调用wg.Done()通知它们已经完成工作
	wg.Wait() // 阻塞,直到WaitGroup的计数器为0
	fmt.Println("所有业务处理完成~~~")
}

使用 Mutex 和 Conditional Variables 实现阻塞

Mutex(互斥锁)和条件变量可以用来同步访问共享资源,并实现基于条件的阻塞。 

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	var mtx sync.Mutex         //创建互斥锁
	cond := sync.NewCond(&mtx) //使用mtx作为底层互斥锁
	ready := false

	// 启动一个 goroutine 来改变条件变量 ready 的值,并通知 cond。
	go func() {
		fmt.Println("循环跟goroutine是go内部决定先调度的--------------------goroutine--------------------")
		time.Sleep(3 * time.Second)
		mtx.Lock() //使用互斥锁
		ready = true
		cond.Signal() // 唤醒至少一个等待的 goroutine
		mtx.Unlock()  //解锁
	}()

	mtx.Lock() // 锁定互斥锁,准备进入条件等待
	for !ready {
		fmt.Println("循环跟goroutine是go内部决定先调度的--------------------阻塞--------------------")
		cond.Wait() // 阻塞,直到 cond.Signal() 被调用
		//mtx.Unlock()
	}
	mtx.Unlock() // 解锁互斥锁,继续执行(此处mtx.Unlock()在for循环里面阻塞等待完成后也可以,也可以没有,因为主线程会结束,但如果后续还需要获取互斥锁则必须要释放否则报错)

	fmt.Println("准备继续~~~")
}

这里是一些关键的修改和注意事项:

  1. sync.Cond 的使用需要一个 sync.Mutex 作为其底层的互斥锁。在使用 cond.Wait() 之前,必须先锁定这个互斥锁。

  2. cond.Wait() 调用中,当前的互斥锁会被自动释放,goroutine 会阻塞直到它被 cond.Signal()cond.Broadcast() 唤醒。

  3. 一旦 cond.Wait() 返回,goroutine 会重新获取互斥锁,然后继续执行循环或代码块。

  4. cond.Signal() 调用之后,您需要在某个地方调用 mtx.Unlock() 来释放互斥锁,否则主 goroutine 会在 cond.Wait() 之后无法获取到锁。

  5. 您的代码中,cond.Wait() 之后的 mtx.Unlock() 应该在 for 循环之外,以避免在循环的每次迭代中重复加锁和解锁。 

在Go语言中,sync.Mutex(互斥锁)用于保护共享资源不被多个goroutine同时修改,以避免竞态条件。sync.Cond(条件变量)与互斥锁结合使用,可以在多个goroutine之间同步共享条件。以下是关于何时使用 mtx.Lock()mtx.Unlock() 的指导:

mtx.Lock()

  • 在访问或修改由互斥锁保护的共享资源之前使用。
  • 在调用 cond.Wait() 之前使用,以确保在等待条件变量时,共享资源不会被其他goroutine并发访问。
  • 在调用 cond.Signal() 或 cond.Broadcast() 之前使用,因为这些操作需要在互斥锁保护的临界区内执行。

mtx.Unlock()

  • 在完成对共享资源的访问或修改后使用。
  • 在 cond.Wait() 返回后使用,因为我们已经完成了等待期间需要的共享资源访问,并且需要重新获取互斥锁以继续执行。
  • 在不再需要互斥锁保护当前goroutine的执行路径时使用,以允许其他等待互斥锁的goroutine继续执行。

注意事项

  • 互斥锁必须在获取后及时释放,否则会导致死锁。
  • 通常,获取互斥锁和释放互斥锁成对出现,以避免忘记释放锁。

永久阻塞

Go 的运行时的当前设计,假定程序员自己负责检测何时终止一个 goroutine 以及何时终止该程序。可以通过调用 os.Exit 或从 main() 函数的返回来以正常方式终止程序。而有时候我们需要的是使程序阻塞在这一行。

使用 sync.WaitGroup 

一直等待直到 WaitGroup 等于 0 

package main

import "sync"

func main() {
	var wg sync.WaitGroup
	wg.Add(1)
	wg.Wait()
}

空 select

 select{}是一个没有任何 case 的 select,它会一直阻塞

package main

func main() {
	select{}
}

 死循环

虽然能阻塞,但会 100%占用一个 cpu。不建议使用

package main

func main() {
	for {}
}

 用 sync.Mutex

一个已经锁了的锁,再锁一次会一直阻塞,这个不建议使用

package main

import "sync"

func main() {
	var m sync.Mutex
	m.Lock()
}

 os.Signal

系统信号量,在 go 里面也是个 channel,在收到特定的消息之前一直阻塞  

package main

import (
	"os"
	"os/signal"
	"syscall"
)

func main() {
	sig := make(chan os.Signal, 2)
	//syscall.SIGTERM 是默认的终止进程信号,通常由服务管理器(如systemd、supervisor等)发送来请求程序正常终止。
	//syscall.SIGINT 是中断信号,一般由用户按下Ctrl+C键触发,用于请求程序中断执行
	signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT)
	<-sig
}
从终端发送信号
  • Ctrl+C: 在大多数Unix-like系统(包括Linux和macOS)以及Windows的命令行中,按 Ctrl+C 键会向当前前台进程发送一个 SIGINT(中断)信号。这通常是停止Go程序的快捷方式。

  • Kill命令: 如果你的程序在后台运行,并且你知道其进程ID(PID),可以通过终端发送一个信号。例如,发送一个 SIGTERM 信号,可以使用:kill PID或者指定型号类型kill -SIGTERM PID

 从Go代码内部发送信号
package main

import (
	"os"
	"os/signal"
	"syscall"
	"time"
)

func main() {
	sig := make(chan os.Signal, 2)
	//syscall.SIGTERM 是默认的终止进程信号,通常由服务管理器(如systemd、supervisor等)发送来请求程序正常终止。
	//syscall.SIGINT 是中断信号,一般由用户按下Ctrl+C键触发,用于请求程序中断执行
	signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT)

	go func() {
		time.Sleep(10 * time.Second)
		sig <- syscall.SIGTERM
	}()

	go func() {
		time.Sleep(5 * time.Second)
		sig <- syscall.SIGINT
	}()

	<-sig
}
使用外部工具或服务管理器

如果你的Go程序作为服务运行,可能由如systemd、supervisord等服务管理器控制,这些管理器通常提供了发送信号给托管服务的机制。具体操作需参考相应服务管理器的文档。

空 channel 或者 nil channel 

channel 会一直阻塞直到收到消息,nil channel 永远阻塞。 

package main

func main() {
	c := make(chan struct{})
	<-c
}
package main

func main() {
	var c chan struct{} //nil channel
	<-c
}
 总结

 注意上面写的的代码大部分不能直接运行,都会 panic,提示“all goroutines are asleep - deadlock!”,因为 go 的 runtime 会检查你所有的 goroutine 都卡住了, 没有一个要执行。

你可以在阻塞代码前面加上一个或多个你自己业务逻辑的 goroutine,这样就不会 deadlock 了。

 

  • 12
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值