首页
技术小册
AIGC
面试刷题
技术文章
MAGENTO
云计算
视频课程
源码下载
PDF书籍
「涨薪秘籍」
登录
注册
01|前世今生:你不得不了解的Go的历史和现状
02|拒绝“Hello and Bye”:Go语言的设计哲学是怎么一回事?
03|配好环境:选择一种最适合你的Go安装方法
04|初窥门径:一个Go程序的结构是怎样的?
05|标准先行:Go项目的布局标准是什么?
06|构建模式:Go是怎么解决包依赖管理问题的?
07|构建模式:Go Module的6类常规操作
08|入口函数与包初始化:搞清Go程序的执行次序
09|即学即练:构建一个Web服务就是这么简单
10|变量声明:静态语言有别于动态语言的重要特征
11|代码块与作用域:如何保证变量不会被遮蔽?
12|基本数据类型:Go原生支持的数值类型有哪些?
13|基本数据类型:为什么Go要原生支持字符串类型?
14|常量:Go在“常量”设计上的创新有哪些?
15|同构复合类型:从定长数组到变长切片
16|复合数据类型:原生map类型的实现机制是怎样的?
17|复合数据类型:用结构体建立对真实世界的抽象
18|控制结构:if的“快乐路径”原则
19|控制结构:Go的for循环,仅此一种
20|控制结构:Go中的switch语句有哪些变化?
21|函数:请叫我“一等公民”
22|函数:怎么结合多返回值进行错误处理?
23|函数:怎么让函数更简洁健壮?
24|方法:理解“方法”的本质
25|方法:方法集合与如何选择receiver类型?
26|方法:如何用类型嵌入模拟实现“继承”?
27|即学即练:跟踪函数调用链,理解代码更直观
28|接口:接口即契约
29|接口:为什么nil接口不等于nil?
30|接口:Go中最强大的魔法
31|并发:Go的并发方案实现方案是怎样的?
32|并发:聊聊Goroutine调度器的原理
33|并发:小channel中蕴含大智慧
34|并发:如何使用共享变量?
35|即学即练:如何实现一个轻量级线程池?
36|打稳根基:怎么实现一个TCP服务器?(上)
37|代码操练:怎么实现一个TCP服务器?(中)
38|成果优化:怎么实现一个TCP服务器?(下)
39 | 驯服泛型:了解类型参数
40|驯服泛型:定义泛型约束
41 | 驯服泛型:明确使用时机
当前位置:
首页>>
技术小册>>
Go语言入门实战经典
小册名称:Go语言入门实战经典
### 31 | 并发:Go的并发方案实现方案是怎样的? 在Go语言的世界里,并发编程不仅仅是一种选择,而是其设计哲学的重要组成部分。Go通过独特的goroutine和channel机制,为开发者提供了一种既简洁又高效的并发编程模型,极大地降低了并发编程的复杂性和错误率。本章将深入探讨Go语言的并发实现方案,包括goroutine的基本概念、channel的通信机制、以及Go提供的同步原语和并发控制工具,旨在帮助读者全面理解和掌握Go的并发编程模型。 #### 31.1 Goroutine:轻量级的线程 ##### 31.1.1 Goroutine简介 Goroutine是Go语言的核心特性之一,它提供了一种比传统线程更轻量、更高效的方式来执行并发任务。与线程相比,goroutine的创建成本极低(大约2KB),并且由Go运行时(runtime)自动管理其调度和上下文切换,这使得成千上万的goroutine能够同时运行在有限的操作系统线程之上,而不会导致过高的资源消耗或频繁的上下文切换开销。 ##### 31.1.2 创建Goroutine 在Go中,使用`go`关键字即可创建一个新的goroutine来执行一个函数。例如: ```go func sayHello(name string) { fmt.Printf("Hello, %s!\n", name) } func main() { go sayHello("World") // 注意:main函数中的代码会继续执行,不会等待sayHello完成 time.Sleep(time.Second) // 为了让main函数等待足够的时间以观察输出 } ``` 上述代码中,`go sayHello("World")`语句会启动一个新的goroutine来执行`sayHello`函数。需要注意的是,由于`main`函数中的代码会继续执行,而不会等待`sayHello`完成,因此可能需要某种形式的等待(如`time.Sleep`)来确保有足够的时间观察输出。 ##### 31.1.3 Goroutine的调度与生命周期 Go的运行时负责调度goroutine,它使用M:P:G模型(Machine:Processor:Goroutine)来管理goroutine的执行。简单来说,多个goroutine(G)被分配到多个处理器(P)上,而处理器则绑定到操作系统线程(M)上。这种模型使得Go能够高效地在多核CPU上运行大量的goroutine,而无需为每个goroutine都创建一个新的线程。 Goroutine的生命周期从创建开始,到执行完成或遇到panic结束。Go运行时会自动回收已经结束的goroutine占用的资源。 #### 31.2 Channel:goroutine间的通信 ##### 31.2.1 Channel简介 Channel是Go语言提供的一种特殊的类型,用于在不同的goroutine之间进行通信。它允许一个goroutine发送值到channel,并且由另一个goroutine从channel接收值。这种通信方式避免了直接使用共享内存进行通信时可能引入的竞态条件和数据不一致问题。 ##### 31.2.2 创建和使用Channel 创建channel的语法如下: ```go ch := make(chan int) // 创建一个传递int类型值的channel ``` 向channel发送和接收值分别使用`<-`操作符的左侧和右侧,例如: ```go ch <- 10 // 向channel发送值10 val := <-ch // 从channel接收值,并赋值给val ``` ##### 31.2.3 Channel的阻塞与非阻塞操作 默认情况下,向一个没有接收者准备的channel发送值会导致发送操作阻塞,直到有接收者准备好接收。同样,从一个没有发送者准备的channel接收值也会导致接收操作阻塞,直到有值被发送到channel。但是,Go提供了非阻塞的发送和接收操作,使用`select`语句或者带缓冲的channel可以实现。 带缓冲的channel允许在阻塞发生之前存储一定数量的值,创建方式如下: ```go ch := make(chan int, 2) // 创建一个带缓冲的channel,容量为2 ``` ##### 31.2.4 关闭Channel 当不再需要向channel发送更多值时,应该关闭channel。这可以通过内置的`close`函数实现。关闭后的channel仍然可以接收值,但不能再发送值。尝试向已关闭的channel发送值会导致panic。 ```go close(ch) ``` ##### 31.2.5 Range与Channel 在Go中,可以使用`range`关键字来遍历channel中的值,直到channel被关闭。这使得从channel接收值并处理变得非常简单。 ```go for val := range ch { // 处理val } ``` #### 31.3 同步原语与并发控制 ##### 31.3.1 sync包 Go标准库中的`sync`包提供了一系列用于并发编程的同步原语,包括互斥锁(Mutex)、读写锁(RWMutex)、等待组(WaitGroup)、条件变量(Cond)等。这些工具可以帮助开发者更好地控制goroutine之间的同步和协作。 - **互斥锁(Mutex)**:用于保护共享资源,确保同一时间只有一个goroutine能够访问该资源。 - **读写锁(RWMutex)**:是互斥锁的一个优化版本,允许多个goroutine同时读取共享资源,但写操作仍然是互斥的。 - **等待组(WaitGroup)**:用于等待一组goroutine的完成。通过调用`Add`方法来增加等待的goroutine数量,每个goroutine完成后调用`Done`方法减少计数,直到计数为0时,`Wait`方法才会返回,表示所有goroutine都已完成。 - **条件变量(Cond)**:用于在多个goroutine之间协调,当满足某个条件时唤醒等待的goroutine。 ##### 31.3.2 Context包 除了`sync`包外,Go还提供了`context`包,用于在goroutine之间传递取消信号、超时时间等上下文信息。这对于控制长时间运行的操作(如HTTP请求、数据库查询等)的取消和超时非常有用。 Context的使用通常遵循以下模式: 1. 在函数开始时创建一个新的Context。 2. 将Context作为第一个参数传递给需要它的函数。 3. 在goroutine中,定期检查Context的Done通道,以判断是否应该停止当前操作。 4. 使用`context.WithCancel`、`context.WithTimeout`等函数来创建具有特定取消策略或超时时间的Context。 #### 31.4 实战案例分析 为了更好地理解Go的并发实现方案,我们通过一个简单的实战案例来展示goroutine、channel以及sync包的使用。 假设我们需要实现一个并发下载多个文件的功能,每个文件的下载都在一个独立的goroutine中执行,并且我们需要等待所有文件下载完成后才能继续后续操作。 ```go package main import ( "fmt" "net/http" "os" "sync" ) func downloadFile(url string, wg *sync.WaitGroup, filename string) { defer wg.Done() // 标记goroutine完成 resp, err := http.Get(url) if err != nil { fmt.Println("Error downloading:", err) return } defer resp.Body.Close() // 假设这里简化了文件保存的逻辑 outFile, err := os.Create(filename) if err != nil { fmt.Println("Error creating file:", err) return } defer outFile.Close() // ... 这里可以添加文件写入的逻辑 } func main() { var wg sync.WaitGroup urls := []string{"http://example.com/file1.zip", "http://example.com/file2.zip"} filenames := []string{"file1.zip", "file2.zip"} for i, url := range urls { wg.Add(1) go func(url, filename string) { downloadFile(url, &wg, filename) }(url, filenames[i]) // 注意闭包中的变量捕获 } wg.Wait() // 等待所有goroutine完成 fmt.Println("All files downloaded successfully.") } ``` 在这个例子中,我们创建了一个`sync.WaitGroup`来等待所有下载任务的完成。对于每个下载任务,我们都启动了一个新的goroutine,并在goroutine内部调用`downloadFile`函数进行实际的下载操作。下载函数执行完毕后,通过调用`wg.Done()`来标记该goroutine已完成。最后,在`main`函数中调用`wg.Wait()`来等待所有goroutine的完成。 通过以上分析,我们可以看到Go语言通过goroutine和channel提供了强大而灵活的并发编程能力,使得开发者能够轻松编写出高效、可伸缩的并发程序。同时,通过`sync`包和`context`包等工具,Go还提供了丰富的同步原语和上下文管理机制,帮助开发者更好地控制goroutine之间的协作和同步。
上一篇:
30|接口:Go中最强大的魔法
下一篇:
32|并发:聊聊Goroutine调度器的原理
该分类下的相关小册推荐:
深入浅出Go语言核心编程(八)
go编程权威指南(一)
Go开发权威指南(上)
Go Web编程(中)
Golang修炼指南
Go 组件设计与实现
Go语言从入门到实战
go编程权威指南(四)
Go-Web编程实战
go编程权威指南(三)
深入浅出Go语言核心编程(二)
Go开发基础入门