Go 并发编程 - Goroutine 基础 (一)

饭宝  金牌会员 | 2023-8-31 18:23:40 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 897|帖子 897|积分 2691

基础概念

进程与线程

进程是一次程序在操作系统执行的过程,需要消耗一定的CPU、时间、内存、IO等。每个进程都拥有着独立的内存空间和系统资源。进程之间的内存是不共享的。通常需要使用 IPC 机制进行数据传输。进程是直接挂在操作系统上运行的,是操作系统分配硬件资源的最小单位。
线程是进程的一个执行实体,一个进程可以包含若干个线程。线程共享进程的内存空间和系统资源。线程是 CPU 调度的最小单位。因为线程之间是共享内存的,所以它的创建、切换、销毁会比进程所消耗的系统资源更少。
举一个形象的例子:一个操作系统就相当于一支师级编制的军队作战,一个师会把手上的作战资源独立的分配各个团。而一个团级的编制就相当于一个进程,团级内部会根据具体的作战需求编制出若干的营连级,营连级会共享这些作战资源,它就相当于是计算机中的线程。
什么是协程

协程是一种更轻量的线程,被称为用户态线程。它不是由操作系统分配的,对于操作系统来说,协程是透明的。协程是程序员根据具体的业务需求创立出来的。一个线程可以跑多个协程。协程之间切换不会阻塞线程,而且非常的轻量,更容易实现并发程序。Golang 中使用 goroutine 来实现协程。
并发与并行

可以用一句话来形容:多线程程序运行在单核CPU上,称为并发;多线程程序运行在多核CPU上,称为并行。
Goroutine

Golang 中开启协程的语法非常简单,使用 Go 关键词即可:
  1. func main() {
  2.         go hello()
  3.         fmt.Println("主线程结束")
  4. }
  5. func hello() {
  6.         fmt.Println("hello world")
  7. }
  8. // 结果
  9. 主线程结束
复制代码
程序打印结果并非是我们想象的先打印出 “hello world”,再打印出 “主线程结束”。这是因为协程是异步执行的,当协程还没有来得及打印,主线程就已经结束了。我们只需要在主线程中暂停一秒就可以打印出想要的结果。
  1. func main() {
  2.         go hello()
  3.         time.Sleep(1 * time.Second) // 暂停一秒
  4.         fmt.Println("主线程结束")
  5. }
  6. // 结果
  7. hello world
  8. 主线程结束
复制代码
这里的一次程序执行其实是执行了一个进程,只不过这个进程就只有一个线程。
在 Golang 中开启一个协程是非常方便的,但我们要知道并发编程充满了复杂性与危险性,需要小心翼翼的使用,以防出现了不可预料的问题。
编写一个简单的并发程序

用来检测各个站点是否能响应:
  1. func main() {
  2.         start := time.Now()
  3.         apis := []string{
  4.                 "https://management.azure.com",
  5.                 "https://dev.azure.com",
  6.                 "https://api.github.com",
  7.                 "https://outlook.office.com/",
  8.                 "https://api.somewhereintheinternet.com/",
  9.                 "https://graph.microsoft.com",
  10.         }
  11.         for _, api := range apis {
  12.                 _, err := http.Get(api)
  13.                 if err != nil {
  14.                         fmt.Printf("响应错误: %s\n", api)
  15.                         continue
  16.                 }
  17.                 fmt.Printf("成功响应: %s\n", api)
  18.         }
  19.         elapsed := time.Since(start) // 用来记录当前进程运行所消耗的时间
  20.         fmt.Printf("主线程运行结束,消耗 %v 秒!\n", elapsed.Seconds())
  21. }
  22. // 结果
  23. 成功响应: https://management.azure.com
  24. 成功响应: https://dev.azure.com
  25. 成功响应: https://api.github.com
  26. 成功响应: https://outlook.office.com/
  27. 响应错误: https://api.somewhereintheinternet.com/
  28. 成功响应: https://graph.microsoft.com
  29. 主线程运行结束,消耗 5.4122892 秒!
