携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情
一、并发编程
并发与并行
- 并发:多线程在一个核的 CPU 上运行
- 并行:多线程在多个核的 CPU 上运行
并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。这就好像两个人用同一把铁锨,轮流挖坑,两个人非常快速的使用同一把铁锹交替挖坑,快到就好像一直在挖。
当有多个线程在操作时,如果系统只有一个CPU,根本不可能真正的同时进行一个以上的线程,它只能把 CPU 运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状态.这种方式我们称之为并发(Concurrent)。
并发的本质还是串行。
并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。就好像两个人各拿一把铁锨在挖坑,一小时后,每人一个大坑。所以无论从微观还是从宏观来看,二者都是一起执行的。
进程、线程以及协程
进程:一段程序的执行过程。
- 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
- 程序是指令、数据及其组织形式的描述,而进程是程序的实体。
- 进程是线程的容器。
线程:轻量级进程,是 CPU 的最先调度单位
- 线程是程序的实际执行者,一个进程至少包含一个主线程以及多个子线程
- 多个线程共享资源
- 线程间通信主要高共享内存,上下文切换较快,资源开销少,相比进程不易丢失数据
协程:又称为轻量级线程
- 一个线程包含多个协程,协程有独立的占空间,多个协程贡献堆空间
- 开启一个协程需要 2k 空间,远低于开启一个线程需要的 2M 空间,且可以自己调度
- 协程是用户级线程,由用户自己调度,而线程需要通过 CPU 调度
二、goroutine
在使用 Java 进行编发编程时,通常需要自己维护一个线程池,并且需要自己包装任务以及调度线程执行任务并维护上下文切换。
goroutine 是 Go 语言中协程的实现(轻量级线程),由 Go 运行时(runtime) 调度和管理,Go 程序会将 goroutine 中的任务合理的分配给每个 CPU,这是应为 Go 在语言层面内置了调度和上下文切换的机制。
Go 程序从 main 包中的 main() 函数开始,在程序启动时就会为 main() 函数创建一个默认的 Go 协程(goroutine)
Go 语言编程不需要再自己写进程、线程,当需要某个任务并发执行时,只需要将这个任务包装成一个函数
//noinspection ALL
func main(){
// 开启一个协程
go testCoroutine()
for i := 0; i < 2; i++ {
fmt.Println("main() 函数执行")
time.Sleep(time.Microsecond * 100)
}
}
func testCoroutine(){
for i := 0; i < 10; i++ {
fmt.Println("testCoroutine() 函数执行")
time.Sleep(time.Microsecond * 100)
}
}
执行上述代码,输出结果如下:
main() 函数执行
testCoroutine() 函数执行
main() 函数执行
testCoroutine() 函数执行
上述代码中主线程中每 100ms 打印一次,总共打印 2 次,通过 go 另起一个协程,打印 10 次
打印是交替的,说明是并行的;另起的协程只打印了 2 次,因为主线程循环两次就退出了。
WaitGroup
通过上述的例子可以确定,当主线程结束后,主线程中包含的协程无论有没有执行完成都会随主线程一起结束,如果需要主线程等待协程执行完毕,那么就需要用到 WaitGroup。
WaitGroup 内部维护着一个计数器,通过计数器来判断所有并发任务是否完成,当启动了 N 个并发任务时,通过调用 Add() 方法就将计数器增加 N,每执行完一个任务都通过调用 Done() 方法使计数器 -1,通过调用 Wait() 方法等待并发任务执行完,当计数器为 0 时,所有并发任务完成。
// 定义一个全局计数器
var waitGroup sync.WaitGroup
//noinspection ALL
func main(){
waitGroup.Add(1)
// 开启第一个任务
go testCoroutineWithWaitGroup1()
// 开启第二个任务
waitGroup.Add(1)
go testCoroutineWithWaitGroup2()
waitGroup.Wait()
for i := 0; i < 2; i++ {
fmt.Println("main() 函数执行")
time.Sleep(time.Microsecond * 100)
}
}
func testCoroutineWithWaitGroup1(){
for i := 0; i < 5; i++ {
fmt.Println("testCoroutineWithWaitGroup1() 函数第", (i+1),"次执行")
time.Sleep(time.Microsecond * 100)
}
// 函数执行完,计数器 -1
waitGroup.Done()
}
func testCoroutineWithWaitGroup2(){
for i := 0; i < 5; i++ {
fmt.Println("testCoroutineWithWaitGroup2() 函数第", (i+1),"次执行")
time.Sleep(time.Microsecond * 100)
}
// 函数执行完,计数器 -1
waitGroup.Done()
}
执行上述代码,输出结果如下:
testCoroutineWithWaitGroup2() 函数第 1 次执行
testCoroutineWithWaitGroup1() 函数第 1 次执行
testCoroutineWithWaitGroup2() 函数第 2 次执行
testCoroutineWithWaitGroup1() 函数第 2 次执行
testCoroutineWithWaitGroup2() 函数第 3 次执行
testCoroutineWithWaitGroup1() 函数第 3 次执行
testCoroutineWithWaitGroup2() 函数第 4 次执行
testCoroutineWithWaitGroup1() 函数第 4 次执行
testCoroutineWithWaitGroup2() 函数第 5 次执行
testCoroutineWithWaitGroup1() 函数第 5 次执行
main() 函数执行
main() 函数执行
相比上一次,这次主线程是在两个协程任务执行完成之后才执行并退出的。
使用 WaitGroup 来让主线程等待协程任务执行需要四个步骤:
- 首先定义一个全局计数器
- 在每个函数/任务的代码块末尾(既函数执行结束时)调用函数将计数器 -1
- 在 main 函数中执行函数前调用计数器 +1
- 在主线程结束执行调用等待方法,直到计数器为 0,所有任务执行完成后,才会调用主线程中等待方法后面的代码