【go】并发编程 Goroutine、Channel、Select、Mutex锁、sync、Atomic等

107 阅读7分钟

参考网址

juejin.cn/post/724024…

感谢大佬分享

1/串行、并发、并行

1.1串行

所有任务一件一件做,按照事先排好的顺序依次执行,没有被执行到的任务只能等待wait。
最终执行完所用的总时间等于各个子任务的时间之和。

图片.png

1.2并发

是以交替的方式利用某个任务的I/O`等待`时间来处理其他任务计算逻辑。
在计算机中,例如一个单核CPU,会通过`时间片`算法,来高效合理的分配cpu计算资源。
从用户角度来看似乎是多个任务在同时执行(其实不是的),用户对此是无感知的。

利用等待数据的时间,去执行其他的任务。

图片.png

1.3并行

在同一时刻处理计算多个任务。
以多核CPU为例,可以实现同时处理计算多个任务,
一个CPU负责一个任务的计算逻辑,大家做到同时进行,就像三个任务有三个工人同时干活一样。

图片.png

2/进程、线程、协程

2.1进程

代码程序是死的,但是一旦执行起来就是一个进程。如果没有执行,就是一段程序。
是程序运行的基本单位,每个进程都有自己的独立内存空间,不同的进程可以通过`进程之间的互相通信`进行交流。
比如:电脑上的 QQ、微信、WPS等都有各自的进程。
在操作系统级别来看,进程是`操作系统`对一个正在运行的程序的一种`抽象`。
一个系统里面可以同时运行多个进程,而每一个进程又好像是在独占的使用硬件资源,通过处理器在进程之间不停的切换来实现。

2.2线程

线程是`处理器(CPU)资源分配和调度的基本单位`。
在一个进程中可以有多个线程,每个线程都运行在进程的环境上下文中,不同线程之间可以通过`线程之间通信`进行数据交换。
比如:在360安全卫士进程中,你可以同时进行`垃圾清理``病毒查杀`,
在微信中,你可以`刷朋友圈`同时`接收消息`

2.3进程和线程的区别

- 创建和开销方面,进程的创建需要系统分配内存和CPU,文件句柄等资源,销毁时也要进行相应的回收,所以进程的管理开销很大;但是线程的管理开销则很小。
- 进程之间不会相互影响;而一个线程崩溃可能会导致进程崩溃,从而影响同个进程里面的其他线程。
- 线程是进程的子任务,是处理器(CPU)分配和调度的基本单位,进程是对运行时程序的封装,是系统进行资源分配和调度的基本单元。

2.4协程

在理解协程之前,需要明白线程的几个问题:

-   1、在执行过程中分为用户态和内核态,两个状态的切换会造成资源开销;
-   2、面线程创建的越多,CPU切换的就越频繁,因为操作系统的调度要保证相对公平
-   3、线程的创建、销毁都需要调用系统调用,每次请求都创建,高并发下开销就显得很大,而且线程的数量不能太多,占用内存是 MB 级别。
-   基于上面的问题,协程被提出,协程是用户态(用户空间)的一种抽象,对操作系统内核而言并没有这个概念,依然是以线程维度调度。协程的主要思想是在用户态实现调度算法,来达到用少量线程,处理大量任务的调度,因为是用户态调度切换,不涉及内核切换和不同线程之间的上下文切换,大大减少开销。

2.5最后用一张图来表示3者之间的关系

图片.png

3/并发核心goroutine

3.1goroutine介绍

在Go中使用goroutine来实现·并发·,
在Go中协程的概念最终落地到goroutine中,可以称为Go协程、协程Coroutine等,其他语言中也有相应的协程
goroutine是由Go的运行时(runtime)调度和管理的,Go程序会将 goroutine 中的任务合理地分配给每个CPU。

在Go并发编程中无需关注:进程、线程、协程的概念,无需写创建和销毁的代码,只需要关注goroutine即可。
而且goroutine的使用相当简单,Go在语言层面提供`go`关键字去开启一个goroutine
。例如你想让函数 `fun1` 使用goroutine执行:go func1()

3.2 使用goroutine

为一个函数创建一个goroutine,只需要在调用函数的时候在前面加上`go`关键字即可。
func func1() {
    fmt.Println("Hello F1!")
}

