基础概念
进程与线程
进程是一次程序在操作系统执行的过程,需要消耗一定的CPU、时间、内存、IO等。每个进程都拥有着独立的内存空间和系统资源。进程之间的内存是不共享的。通常需要使用 IPC 机制进行数据传输。进程是直接挂在操作系统上运行的,是操作系统分配硬件资源的最小单位。
线程是进程的一个执行实体,一个进程可以包含若干个线程。线程共享进程的内存空间和系统资源。线程是 CPU 调度的最小单位。因为线程之间是共享内存的,所以它的创建、切换、销毁会比进程所消耗的系统资源更少。
举一个形象的例子:一个操作系统就相当于一支师级编制的军队作战,一个师会把手上的作战资源独立的分配各个团。而一个团级的编制就相当于一个进程,团级内部会根据具体的作战需求编制出若干的营连级,营连级会共享这些作战资源,它就相当于是计算机中的线程。
什么是协程
协程是一种更轻量的线程,被称为用户态线程。它不是由操作系统分配的,对于操作系统来说,协程是透明的。协程是程序员根据具体的业务需求创立出来的。一个线程可以跑多个协程。协程之间切换不会阻塞线程,而且非常的轻量,更容易实现并发程序。Golang 中使用 goroutine 来实现协程。
并发与并行
可以用一句话来形容:多线程程序运行在单核CPU上,称为并发;多线程程序运行在多核CPU上,称为并行。
Goroutine
Golang 中开启协程的语法非常简单,使用 Go 关键词即可:- func main() {
- go hello()
- fmt.Println("主线程结束")
- }
- func hello() {
- fmt.Println("hello world")
- }
- // 结果
- 主线程结束
复制代码 程序打印结果并非是我们想象的先打印出 “hello world”,再打印出 “主线程结束”。这是因为协程是异步执行的,当协程还没有来得及打印,主线程就已经结束了。我们只需要在主线程中暂停一秒就可以打印出想要的结果。- func main() {
- go hello()
- time.Sleep(1 * time.Second) // 暂停一秒
- fmt.Println("主线程结束")
- }
- // 结果
- hello world
- 主线程结束
复制代码 这里的一次程序执行其实是执行了一个进程,只不过这个进程就只有一个线程。
在 Golang 中开启一个协程是非常方便的,但我们要知道并发编程充满了复杂性与危险性,需要小心翼翼的使用,以防出现了不可预料的问题。
编写一个简单的并发程序
用来检测各个站点是否能响应:- func main() {
- start := time.Now()
- apis := []string{
- "https://management.azure.com",
- "https://dev.azure.com",
- "https://api.github.com",
- "https://outlook.office.com/",
- "https://api.somewhereintheinternet.com/",
- "https://graph.microsoft.com",
- }
- for _, api := range apis {
- _, err := http.Get(api)
- if err != nil {
- fmt.Printf("响应错误: %s\n", api)
- continue
- }
- fmt.Printf("成功响应: %s\n", api)
- }
- elapsed := time.Since(start) // 用来记录当前进程运行所消耗的时间
- fmt.Printf("主线程运行结束,消耗 %v 秒!\n", elapsed.Seconds())
- }
- // 结果
- 成功响应: https://management.azure.com
- 成功响应: https://dev.azure.com
- 成功响应: https://api.github.com
- 成功响应: https://outlook.office.com/
- 响应错误: https://api.somewhereintheinternet.com/
- 成功响应: https://graph.microsoft.com
- 主线程运行结束,消耗 5.4122892 秒!
复制代码 我们检测六个站点一个消耗了5秒的时间,假设现在需要对一百个站点进行检测,那么这个过程就会耗费大量的时间,这些时间都被消耗到了 http.Get(api) 这里。
在 http.get(api) 还没有获取到结果时,主线程会等待请求的响应,会阻塞在这里。这时候我们就可以使用协程来优化这段代码,将各个网络请求的检测变成异步执行,从而减少程序响应的总时间。- func main() {
- ...
- for _, api := range apis {
- go checkApi(api)
- }
- time.Sleep(3 * time.Second) // 等待三秒,不然主线程会瞬间结束,导致协程被杀死
- ...
- }
- func checkApi(api string) {
- _, err := http.Get(api)
- if err != nil {
- fmt.Printf("响应错误: %s\n", api)
- return
- }
- fmt.Printf("成功响应: %s\n", api)
- }
- // 结果
- 响应错误: https://api.somewhereintheinternet.com/
- 成功响应: https://api.github.com
- 成功响应: https://graph.microsoft.com
- 成功响应: https://management.azure.com
- 成功响应: https://dev.azure.com
- 成功响应: https://outlook.office.com/
- 主线程运行结束,消耗 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 来优雅的结束程序:- package main
- import (
- "fmt"
- "net/http"
- "sync"
- "time"
- )
- func main() {
- var (
- start = time.Now()
- apis = []string{
- "https://management.azure.com",
- "https://dev.azure.com",
- "https://api.github.com",
- "https://outlook.office.com/",
- "https://api.somewhereintheinternet.com/",
- "https://graph.microsoft.com",
- }
- wg = sync.WaitGroup{} // 初始化WaitGroup
- )
- wg.Add(len(apis)) // 表示需要等待六个协程请求
- for _, api := range apis {
- go checkApi(api, &wg)
- }
- wg.Wait() // 阻塞主线程,等待 WaitGroup 归零后再继续
- elapsed := time.Since(start) // 用来记录当前进程运行所消耗的时间
- fmt.Printf("线程运行结束,消耗 %v 秒!\n", elapsed.Seconds())
- }
- func checkApi(api string, wg *sync.WaitGroup) {
- defer wg.Done() // 标记当前协程执行完成,计数器-1
- _, err := http.Get(api)
- if err != nil {
- fmt.Printf("响应错误: %s\n", api)
- return
- }
- fmt.Printf("成功响应: %s\n", api)
- }
- // 结果
- 响应错误: https://api.somewhereintheinternet.com/
- 成功响应: https://api.github.com
- 成功响应: https://management.azure.com
- 成功响应: https://graph.microsoft.com
- 成功响应: https://dev.azure.com
- 成功响应: https://outlook.office.com/
- 线程运行结束,消耗 0.9718695 秒!
复制代码 可以看到,我们优雅了监听了所有协程是否执行完毕,且大幅度缩短了程序运行时间。但同时我们的打印响应信息也是无序的了,这代表了我们的协程确确实实异步的请求了所有的站点。
Channel
channel 也可以完成我们的”优雅小目标“。 channel 的中文名字被称为“通道”,是 goroutine 的通信机制。当需要将值从一个 goroutine 发送到另一个时,可以使用通道。Golang 的并发理念是:“通过通信共享内存,而不是通过共享内存通信”。channel 是并发编程中的一个重要概念,遵循着数据先进先出,后进后出的原则。
声明 Channel
声明通道需要使用内置的 make() 函数:- ch := make(chan <type>) // type 代表数据类型,如 string、int
复制代码 Channel 发送数据和接收数据
创建好 channle 后可以使用 |