设为首页 加入收藏

TOP

Go 并发编程 - Goroutine 基础 (一)(一)
2023-08-26 21:10:02 】 浏览:148
Tags: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 提供了三种方法:

  1. Add(n int):添加 n 个goroutine 到 WaitGroup 中,表示需要等待 n 个 goroutine 执行完成。

  2. Done():每个 goroutine 执行完成时调用 Done 方法,表示该 goroutine 已完成执行,相当于把计数器 -1。

  3. Wait():主线程调用 Wait 方法来等待所有 goroutine 执行完成,会阻塞到所有的 goroutine 执行完成。

我们来使用 sync

首页 上一页 1 2 3 4 下一页 尾页 1/4/4
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇番外1.ssh连接管理器 下一篇Fabric区块链浏览器(2)

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目