Golang 协程与通道

前言

在 Go 语言中,
协程被称为 goroutines。goroutine 是 Go 的并发执行单元,它比传统的线程更轻量级,允许你以非常低的开销启动成千上万个并发任务
通道(channel)是一种特殊的类型,用于在不同的 goroutines 之间进行通信和同步。通道可以被想象成一个传递数据的管道,它可以帮助确保并发程序的数据同步,避免竞态条件

协程

goroutine 在使用上非常简单,只需要在函数调用前加上关键字 go。当 go 关键字用于调用一个函数时,该函数会在一个新的 goroutine 中异步执行

package main

import (
	"fmt"
	"time"
)

// 这是一个将在goroutine中运行的函数
func sayHello() {
	fmt.Println("Hello, World!")
}

func main() {
	// 启动一个goroutine
	go sayHello()

	// 主goroutine需要等待一段时间,否则程序可能在 sayHello goroutine运行之前就退出了
	time.Sleep(1 * time.Second)
	fmt.Println("Main function finished")
}

sayHello 函数在一个新的 goroutine 中被调用。主 main 函数在调用 sayHello 后会继续执行,几乎立即执行 time.Sleep 来等待一秒钟,确保有足够的时间让 sayHello goroutine 执行。如果没有 time.Sleep,程序可能会在 sayHello goroutine 还没来得及运行时结束,因为主函数(也是一个 goroutine)结束时会关闭所有的其他 goroutines

注意:在实际程序中,我们通常不会使用 time.Sleep 来等待一个 goroutine 完成,因为这种方法并不可靠。相反,我们通常会使用通道(channels)或其他同步机制,如 WaitGroups,来协调不同 goroutines 之间的执行

通道

通道可以是带缓冲的或不带缓冲的

  • 不带缓冲的通道在发送和接收数据时会阻塞,直到另一端准备好进行接收或发送操作
  • 带缓冲的通道则允许在阻塞之前存储一定数量的值

创建通道:

// 创建一个不带缓冲的通道
ch := make(chan int)

// 创建一个带缓冲的通道,缓冲大小为2
bufferedCh := make(chan int, 2)

向通道发送数据和从通道接收数据:

// 发送数据到通道
ch <- value

// 从通道接收数据,并将值存储在变量中
value := <-ch

// 或者,仅从通道接收数据,不存储值
<-ch

举个使用通道在两个 goroutines 之间发送和接收数据的例子:

package main

import (
	"fmt"
	"time"
)

func sendMessage(ch chan string) {
	// 向通道发送消息
	ch <- "Hello, World!"
}

func main() {
	// 创建一个不带缓冲的通道
	ch := make(chan string)

	// 启动一个goroutine来发送消息
	go sendMessage(ch)

	// 从通道接收消息
	message := <-ch
	fmt.Println(message)
}

在这个例子中,sendMessage goroutine 向 ch 通道发送了一个字符串。主函数中的 <-ch 语句会阻塞,直到有数据可以从通道中接收。一旦 sendMessage goroutine 发送了数据,主函数就会接收到消息并打印出来

通道是实现 Go 并发模型的核心,利用通道可以构建复杂的并发程序,同时保持代码的安全性和清晰性

常用的两种同步机制:通道与WaitGroups

通道介绍过了,再来一个例子:

func worker(done chan bool) {
	fmt.Print("Working...")
	time.Sleep(time.Second)
	fmt.Println("done")

	// 发送信号完成工作
	done <- true
}

func main() {
	done := make(chan bool, 1)
	go worker(done)

	// 等待工作完成
	<-done
}

在这个例子中,main 函数启动了一个 worker goroutine,并传递了一个通道 done。worker 完成工作后,向 done 通道发送了一个信号(一个 true 值)。main 函数通过 <-done 表达式等待这个信号,这确保了 main 函数会等待 worker 完成工作才继续执行

WaitGroup
sync.WaitGroup 是一个等待一组 goroutines 完成的同步机制。主要的方法包括 Add(增加等待的 goroutines 数),Done(goroutine 完成时调用),和 Wait(等待所有 goroutine 完成)
示例:

func worker(id int, wg *sync.WaitGroup) {
	defer wg.Done() // 完成时通知 WaitGroup

	fmt.Printf("Worker %d starting\n", id)
	time.Sleep(time.Second)
	fmt.Printf("Worker %d done\n", id)
}

func main() {
	var wg sync.WaitGroup

	for i := 1; i <= 5; i++ {
		wg.Add(1) // 增加等待的 goroutines 数
		go worker(i, &wg)
	}

	wg.Wait() // 等待所有 goroutine 完成
	fmt.Println("All workers completed")
}

main 函数创建了一个 WaitGroup 并启动了五个 worker goroutines。每个 worker 在开始时通过 defer wg.Done() 确保在函数返回时通知 WaitGroup 完成。main 函数使用 wg.Wait() 来阻塞,直到所有的 worker goroutines 都调用了 Done 方法

死锁或 panic

在使用 sync.WaitGroup 时,如果不正确地管理协程的启动与结束,就可能遇到死锁或 panic 的情况

  1. 死锁 (Deadlock): 这通常发生在 Wait 被调用,但是 WaitGroup 的计数器没有被正确减至0的情况。在这种场合,程序将永远等待,因为没有更多的操作能够减少计数器的值
    例如:
var wg sync.WaitGroup
wg.Add(1)
// 没有启动任何协程来调用 wg.Done()
wg.Wait() // 这里会发生死锁,因为没有任何操作能使计数器变为0
  1. Panic: 如果调用 Done 方法的次数比通过 Add 方法设置的值还要多,程序将会 panic,错误信息大致为 “negative WaitGroup counter” 或类似的信息。这表明你试图减少一个已经为0的计数器
    例如:
var wg sync.WaitGroup
// 没有调用 wg.Add() 来增加计数器
wg.Done() // 这里会导致 panic,因为计数器变成了负数

因而,为了避免这些问题,确保:

  • 使用 wg.Add(n) 时,确保后续有足够的 wg.Done() 调用与之对应
  • 不要在所有协程启动之前调用 wg.Wait()
  • 在使用 wg.Add(1) 后应立即启动协程
  • 在协程内部,只有当实际工作完成后,才调用 wg.Done()

你可能感兴趣的:(golang)