39、Go语言基础 - 并发编程 - goroutine(协程)

作者: 温新

分类: 【Go基础】

阅读: 283

时间: 2023-12-05 00:33:27

hi,我是温新

并发编程基础

并发与并行

并发:同一时间段内执行多个任务;

并行:同一时刻执行多个任务。

下面通过案例来解释:

并行(Parallelism)

在并行的情况下,银行拥有多个独立的柜台,每个柜台由一个银行职员处理一个客户的事务。这意味着多个事务可以同时进行,因为每个柜台都有自己的处理能力。客户1可以在柜台1办理业务,同时客户2可以在柜台2办理业务,这两个事务是真正同时执行的,不互相干扰。

这是真正的并行处理,就像在多核 CPU 上运行多个任务一样。并行处理提高了整体的吞吐量,因为多个事务可以同时进行。

并发(Concurrency): 在并发的情况下,银行只有一个柜台,但有多个客户排队等待办理业务。柜台的银行职员在不同的客户之间快速切换,每个客户都得到一些时间来处理他们的业务,然后移到下一个客户。这是因为柜台的银行职员在不同的客户之间交替工作,以最大限度地减少等待时间。

虽然柜台只有一个,但多个客户的业务在时间上交替执行,这就是并发。并发处理通常用于提高资源利用率,因为在任何给定时刻只有一个客户被服务,但它们可以共享柜台和银行职员的资源

总结:

  • 并行是多个任务真正同时执行,通常需要多个处理单元。
  • 并发是多个任务在时间上交替执行,通过快速切换来模拟同时执行的效果,可以在单核或多核环境中使用。

进程、线和、协程

  1. 进程(Process)
    • 进程是操作系统中的一个独立执行单元,拥有自己的内存空间、系统资源和运行环境。
    • 每个进程都有独立的地址空间,相互隔离,通常需要通过进程间通信(IPC)来进行数据交换。
    • 进程通常用于执行独立的程序,如应用程序或服务,每个进程都有自己的进程号(PID)。
    • 进程切换需要较多的系统开销,通常较为重量级。
  2. 线程(Thread)
    • 线程是进程内的执行单元,共享相同的内存空间和资源。
    • 线程可以看作是轻量级的进程,多个线程可以并发执行,它们之间可以更容易地共享数据。
    • 线程通常用于实现多线程编程,提高程序的并发性能,但需要注意处理线程间的同步和互斥问题。
    • 线程通常属于同一进程,因此不需要像进程之间那样的IPC。
  3. 协程(Goroutine 或 Coroutine)
    • 协程是一种轻量级的线程,它可以在用户空间实现并发,而不需要像线程那样由操作系统进行管理。
    • 协程可以被认为是用户级线程,由编程语言或库来管理,而不受操作系统的直接控制。
    • 协程之间的切换通常非常高效,因为它们不需要进行完整的上下文切换。
    • 协程通常用于编写高效且可伸缩的并发代码,例如在 Go 语言中的 Goroutine。

总结:

  • 进程是操作系统中的独立执行单位,线程是进程内的执行单位,而协程是更轻量级的执行单位。
  • 进程间通信需要IPC,线程共享进程内的内存,协程通常由编程语言或库来管理。
  • 进程和线程通常由操作系统管理,协程是用户级的并发机制,更容易实现高效的并发编程。

goroutine(协程)

goroutine 是 Go 语言并行设计的核心。goruntine 其实就是协程,它比线程更小、更轻量。

执行 goroutine 只需要极光的内存(大概 4~5KB),它会根据相应的数据进行伸缩。也正因如此,程序可以同时运行成千上万个并发任务。

goruntine 是通过 Go 程序的 runtime 管理的一个线程管理器。goruntine 通过 go 关键字实现。

goroutine 是 Go 程序中最基本的并发执行单元。每一个 Go 程序都至少包含一个 goroutine——main goroutine,当 Go 程序启动时它会自动创建。

当我们在编写某个并发任务时,只需要把这个任务包装成一个函数,然后开启一个 goruntine 去执行这个函数,就可以了。

go 关键字

Go 中使用 goruntine 只需要在函数或方法前加上 go 关键字就可以创建一个 goruntine,从而让该函数或方法在新创建的 goruntine 中执行。其方式如下:

go

// 使用 go 关键字,在调用方法前加上即可
go f()

匿名函数

go func() {
   //  
}()

一个 goruntine 必须对应一个函数或方法,可以创建多个 goruntine 去执行相同的函数或方法。

基本案例

go 案例

package main

import "fmt"

func main() {
	go say()
	fmt.Println("学习 Go goruntine")
}

func say() {
	fmt.Println("go goruntine")
}

匿名函数案例

package main

import "fmt"

func main() {
	go func() {
		fmt.Println("go goruntine")
	}()
	fmt.Println("学习 Go goruntine")
}
学习 Go goruntine

