Goroutines学习笔记 | 青训营

58 阅读4分钟

当谈到Go语言中的并发编程,Goroutines是一个不可或缺的概念。Goroutines在Go语言中的地位与重要性,犹如线程在其他编程语言中的地位一样。但是,与传统的线程相比,Goroutines更加轻量级、高效,并且更易于管理。在本部分,我们将深入探讨Goroutines的各个方面,包括创建、同步、通信、调度等。

1. 创建Goroutines

创建一个Goroutine非常简单,只需要在函数调用前添加关键字go。这样,函数将以Goroutine的方式运行,而不会阻塞主程序的执行。

func main() {
    go func() {
        fmt.Println("Hello from Goroutine!")
    }()
    
    fmt.Println("Hello from main function!")
    
    // 防止程序过早退出
    time.Sleep(time.Second)
}

在上面的示例中,我们在一个匿名函数前添加了go关键字,这使得该函数在一个单独的Goroutine中执行。因此,你会看到两个输出分别来自主函数和Goroutine,它们可能会交叉输出,因为Goroutines的执行顺序是不确定的。

2. 并发通信:Channel

在多个Goroutines之间进行通信是并发编程的核心。Goroutines之间不能直接共享变量,因此我们需要一种方式来安全地传递数据。这就是Channel的作用。

func main() {
    ch := make(chan int) // 创建一个整数类型的Channel

    go func() {
        ch <- 42 // 发送数据到Channel
    }()

    result := <-ch // 从Channel接收数据
    fmt.Println(result)
}

在这个例子中,我们创建了一个整数类型的Channel,然后在一个Goroutine中向Channel发送了数据42。接着,在主函数中,我们从Channel接收到这个值并打印出来。Channel会自动处理同步,保证发送和接收的顺序是正确的。

3. 多个Goroutine的同步

在许多情况下,我们需要等待多个Goroutines完成后再进行下一步操作。这就需要使用sync.WaitGroup来实现同步。

func main() {
    var wg sync.WaitGroup
    
    for i := 0; i < 5; i++ {
        wg.Add(1) // 增加等待组计数
        
        go func(i int) {
            defer wg.Done() // 减少等待组计数
            fmt.Println("Goroutine", i, "is done.")
        }(i)
    }
    
    wg.Wait() // 等待所有Goroutines完成
    fmt.Println("All Goroutines are done.")
}

在上面的示例中,我们使用sync.WaitGroup来跟踪一组Goroutines的完成状态。通过Add方法增加计数,通过在Goroutine内部的Done方法减少计数。最后,通过调用Wait方法,主函数会阻塞等待所有的Goroutines完成。

4. 并发安全:互斥锁

当多个Goroutines同时访问共享资源时,可能会引发竞争条件,导致数据不一致。互斥锁(Mutex)是一种保护共享资源的机制,只有一个Goroutine可以获取锁,从而防止其他Goroutines同时访问。

func main() {
    var mu sync.Mutex
    var count int

    for i := 0; i < 1000; i++ {
        go func() {
            mu.Lock()   // 获取锁
            defer mu.Unlock() // 释放锁
            count++
        }()
    }

    time.Sleep(time.Second)
    fmt.Println("Count:", count)
}

在上述代码中,我们使用互斥锁来保护共享变量count,以确保每次只有一个Goroutine可以修改它。在每个Goroutine中,我们通过Lock方法获取锁,在操作完成后通过Unlock方法释放锁。

5. 通过Select进行多路复用

select语句是Go语言中处理多个Channel的关键机制之一。它允许我们同时等待多个Channel上的操作,并在其中一个操作准备就绪时执行相应的代码。

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)

    go func() {
        ch1 <- 42
    }()
    
    go func() {
        ch2 <- 100
    }()
    
    select {
    case val := <-ch1:
        fmt.Println("Received from ch1:", val)
    case val := <-ch2:
        fmt.Println("Received from ch2:", val)
    }
}

在这个示例中,我们创建了两个Goroutines,分别向不同的Channel发送数据。然后,通过select语句等待两个Channel中的任意一个操作完成,一旦有一个操作准备就绪,就会执行相应的代码块。

6. 基于Context的取消

在某些情况下,我们可能需要在外部触发的条件下取消正在执行的Goroutines。这可以通过context包来实现,它提供了一种在Goroutines之间传递上下文信息的方式,也可以用于取消Goroutines的执行。

func worker(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("Worker", id, "cancelled.")
            return
        default:
            fmt.Println("Worker", id, "is working.")
            time.Sleep(time.Second)
        }
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    
    for i := 0; i < 3; i++ {
        go worker(ctx, i)
    }
    
    time.Sleep(3 * time.Second)
    cancel() // 取消所有Goroutines
    time.Sleep(time.Second)
}

在这个示例中,我们

创建了一个可以被取消的上下文(Context),然后通过context.WithCancel函数获得一个取消函数。在每个Goroutine中,我们使用select语句监听取消操作,一旦收到取消信号,就会终止Goroutine的执行。