Go 阻塞的实现示例
目录
- 阻塞
- 使用 Channel 实现阻塞
- 使用 WaitGroup 实现阻塞
- 使用 Mutex 和 Conditional Variables 实现阻塞
- mtx.Lock()
- mtx.Unlock()
- 注意事项
- 永久阻塞
- 使用 sync.WaitGroup
- 空 select
- 死循环
- 用 sync.Mutex
- os.Signal
- 从终端发送信号
- 从Go代码内部发送信号
- 使用外部工具或服务管理器
- 空 channel 或者 nil channel
- 总结
阻塞
在Go语言中,阻塞通常指的是一个goroutine(轻量级线程)在等待另一个goroutine完成操作(如I/O操作、channel通信等)时,暂时停止执行的现象。Go语言提供了多种同步和通信机制,可以用于实现阻塞的效果。
使用 Channel 实现阻塞
Channel 是Go语言中的一个核心特性,用于在goroutines之间进行通信。通过channel,你可www.devze.com以实现阻塞等待数据或命令。
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),但更明确地表达了“完成一个任务”的意图,并且在使用上更安全php,因为它不会导致计数变为负数(如果已经到达零,则会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("准备继续~~~") }
这里是一些关键的修改和注意事项:
sync.Cond
的使用需要一个sync.Mutex
作为其底层的互斥锁。在使用cond.Wait()
之前,必须先锁定这个互斥锁。在
cond.Wait()
调用中,当前的互斥锁会被自动释放,goroutine 会阻塞直到它被cond.Signal()
或cond.Broadcast()
唤醒。一旦
cond.Wait()
返回,goroutine 会重新获取互斥锁,然后继续执行循环或代码块。在
cond.Signal()
调用之后,您需要在某个地方调用mtx.Unlock()
来释放互斥锁,否则主 goroutine 会在cond.Wait()
之后无法获取到锁。您的代码中,
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 是默认的终止进android程信号,通常由服务管理器(如systemd、supervisor等)发送来请求程序正常终止。 //syscall.SIGpythonINT 是中断信号,一般由用户按下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等服务管理器控制,这些管理js器通常提供了发送信号给托管服务的机制。具体操作需参考相应服务管理器的文档。
空 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
了。
到此这篇关于Go 阻塞的实现示例的文章就介绍到这了,更多相关Go 阻塞内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!
精彩评论