Process finished with the exit code 0

在这两个案例中,可能会出现 go goruntine 没有被打印出来的情况,这是为什么?

Go 程序在启动时,就会为 main 函数创建一个默认的 goruntine。在这两个案例中,我们是在 main 函数中使用了 go 关键字创建新的 goruntine 去执行 say 函数,而此时 main goruntine 还在往下执行,此时程序中存在 2 个并发执行的 goruntine。

当 main 函数结束时, 整个程序也就结束了,同时 main goruntine 也结束了,所以由 main goruntine 创建的新的 goruntine 也会一起退出。也就是说新创建的 goruntine 还没有执行,程序就已经退出了。所以 go goruntine也就没有打印出来了。

如何才能让新创建的 goruntine 执行?得想办法让 main goruntine 等一等。如下案例让程序等 1 秒,也就得到了我们想要的输出了。

go func() {
    fmt.Println("go goruntine")
}()
fmt.Println("学习 Go goruntine")
// 等 1 秒
time.Sleep(time.Second)

go goruntine学习 Go goruntine 谁先被打印?这个没有办法确定。

sync 包

上面的案例中,我们是通过time.Sleep(time.Second)强制程序等待 1 秒,这个做法并不准确。最好的做法是得告诉主程序 goruntine 程序是否执行完毕。Go 语言提供了 sync 包来实现这一想法。

package main

import (
	"fmt"
	"sync"
)

// 全局等待组
var wg sync.WaitGroup

func main() {
	// 标记有 1 个 goruntine 任务
	wg.Add(1)
	go say()
	fmt.Println("学习 Go 协程")

	// 阻塞等待登记的 goruntine 完成
	wg.Wait()
}

func say() {
	fmt.Println("Go goruntine")

	// 告诉当前 goruntine 代码执行完毕
	wg.Done()
}

多个 goruntine

上面的案例都是一个一个执行,怎么多个执行?

在 Go 语言中,实现并发很简单,启动多个 goruntine 即可,如下案例:

package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup

func main() {
	for i := 0; i < 10000; i++ {
		// 创建一个 goruntine 就登记 +1
		wg.Add(1)
		go Say(i)
	}
	wg.Wait()
}

func Say(i int) {
	// goruntine 结束时 -1
	defer wg.Done()
	fmt.Println("学习 Go goruntine, ", i)
}

输出结果如下:

...
学习 Go goruntine,  9932
学习 Go goruntine,  9993
学习 Go goruntine,  9768
学习 Go goruntine,  9962
学习 Go goruntine,  9935

每次输出的结果是不一样的,这是因为 10000 个 goruntine 是并发执行的,而 goruntine 的调度是随机的。

GOMAXPROCS

GOMAXPROCS 是一个用于设置 Go 程序并发执行的最大 CPU 核心数的环境变量。下面是一个简单的 Go 代码示例,演示如何使用 GOMAXPROCS 来设置最大 CPU 核心数:

package main

import (
	"fmt"
	"runtime"
	"sync"
)

func main() {
	// 获取当前系统的 CPU 核心数
	numCPU := runtime.NumCPU()
	fmt.Printf("Current CPU cores: %d\n", numCPU)

	// 设置最大 CPU 核心数为2
	runtime.GOMAXPROCS(2)

	// 获取已设置的 CPU 核心数
	maxProcs := runtime.GOMAXPROCS(0)
	fmt.Printf("Max CPU cores: %d\n", maxProcs)

	// 创建一个等待组,用于等待 Goroutine 完成
	var wg sync.WaitGroup

	// 启动多个 Goroutine
	for i := 1; i <= 4; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			fmt.Printf("Goroutine %d is using CPU core %d\n", id, runtime.NumCPU())
		}(i)
	}

	// 等待所有 Goroutine 完成
	wg.Wait()
}

在这个示例中,我们首先使用 runtime.NumCPU() 获取当前系统的 CPU 核心数,然后使用 runtime.GOMAXPROCS() 来设置最大 CPU 核心数为2。接着,我们使用 runtime.GOMAXPROCS(0) 获取已设置的 CPU 核心数。

随后,我们创建一个等待组(sync.WaitGroup)来等待多个 Goroutine 完成。我们启动了4个 Goroutine,每个 Goroutine 打印自己正在使用的 CPU 核心编号。

运行这个程序,你会看到输出类似以下内容:

Current CPU cores: 16
Max CPU cores: 2
Goroutine 4 is using CPU core 16
Goroutine 1 is using CPU core 16
Goroutine 2 is using CPU core 16
Goroutine 3 is using CPU core 16

这个示例演示了如何使用 GOMAXPROCS 来设置最大 CPU 核心数,以及如何在多核 CPU 上并行执行 Goroutine。在这种情况下,我们将最大 CPU 核心数设置为2,因此 Goroutine 在两个核心上并行执行。

请登录后再评论