复制代码
我们检测六个站点一个消耗了5秒的时间,假设现在需要对一百个站点进行检测,那么这个过程就会耗费大量的时间,这些时间都被消耗到了 http.Get(api) 这里。
在 http.get(api) 还没有获取到结果时,主线程会等待请求的响应,会阻塞在这里。这时候我们就可以使用协程来优化这段代码,将各个网络请求的检测变成异步执行,从而减少程序响应的总时间。
  1. func main() {
  2.         ...
  3.         for _, api := range apis {
  4.                 go checkApi(api)
  5.         }
  6.         time.Sleep(3 * time.Second)  // 等待三秒,不然主线程会瞬间结束,导致协程被杀死
  7.         ...
  8. }
  9. func checkApi(api string) {
  10.         _, err := http.Get(api)
  11.         if err != nil {
  12.                 fmt.Printf("响应错误: %s\n", api)
  13.                 return
  14.         }
  15.         fmt.Printf("成功响应: %s\n", api)
  16. }
  17. // 结果
  18. 响应错误: https://api.somewhereintheinternet.com/
  19. 成功响应: https://api.github.com
  20. 成功响应: https://graph.microsoft.com
  21. 成功响应: https://management.azure.com
  22. 成功响应: https://dev.azure.com
  23. 成功响应: https://outlook.office.com/
  24. 主线程运行结束,消耗 3.0013905 秒!
复制代码
可以看到,使用 goroutine 后,除去等待的三秒钟,程序的响应时间产生了质的变化。但美中不足的是,我们只能在原地傻傻的等待三秒。那么有没有一种方法可以感知协程的运行状态,当监听到协程运行结束时再优雅的关闭主线程呢?
sync.waitgroup

sync.waitgroup 可以完成我们的”优雅小目标“。 sync.waitgroup 是 goroutine 的一个“计数工具”,通常用来等待一组 goroutine 的执行完成。当我们需要监听协程是否运行完成就可以使用该工具。sync.waitgroup 提供了三种方法:

  • Add(n int):添加 n 个goroutine 到 WaitGroup 中,表示需要等待 n 个 goroutine 执行完成。
  • Done():每个 goroutine 执行完成时调用 Done 方法,表示该 goroutine 已完成执行,相当于把计数器 -1。
  • Wait():主线程调用 Wait 方法来等待所有 goroutine 执行完成,会阻塞到所有的 goroutine 执行完成。
我们来使用 sync.waitgroup 来优雅的结束程序:
  1. package main
  2. import (
  3.         "fmt"
  4.         "net/http"
  5.         "sync"
  6.         "time"
  7. )
  8. func main() {
  9.         var (
  10.                 start = time.Now()
  11.                 apis  = []string{
  12.                         "https://management.azure.com",
  13.                         "https://dev.azure.com",
  14.                         "https://api.github.com",
  15.                         "https://outlook.office.com/",
  16.                         "https://api.somewhereintheinternet.com/",
  17.                         "https://graph.microsoft.com",
  18.                 }
  19.                 wg = sync.WaitGroup{} // 初始化WaitGroup
  20.         )
  21.         wg.Add(len(apis)) // 表示需要等待六个协程请求
  22.         for _, api := range apis {
  23.                 go checkApi(api, &wg)
  24.         }
  25.         wg.Wait()                    // 阻塞主线程,等待 WaitGroup 归零后再继续
  26.         elapsed := time.Since(start) // 用来记录当前进程运行所消耗的时间
  27.         fmt.Printf("线程运行结束,消耗 %v 秒!\n", elapsed.Seconds())
  28. }
  29. func checkApi(api string, wg *sync.WaitGroup) {
  30.         defer wg.Done() // 标记当前协程执行完成,计数器-1
  31.         _, err := http.Get(api)
  32.         if err != nil {
  33.                 fmt.Printf("响应错误: %s\n", api)
  34.                 return
  35.         }
  36.         fmt.Printf("成功响应: %s\n", api)
  37. }
  38. // 结果
  39. 响应错误: https://api.somewhereintheinternet.com/
  40. 成功响应: https://api.github.com
  41. 成功响应: https://management.azure.com
  42. 成功响应: https://graph.microsoft.com
  43. 成功响应: https://dev.azure.com
  44. 成功响应: https://outlook.office.com/
  45. 线程运行结束,消耗 0.9718695 秒!
复制代码
可以看到,我们优雅了监听了所有协程是否执行完毕,且大幅度缩短了程序运行时间。但同时我们的打印响应信息也是无序的了,这代表了我们的协程确确实实异步的请求了所有的站点。
Channel

channel 也可以完成我们的”优雅小目标“。 channel 的中文名字被称为“通道”,是 goroutine 的通信机制。当需要将值从一个 goroutine 发送到另一个时,可以使用通道。Golang 的并发理念是:“通过通信共享内存,而不是通过共享内存通信”。channel 是并发编程中的一个重要概念,遵循着数据先进先出,后进后出的原则。
声明 Channel 

声明通道需要使用内置的 make() 函数:
  1. ch := make(chan <type>) // type 代表数据类型,如 string、int
复制代码
Channel 发送数据和接收数据


创建好 channle 后可以使用
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

饭宝

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表