Go 并发编程 - Goroutine 基础 (一)
基础概念进程与线程
进程是一次程序在操作系统执行的过程,需要消耗一定的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、intChannel 发送数据和接收数据
创建好 channle 后可以使用
页:
[1]