func main() {
    go func1()
    fmt.Println("main goroutine done!")
    // 这里睡一会,防止main结束后,func1 来不及运行
    time.Sleep(time.Second)
}
两次运行结果可能不一样,如下所示:
PS D:\dev\go\workspace\go_demo_code> go run .\temp\t1.go
Hello F1!
main done!
PS D:\dev\go\workspace\go_demo_code> go run .\temp\t1.go
main done!
Hello F1!
go关键字可以用于匿名函数,并且goroutine实现多个并发非常简单,如下启动是个goroutine:
func main() {
	for i := 0; i < 10; i++ {
		go func(n int) {
			fmt.Println("执行了:", n)
		}(i)
	}
	fmt.Println("main done!")
	// 这里睡一会,防止main结束后,func1 来不及运行
	time.Sleep(time.Second)
}
运行结果如下所示:
PS D:\dev\go\workspace\go_demo_code> go run .\temp\t1.go
main done!
执行了: 4
执行了: 5
执行了: 1
执行了: 0
执行了: 7
执行了: 2
执行了: 3
执行了: 8
执行了: 9
执行了: 6

3.3 goroutine资源

在第一章中知道,线程是由操作系统内核进行调度的,涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,调度成本和开销比较大,
而goroutine是由`go runtime`进行管理调度,大量的goroutine映射到少量的线程中去,其调度和切换更加轻量,基本都在用户态完成。一个goroutine的栈只有`几K`大小,非常轻量,轻松是实现`10W`级别并发支持。

4/数据交换channel

5/多路复用-Select

第三章我们知道了可以通过channel进行多个goroutine的数据交换。
在使用通道时,如果没有数据接收会阻塞,处理监听多个通道,就无法通过一个goroutine很好的接收数据。
这种情况go提供了`select`,多路复用,可以同时监听多个channel,使用如下:
   select {
     case c1 := <- ch1:
        fmt.Println("c1=", c1)
     case c2 := <- ch1:
        fmt.Println("c2=", c2)
}
 如何解读上述代码:
-   select语句是专为通道而设计的,所以每个case表达式中都只能包含操作通道的表达式

-   select 默认阻塞,只有监听的channel中有发送或者接受数据时才运行

-   设置default则不阻塞,通道内没有待接受的数据则执行default

-   多个channel准备好时,会随机选一个执行

6/并发控制-Sync

多个goroutine并发运行时,难免需要对并发程序进行控制,如第五章的锁控制并发安全访问,灾还有一些常见的情况:
1.  一个goroutine,需要等待多个goroutine完成任务再执行业务代码。
1.  某些特定代码在并发场景下只希望被执行一次。
1.  多个等待中的goroutine,接收到一个goroutine通知后,开始处理一些业务(如果单纯使用 chan 或互斥锁,那么只能有一个协程可以等待,并读取到数据)
1.  go默认的map是并发不安全的,实际开发中我们需要一些并发类的容器,例如map等

sync包提供了一些开箱即用的api和对象,帮助我们控制并发goroutine,解决实际需求。

6.1 sync.WaitGroup

WaitGroup类似Java的CountDownLatch,可以实现等待并发任务执行完, 
sync.WaitGroup有以下几个方法:
    -   Add(delta int) 计数器+delta
    -   Done() 计数器-1
    -   Wait() 阻塞直到计数器变为0
var num int

//var lock sync.Mutex

var rwlock sync.RWMutex
//定义一个WaitGroup
var wg sync.WaitGroup

func main() {
    // 三个 goroutine,这里直接加三
	wg.Add(3)
	go add()
	go add()
	go read()
    // 等待所有goroutine任务结束
	wg.Wait()
	fmt.Println(num)
}

func read() {
	// 测试效果读取5次
	for i := 0; i < 5; i++ {
		// 加读锁
		rwlock.RLock()
		fmt.Println("读取:", num)
		// 释放读锁
		rwlock.RUnlock()
		// 让出CPU执行时间
		runtime.Gosched()
	}
    // 结束一个减一
	wg.Done()
}

func add() {
	for i := 0; i < 10000; i++ {
		// 加写锁
		rwlock.Lock()
		num++
		// 释放写锁
		rwlock.Unlock()
	}
	wg.Done()
}