Go 中的并发

并发和并行的区别如下:

  • 并发:逻辑上具备同时处理多个任务的能力。
  • 并行:物理上在同一时刻执行多个并发任务。
    我们通常会说程序是并发设计的,也就是说它允许多个任务同时执行,但实际上并不一定真在同一时刻发生。在单核处理器上,它们能以间隔方式切换执行。而并行则依赖多核处理器等物理设备,让多个任务真正在同一时刻执行,它代表了当前程序运行状态。简单点说,并行是并发设计的理想执行模式。
    多线程或多进程是并行的基本条件,但单线程也可用协程做到并发。尽管协程在单个线程上通过主动切换来实现多任务并发,但它也有自己的优势。除了将因阻塞而浪费的时间找回来外,还免去了线程切换的开销,有着不错的执行效率。协程上运行的多个任务本质上是依旧串行的,加上可控自主调度,所以并不需要做同步处理。
    很难说哪种方式更好一些,它们各有自使用的场景。通常情况下,用多进程来实现分布式和负载平衡,减轻单进程垃圾回收压力;用多线程(LWP)抢夺更多的处理器资源;用协程来提高处理器时间片利用率。
    关键字 go 并非执行并发操作,而是创建一个并发任务单元。新建任务被放置在系统队列中,等待调度器安排合适系统线程去获取执行权。当前流程不会阻塞,不会等待该任务启动,切运行时也不会保证并发任务的执行次序。
    每个任务单元除保存函数指针、调用参数外,还会分配执行所需的栈内存空间。相比系统默认 MB 级别的线程栈, goroutine 自定义栈初始化仅须 2KB,所以才能创建成千上万的并发任务。自定义栈采取按需分配策略,在需要是进行扩容,最大能到 GB 规模。

Wait
进程退出时不会等待并发任务结束,可用通道(channel)阻塞,然后发出退出信号。

package main

import "time"

func main()  {
	exit := make(chan struct{})  // 创建通道,因为仅是通知,数据并没有实际意义
	go func() {
		time.Sleep(time.Second)
		println("go routine done.")
		close(exit)       // 管理通道,发出信号
	}()
	println("main....")
	<-exit               // 如通道管理,立即解除阻塞
	println("main exit.")
}

输出结果为:
main…
go routine done.
main exit.
尽管 WaitGroup.Add 实现了原子操作,但建议在 goroutine 外累加计数器,以免 Add 尚未执行,Wait 已经退出。

package main

import "sync"

func main()  {
	var wg sync.WaitGroup
	go func() {
		wg.Add(1)       // 来不及设置
		println("hi!")
	}()
	wg.Wait()
	println("exit.")

}

输出为:
exit.
hi!
可在多处使用 Wait 阻塞,它们都能接收到通知。

package main

import (
	"sync"
	"time"
)

func main()  {
	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		wg.Wait()
		println("wait exit...")
	}()
	go func() {
		time.Sleep(time.Second)
		println("done..")
		wg.Done()
	}()
	wg.Wait()
	println("main exit...")
}

输出为:
done…
wait exit…
main exit…

GOMAXPROCS
运行时可能会创建很多线程,但任何时候仅有限的几个线程参与并发任务执行。该数量默认与处理器核数相等,可用 runtine.GOMAXPROCS 函数(或环境变量)修改。
如参数小于1,GOMAXPROCS 仅返回当前设置值,不做任何调整。

Local Storage
与线程不同,goroutine 任务无法设置优先级,无法获取编号。没有局部存储(TLS),甚至连返回值都会被抛弃。但除优先级外,其他功能都很容易实现。

package main

import (
	"fmt"
	"sync"
)

func main()  {
	var wg sync.WaitGroup
	var gs [5]struct{         // 用于实现类似 TLS 功能
		id		int           // 编号
		result  int           // 返回值
	}
	for i := 0; i < len(gs); i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			gs[id].id = id
			gs[id].result = (id + 1) * 100
		}(i)
	}
	wg.Wait()
	fmt.Printf("%+v\n", gs)
}

输出为:
[{id:0 result:100} {id:1 result:200} {id:2 result:300} {id:3 result:400} {id:4 result:500}]
如使用 map 做局部存储容器,建议做同步处理,因为运行时会对其做并发读写检查。

Gosched
暂停,释放线程去执行其他任务。当前任务被放回队列,等待下次调度时恢复执行。

package main

import "runtime"

func main()  {
	runtime.GOMAXPROCS(1)
	exit := make(chan struct{})

	go func() {
		defer close(exit)
		go func() {
			println("b")
		}()
		for i := 0; i < 4; i++ {
			println("a:", i)
			if i == 1 {
				runtime.Gosched()
			}
		}
	}()
	<-exit
}

输出结果为:
a: 0
a: 1
b
a: 2
a: 3

Goexit
Goexit 立即终止当前任务,运行时确保所有已注册延迟函数被执行。该函数不会影响其他并发任务,不会引发 panic,自然也就无法捕获。

package main

import "runtime"

func main()  {
	exit := make(chan struct{})

	go func() {
		defer close(exit)        // 执行
		defer println("a")  // 执行

		func() {
			defer func() {
				println("b", recover() == nil)   // 执行, recover 返回 nil
			}()

			func() {                 // 在多层调用中执行 Goexit
				println("c")
				runtime.Goexit()           // 立即终止整个调用堆栈
				println("c done.")  // 不会执行
			}()
			println("b done.")    // 不会执行
		}()
		println("a done.")       // 不会执行
	}()
	<-exit
}

输出为:
c
b true
a
main exit.
如果在 main.main 里调用了 Goexit,它会等待其他任务结束,然后让进城直接崩溃。

package main

import (
	"fmt"
	"runtime"
	"time"
)

func main()  {
	for i := 0; i < 2; i++ {
		go func(x int) {
			for n := 0; n < 2; n++ {
				fmt.Printf("%c: %d\n", 'a' + x, n)
				time.Sleep(time.Millisecond)
			}
		}(i)
	}
	runtime.Goexit()
	println("main exit.")
}

输出:
b: 0
a: 0
a: 1
b: 1
fatal error: no goroutines (main called runtime.Goexit) - deadlock!
无论身处哪一层,Goexit 都能立即终止整个调用堆栈,这与 return 仅退出当前函数不同。标注库函数 osExit 可终止进程,但不会执行延迟调用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值