1。运行每个示例:不要只阅读代码。输入它,运行它,然后观察其行为。⚠️ 这个系列如何进行?
2。实验和打破常规: 删除睡眠并看看会发生什么,更改通道缓冲区大小,修改 goroutine 计数。
打破东西会教你它们是如何工作的
3。关于行为的原因: 在运行修改后的代码之前,尝试预测结果。当您看到意外行为时,请停下来思考原因。挑战解释。
4。建立心理模型:每个可视化代表一个概念。尝试为修改后的代码绘制自己的图表。
这是“掌握 Go 并发”系列的 第 1 部分,我们将介绍:
我们将从基础知识开始,逐步发展如何有效使用它们的直觉。
这会有点长,相当长,所以做好准备。
我们将亲力亲为完成整个过程。
让我们从一个下载多个文件的简单程序开始。
package main import ( "fmt" "time" ) func downloadFile(filename string) { fmt.Printf("Starting download: %s\n", filename) // Simulate file download with sleep time.Sleep(2 * time.Second) fmt.Printf("Finished download: %s\n", filename) } func main() { fmt.Println("Starting downloads...") startTime := time.Now() downloadFile("file1.txt") downloadFile("file2.txt") downloadFile("file3.txt") elapsedTime := time.Since(startTime) fmt.Printf("All downloads completed! Time elapsed: %s\n", elapsedTime) }
该程序总共需要 6 秒,因为每个 2 秒的下载必须在下一个开始之前完成。让我们想象一下:
我们可以缩短这个时间,让我们修改我们的程序以使用go例程:
注意:函数调用前使用 go 关键字
package main import ( "fmt" "time" ) func downloadFile(filename string) { fmt.Printf("Starting download: %s\n", filename) // Simulate file download with sleep time.Sleep(2 * time.Second) fmt.Printf("Finished download: %s\n", filename) } func main() { fmt.Println("Starting downloads...") // Launch downloads concurrently go downloadFile("file1.txt") go downloadFile("file2.txt") go downloadFile("file3.txt") fmt.Println("All downloads completed!") }
等等什么?没有打印任何内容?为什么?
让我们想象一下这一点,以了解可能发生的情况。
从上面的可视化中,我们了解到 main 函数在 goroutine 完成之前就存在了。一项观察结果是,所有 goroutine 的生命周期都依赖于 main 函数。
注意:main函数本身就是一个goroutine;)
为了解决这个问题,我们需要一种方法让主 goroutine 等待其他 goroutine 完成。有几种方法可以做到这一点:
让我们等待几秒钟让 go 例程完成。
package main import ( "fmt" "time" ) func downloadFile(filename string) { fmt.Printf("Starting download: %s\n", filename) // Simulate file download with sleep time.Sleep(2 * time.Second) fmt.Printf("Finished download: %s\n", filename) } func main() { fmt.Println("Starting downloads...") startTime := time.Now() downloadFile("file1.txt") downloadFile("file2.txt") downloadFile("file3.txt") elapsedTime := time.Since(startTime) fmt.Printf("All downloads completed! Time elapsed: %s\n", elapsedTime) }
问题是,我们可能不知道 goroutine 可能需要多长时间。在这种情况下,我们每个人都有固定的时间,但在实际场景中,我们知道下载时间会有所不同。
Go中的sync.WaitGroup是一种并发控制机制,用于等待一组goroutines执行完成。
在这里让我们看看它的实际效果并可视化:
package main import ( "fmt" "time" ) func downloadFile(filename string) { fmt.Printf("Starting download: %s\n", filename) // Simulate file download with sleep time.Sleep(2 * time.Second) fmt.Printf("Finished download: %s\n", filename) } func main() { fmt.Println("Starting downloads...") // Launch downloads concurrently go downloadFile("file1.txt") go downloadFile("file2.txt") go downloadFile("file3.txt") fmt.Println("All downloads completed!") }
让我们可视化这一点并了解sync.WaitGroup 的工作原理:
计数器机制:
同步流程:
要避免的常见陷阱
package main
import (
"fmt"
"time"
)
func downloadFile(filename string) {
fmt.Printf("Starting download: %s\n", filename)
// Simulate file download with sleep
time.Sleep(2 * time.Second)
fmt.Printf("Finished download: %s\n", filename)
}
func main() {
fmt.Println("Starting downloads...")
startTime := time.Now() // Record start time
go downloadFile("file1.txt")
go downloadFile("file2.txt")
go downloadFile("file3.txt")
// Wait for goroutines to finish
time.Sleep(3 * time.Second)
elapsedTime := time.Since(startTime)
fmt.Printf("All downloads completed! Time elapsed: %s\n", elapsedTime)
}
这样我们就很好地理解了 goroutine 是如何工作的。不,两个 Go 例程如何通信?这就是频道发挥作用的地方。
Go 中的Channels 是一个强大的并发原语,用于 goroutine 之间的通信。它们为 goroutine 提供了一种安全共享数据的方法。
将通道视为管道:一个 goroutine 可以将数据发送到通道,另一个 goroutine 可以接收数据。
以下是一些属性:
package main import ( "fmt" "time" ) func downloadFile(filename string) { fmt.Printf("Starting download: %s\n", filename) // Simulate file download with sleep time.Sleep(2 * time.Second) fmt.Printf("Finished download: %s\n", filename) } func main() { fmt.Println("Starting downloads...") startTime := time.Now() downloadFile("file1.txt") downloadFile("file2.txt") downloadFile("file3.txt") elapsedTime := time.Since(startTime) fmt.Printf("All downloads completed! Time elapsed: %s\n", elapsedTime) }
为什么 ch
让我们通过添加 goroutine 来解决这个问题
package main import ( "fmt" "time" ) func downloadFile(filename string) { fmt.Printf("Starting download: %s\n", filename) // Simulate file download with sleep time.Sleep(2 * time.Second) fmt.Printf("Finished download: %s\n", filename) } func main() { fmt.Println("Starting downloads...") // Launch downloads concurrently go downloadFile("file1.txt") go downloadFile("file2.txt") go downloadFile("file3.txt") fmt.Println("All downloads completed!") }
让我们想象一下:
这次消息从不同的 Goroutine 发送,因此主 Goroutine 在发送到通道时不会被阻塞,因此它会移动到 msg :=
现在让我们使用channel来修复文件下载器问题(main不等待其他人完成)。
package main import ( "fmt" "time" ) func downloadFile(filename string) { fmt.Printf("Starting download: %s\n", filename) // Simulate file download with sleep time.Sleep(2 * time.Second) fmt.Printf("Finished download: %s\n", filename) } func main() { fmt.Println("Starting downloads...") startTime := time.Now() // Record start time go downloadFile("file1.txt") go downloadFile("file2.txt") go downloadFile("file3.txt") // Wait for goroutines to finish time.Sleep(3 * time.Second) elapsedTime := time.Since(startTime) fmt.Printf("All downloads completed! Time elapsed: %s\n", elapsedTime) }
可视化它:
让我们进行一次演练以更好地理解:
节目开始:
主协程创建完成通道
启动三个下载 goroutine
每个 goroutine 都会获得对同一通道的引用
下载执行:
频道循环:
循环行为:
完成顺序并不重要!
观察:
⭐ 每次发送(完成 ⭐ 主协程通过循环协调一切
我们已经了解了两个 goroutine 如何进行通信。什么时候?一直以来。 我们不要忘记 main 函数也是一个 goroutine。
package main import ( "fmt" "time" ) func downloadFile(filename string) { fmt.Printf("Starting download: %s\n", filename) // Simulate file download with sleep time.Sleep(2 * time.Second) fmt.Printf("Finished download: %s\n", filename) } func main() { fmt.Println("Starting downloads...") startTime := time.Now() downloadFile("file1.txt") downloadFile("file2.txt") downloadFile("file3.txt") elapsedTime := time.Since(startTime) fmt.Printf("All downloads completed! Time elapsed: %s\n", elapsedTime) }
让我们想象一下并试运行一下:
试运行:
程序开始(t=0ms)
第一条消息(t=1ms)
第二条消息(t=101ms)
第三条消息(t=201ms)
通道关闭(t=301ms)
完成(t=302-303ms)
为什么我们需要缓冲通道?
无缓冲的通道会阻塞发送方和接收方,直到另一方准备好为止。当需要高频通信时,无缓冲的通道可能会成为瓶颈,因为两个 goroutine 必须暂停来交换数据。
缓冲通道属性:
我们看到它的实际效果:
package main import ( "fmt" "time" ) func downloadFile(filename string) { fmt.Printf("Starting download: %s\n", filename) // Simulate file download with sleep time.Sleep(2 * time.Second) fmt.Printf("Finished download: %s\n", filename) } func main() { fmt.Println("Starting downloads...") startTime := time.Now() downloadFile("file1.txt") downloadFile("file2.txt") downloadFile("file3.txt") elapsedTime := time.Since(startTime) fmt.Printf("All downloads completed! Time elapsed: %s\n", elapsedTime) }
输出(在取消注释 ch
为什么它没有阻塞主协程?
缓冲通道允许发送至其容量而不阻塞发送者。
通道的容量为 2,这意味着它在阻塞之前可以在缓冲区中保存两个值。
缓冲区已经满了“第一”和“第二”。由于没有并发接收者来使用这些值,因此发送操作会无限期地阻塞。
因为主 goroutine 也负责发送,并且没有其他活动的 goroutine 从通道接收值,所以程序在尝试发送第三条消息时陷入死锁。
取消注释第三条消息会导致死锁,因为容量现在已满,第三条消息将阻塞,直到缓冲区释放。
Aspect | Buffered Channels | Unbuffered Channels |
---|---|---|
Purpose | For decoupling sender and receiver timing. | For immediate synchronization between sender and receiver. |
When to Use | - When the sender can proceed without waiting for receiver. | - When sender and receiver must synchronize directly. |
- When buffering improves performance or throughput. | - When you want to enforce message-handling immediately. | |
Blocking Behavior | Blocks only when buffer is full. | Sender blocks until receiver is ready, and vice versa. |
Performance | Can improve performance by reducing synchronization. | May introduce latency due to synchronization. |
Example Use Cases | - Logging with rate-limited processing. | - Simple signaling between goroutines. |
- Batch processing where messages are queued temporarily. | - Hand-off of data without delay or buffering. | |
Complexity | Requires careful buffer size tuning to avoid overflows. | Simpler to use; no tuning needed. |
Overhead | Higher memory usage due to the buffer. | Lower memory usage; no buffer involved. |
Concurrency Pattern | Asynchronous communication between sender and receiver. | Synchronous communication; tight coupling. |
Error-Prone Scenarios | Deadlocks if buffer size is mismanaged. | Deadlocks if no goroutine is ready to receive or send. |
使用 缓冲 通道如果:
使用 无缓冲 通道,如果:
这些基础知识为更高级的概念奠定了基础。在我们即将发布的帖子中,我们将探讨:
下一篇文章:
请继续关注我们,我们将继续加深对 Go 强大并发功能的理解!
以上是通过直观的视觉效果了解 Golang 中的 Goroutines 和 Channel的详细内容。更多信息请关注PHP中文网其他相关文章!