Go快速上手:goroutines与Channels使用

79 阅读5分钟

Go语言并发编程的深度探索:goroutines与Channels的奇妙世界

在编程的浩瀚宇宙中,Go语言以其简洁的语法、强大的并发模型以及高效的性能,在众多编程语言中脱颖而出。Go语言的并发编程模型,尤其是goroutines和Channels的设计,为开发者提供了一种既直观又高效的并发编程方式。今天,我们就来深入探索Go语言的并发编程精髓,揭开goroutines与Channels的神秘面纱。

一、Go语言并发编程基础

1.1 并发与并行的区别

在讨论Go的并发编程之前,我们需要先澄清两个概念:并发(Concurrency)与并行(Parallelism)。并发指的是程序中的多个任务可以在同一时间段内交错执行,而并行则是指这些任务在同一时刻真正的同时执行。Go语言的goroutines和Channels主要解决的是并发问题,但得益于Go的运行时(runtime)和调度器(scheduler),它们也能在多核处理器上实现高效的并行执行。

1.2 Goroutines:轻量级的线程

Goroutines是Go语言的核心特性之一,它们是由Go的运行时管理的轻量级线程。与传统的线程或进程相比,goroutines的创建和切换成本极低,这使得在Go中编写高并发的程序变得异常简单。你只需使用go关键字在函数调用前,就可以轻松地启动一个新的goroutine。

go func() {
    // 这里是goroutine执行的代码
}()

二、Channels:goroutines之间的通信桥梁

2.1 Channels简介

Channels是Go语言中goroutines之间通信的主要机制。你可以将Channels想象成goroutines之间的管道,通过它们可以安全地在不同的goroutines之间传递数据。Channels的使用避免了使用共享内存进行通信时可能出现的竞态条件(race conditions)和数据竞争(data races)问题。

2.2 Channels的基本操作

  • 发送数据:使用<-操作符和channel变量,可以将数据发送到channel中。如果channel已满(对于无缓冲的channel),发送操作将阻塞,直到有goroutine从channel中接收数据。

ch <- value
  • 接收数据:同样使用<-操作符,但位于channel变量的左侧,用于从channel中接收数据。如果channel为空(对于无缓冲的channel),接收操作将阻塞,直到有goroutine向channel中发送数据。

value := <-ch

2.3 缓冲Channels与无缓冲Channels

  • 无缓冲Channels:在无缓冲的Channels中,发送操作会阻塞,直到另一个goroutine准备好接收数据。接收操作也会阻塞,直到有数据到来。

  • 缓冲Channels:你可以为Channels指定一个缓冲区大小,在缓冲区未满时,发送操作不会阻塞。同样,在缓冲区非空时,接收操作也不会阻塞。

ch := make(chan int, 10) // 创建一个缓冲大小为10的int类型channel

三、进阶应用:使用goroutines与Channels解决并发问题

3.1 返回单向接收通道做为函数返回结果

package main

import (
	"fmt"
	"math/rand"
	"time"
)

func demo() <-chan int {
	r := make(chan int)

	go func() {
		time.Sleep(time.Second * 3) // 模拟实际业务
		r <- rand.Intn(100)
	}()

	return r
}

func sum(a, b int) int {
	return a*a + b*b
}

func main() {
	rand.Seed(time.Now().UnixNano()) // Go 1.20之前需要

	a, b := demo(), demo()
	fmt.Println(sum(<-a, <-b))
}

3.2 将单向发送通道类型用做函数实参

package main

import (
	"fmt"
	"math/rand"
	"time"
)

func demo(r chan<- int) {
	time.Sleep(time.Second * 3) // 模拟业务
	r <- rand.Intn(100)
}

func sum(a, b int) int {
	return a*a + b*b
}

func main() {
	rand.Seed(time.Now().UnixNano()) // Go 1.20之前需要

	ra, rb := make(chan int, 1), make(chan int, 1)
	go demo(ra)
	go demo(rb)

	fmt.Println(sum(<-ra, <-rb))
}

3.3 使用通道实现通知

3.3.1 单对单通知

package main

import (
	"crypto/rand"
	"fmt"
	"os"
	"sort"
)

func main() {
	values := make([]byte, 32 * 1024 * 1024)
	if _, err := rand.Read(values); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	done := make(chan struct{}) // 也可以是缓冲的

	// 排序协程
	go func() {
		sort.Slice(values, func(i, j int) bool {
			return values[i] < values[j]
		})
		done <- struct{}{} // 通知排序已完成
	}()

	// 并发地做一些其它事情...

	<- done // 等待通知
	fmt.Println(values[0], values[len(values)-1])
}

3.3.2 单对多通知

package main

import "log"
import "time"

type T = struct{}

func worker(id int, ready <-chan T, done chan<- T) {
	<-ready // 阻塞在此,等待通知
	log.Print("Worker#", id, "开始工作")
	// 模拟一个工作负载。
	time.Sleep(time.Second * time.Duration(id+1))
	log.Print("Worker#", id, "工作完成")
	done <- T{} // 通知主协程(N-to-1)
}

func main() {
	log.SetFlags(0)

	ready, done := make(chan T), make(chan T)
	go worker(0, ready, done)
	go worker(1, ready, done)
	go worker(2, ready, done)

	// 模拟一个初始化过程
	time.Sleep(time.Second * 3 / 2)
	// 单对多通知
	close(ready)
	// 等待被多对单通知
	<-done
	<-done
	<-done
}

3.4 互斥锁

package main

import "fmt"

func main() {
	mutex := make(chan struct{}, 1) // 容量必须为1

	counter := 0
	increase := func() {
		mutex <- struct{}{} // 加锁
		counter++
		<-mutex // 解锁
	}

	increase1000 := func(done chan<- struct{}) {
		for i := 0; i < 1000; i++ {
			increase()
		}
		done <- struct{}{}
	}

	done := make(chan struct{})
	go increase1000(done)
	go increase1000(done)
	<-done; <-done
	fmt.Println(counter) // 2000
}

3.5 信号量

package main

import (
	"log"
	"time"
	"math/rand"
)

type Seat int
type Bar chan Seat

func (bar Bar) ServeCustomer(c int) {
	log.Print("顾客#", c, "进入酒吧")
	seat := <- bar // 需要一个位子来喝酒
	log.Print("++ customer#", c, " drinks at seat#", seat)
	log.Print("++ 顾客#", c, "在第", seat, "个座位开始饮酒")
	time.Sleep(time.Second * time.Duration(2 + rand.Intn(6)))
	log.Print("-- 顾客#", c, "离开了第", seat, "个座位")
	bar <- seat // 释放座位,离开酒吧
}

func main() {
	rand.Seed(time.Now().UnixNano()) // Go 1.20之前需要

	bar24x7 := make(Bar, 10) // 此酒吧有10个座位
	// 摆放10个座位。
	for seatId := 0; seatId < cap(bar24x7); seatId++ {
		bar24x7 <- Seat(seatId) // 均不会阻塞
	}

	for customerId := 0; ; customerId++ {
		time.Sleep(time.Second)
		go bar24x7.ServeCustomer(customerId)
	}
	for {time.Sleep(time.Second)} // 睡眠不属于阻塞状态
}

四、总结

以上就是Go语言的并发编程模型中goroutines跟channel的使用教程。有其他用途可以在评论区留言。