在 Go 语言中,使用协程来实现并发模型。
协程是 Go 语言的并发执行单元,它比传统的线程更轻量级,允许我们并发执行多个任务。
Go 会在内部使用一组线程来运行创建的协程,并在这些线程之间高效地分配协程执行,这样可以在不增加太多操作系统线程的情况下执行大量的协程。
在golang中,我们可以方便的使用go func() {}()
语句用于启动一个新的协程(goroutine)。
协程是 Go 语言的并发执行单元,它比传统的线程更轻量级,允许我们并发执行多个任务。
当在函数调用前加上 go 关键字时,这个函数就会在新的协程中异步执行。
下面是一个简单的在golang主函数中创建协程的例子:
func printNumbers() {
for i := 1; i <= 5; i++ {
fmt.Println(i)
time.Sleep(time.Second)
}
}
func main() {
// 启动一个新的协程来执行 printNumbers 函数
go printNumbers()
// 主协程中执行其他工作
for i := 'a'; i <= 'e'; i++ {
fmt.Printf("%c", i)
time.Sleep(time.Second)
}
// 等待足够的时间以确保 printNumbers 协程可以完成执行
time.Sleep(6 * time.Second)
fmt.Println("main function finished")
}
我们在新的协程中,启动了一个printNumbers()函数,运行结果如下:a12bc3d4e5
这说明我们成功创建了一个协程,这个协程和主程序并发运行。上述代码也可以写成下面的形式:
func main() {
// 启动一个新的协程来执行 printNumbers 函数
go func(){ for i := 1; i <= 5; i++ {
fmt.Println(i)
time.Sleep(time.Second)
}}()
// 主协程中执行其他工作
for i := 'a'; i <= 'e'; i++ {
fmt.Printf("%c", i)
time.Sleep(time.Second)
}
// 等待足够的时间以确保 printNumbers 协程可以完成执行
time.Sleep(6 * time.Second)
fmt.Println("main function finished")
}
我们再次运行发现,运行结果如下:a1b23c45e
,可以看到虽然代码是一样的,但是打印出字母和数字的顺序却不同。
这是为什么呢?因为我们只开启了一个协程,并没有控制协程和主程序的执行顺序,所以每次运行,有时协程运行的快些,有时主程序运行的快些。
那么如果我们想要打印一个字母,再打印一个数字,要怎么办呢?这就需要用到golang提供的,协程间的通信机制。
在gonglang中,可以使用管道来进行协程间的通信。
通过 make(chan Type)
可以创建一个管道,其中 Type
是管道将传递的数据类型。我们通过通道对上述代码进行改造:
func main() {
ch := make(chan bool) // 用于从 printNumbers 协程到主协程的同步
done := make(chan bool) // 用于从主协程到 printNumbers 协程的同步
// 启动一个新的协程来执行 printNumbers 函数
go func() {
for i := 1; i <= 5; i++ {
fmt.Printf("%d", i)
ch <- true // 发送数字到主协程
<-done // 等待主协程的信号
}
close(ch) // 关闭通道
}()
// 主协程中执行其他工作
for i := 'a'; i <= 'e'; i++ {
<-ch // 等待来自 printNumbers 协程的数字
fmt.Printf("%c", i)
done <- true // 发送信号到 printNumbers 协程
}
fmt.Println("main function finished")
}
在上述代码中,我们通过通道ch
和done
进行进程间的消息传递,主程序通过<-ch
收到ch
通道传来的信号后,才打印出数字。
同样,协程收到来自主程序通道传来的信号后,再继续打印字母。
这样就可以保证打印的顺序始终为1a2b3c4d5
。
在我们的代码中使用了<-ch
,即没有关心通道传来的具体值,如果需要用到通道传递过来的值,可以使用a<-ch
,这样通道传来的值就会被存储在变量a
中。
此外管道可以是无缓冲的,也可以是有缓冲的,我们上述使用的是无缓冲的通道。
无缓冲管道在发送和接收之间是同步的:发送操作会阻塞,直到另一端的协程执行接收操作,而接收操作也会阻塞,直到有数据被发送。这种同步通信机制确保了协程间的同步执行。
有缓冲的管道则允许在协程间异步传输数据。当你创建一个有缓冲的管道时,你需要指定缓冲区的大小。有缓冲的管道可以存储多个元素,直到达到其缓冲区的容量为止。发送操作只有在缓冲区满时才会阻塞,接收操作只有在缓冲区空时才会阻塞。
创建有缓冲的管道的示例:ch := make(chan int, 5)
在这个例子中,ch 是一个可以存储最多 5 个整数的有缓冲管道。这意味着我们可以在没有接收方的情况下向管道发送最多 5 个整数,而不会发生阻塞。
golang中当协程的父程序即创建协程的程序结束时,父程序中的协程会全部终止
在文章最开始的代码中,我们通过使用 time.Sleep(6 * time.Second)
来确保在协程结束之前,主程序不会停止,这其实是非常不好的做法。
实际上,在gonglang中,一般使用WaitGroup
来进行进程间的同步控制,确保在子协程全部结束时,父协程才会终止。
sync.WaitGroup
是 Go 语言中的一个同步原语,用于等待一组协程(goroutines)
的完成。它主要用于确保所有协程都已经完成它们的工作。
在使用sync.WaitGroup
之前我们首先要学习其Add()、Done()和Wait()函数。
wg.Add(delta int)
方法用于设置等待组中的协程数量。每次调用 wg.Add()
时,传递的参数(通常是 1)将会被加到内部计数器上。这个方法通常在启动协程之前调用,以表明有新的协程需要等待。
wg.Done()
方法用于表示一个协程完成了它的工作。每当协程完成它的执行时,它应该调用 wg.Done()
,这将从内部计数器减去 1。
当内部计数器减到 0 时,任何阻塞在 wg.Wait()
调用上的协程都会停止阻塞,继续执行。
通过下面这个函数我们可以确保所有子协程全部完成之后,主程序才会终止:
func main() {
// 启动一个新的协程来执行 printNumbers 函数
var wg sync.WaitGroup
// 等待协程数增加一
wg.Add(1)
go func() {
for i := 1; i <= 5; i++ {
fmt.Println(i)
}
//协程完成等待协程数减少1
wg.Done()
}()
wg.Add(1)
go func() {
for i := 'a'; i <= 'e'; i++ {
fmt.Printf("%c", i)
}
wg.Done()
}()
// 等待所有的协程完成
wg.Wait()
fmt.Println("main function finished")
}
通过这种方法,我们发现,只有当两个子程序都运行完成后,才会打印出main function finished。