一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第21天,点击查看活动详情。
什么是并发
go语言的并发属于go语言中一大亮点,其他语言创建并发是通过线程,而go语言则通过协程,协程是一个轻量级的线程。进程或者线程在一台电脑中最多不能超过一万个,而协程可以在一台电脑中创建上百万个也不会影响到电脑资源。学习之前先知道一些并发与并行的一些概念。
-
并发是指在同一个时间点上只能执行同一个任务,但是因为速度非常快,所以就像同时进行一样。 -
并行是指在一个时间点上同时处理多个任务。真正的并行,是需要电脑硬件的支持,单核的CPU是无法达到并行的。并行,他不一定快因为并行运行时是需要通信的,这种通信的成本还是很高的,而并发的程序成本很低。 -
进程就是一个独立功能的程序,在一个数据集中的一次动态执行过程,可以认为他是一个正在执行的程序,比如打开一个QQ就是在运行一个进程。 -
线程线程是被包含在进程之中的,它是比进程更小的能独立运行的基本单位 一个进程可以包含多个线程。例如、打开文档在你输入文字的时候他还在后台检测你输入的文字的大小写,还有拼写是否正确 ,这就是一个线程来检测的。 -
协程协程属于一种轻量级的线程,英文名 Goroutine 协程之间的调度由 Go运行时(runtime)管理。
什么是Goroutine
goroutine 协程。是go语言中特有的名词,他不同于进程Process,以及线程Thread。Go语言创造者认为和他们还是有区别的,所以创造为goroutine。goroutine与线程相比创建成本非常小,可以认为goroutine就是一小段代码,我们使用goroutine往往是执行某一个特定的任务,也就是函数或者方法。
与函数不同的是goroutine调用之后会立即返回,不会等待goroutine的执行结果,所以goroutine不会接收返回值。 把封装main函数的goroutine叫做主goroutine,main函数作为主goroutine执行,如果main函数中goroutine终止了,程序也将终止,其他的goroutine都不会再执行。
使用匿名函数创建Goroutine
func main() {
go func() {
fmt.Println("匿名函数创建goroutine执行")
}()
time.Sleep(1000 * time.Millisecond)
fmt.Println("主函数执行")
}
runtime包
虽然说Go编译器将Go的代码编译成本地可执行代码。不需要像java或者.net那样的语言需要一个虚拟机来运行,但其实go是运行在runtime调度器上的,它主要负责内存管理、垃圾回收、栈处理等等。也包含了Go运行时系统交互的操作,控制goroutine的操作,Go程序的调度器可以很合理的分配CPU资源给每一个任务。
Go1.5版本之前默认是单核执行的。从1.5之后使用可以通过runtime.GOMAXPROCS()来设置让程序并发执行,提高CPU的利用率。
Go语言临界资源安全
临界资源
指并发环境中多个协程之间的共享资源,如果对临界资源处理不当,往往会导致数据不一致的情况。例如:多个goroutine在访问同一个数据资源的时候,其中一个修改了数据,另一个goroutine在使用的时候就不对了。
需要控制goroutine协程在执行过程中保证数据的安全。
sync同步包
sync同步包,是Go语言提供的内置同步操作,保证数据统一的一些方法,WaitGroup 等待一个goroutine的集合执行完成,也叫同步等待组,使用Add()方法,来设置要等待一组goroutine 要执行的数量。用Done()方法来减去执行goroutine集合的数量。使用Wait() 方法让主goroutine也就是main函数进入阻塞状态,等待其他的子goroutine执行结束后,main函数才会解除阻塞状态。
//创建一个同步等待组的对象
var wg sync.WaitGroup
func main() {
wg.Add(3) //设置同步等待组的数量
go Relief1()
go Relief2()
go Relief3()
wg.Wait() //主goroutine进入阻塞状态
fmt.Println("main end...")
}
func Relief1() {
fmt.Println("func1...")
wg.Done() //执行完成 同步等待数量减1
}
func Relief2() {
defer wg.Done()
fmt.Println("func2...")
}
func Relief3() {
defer wg.Done() //推荐使用延时执行的方法来减去执行组的数量
fmt.Println("func3...")
}
在并发编程中存在多个goroutine协程共用同一条数据,也就是临界资源的安全问题,为了解决这种的问题Go语言提供了同步等待组的方法解决,Go语言还提供了锁,控制并发情况下的数据安全。Go语言提供两种锁类型互斥锁和读写锁。当一个协程在访问当前数据资源的时候,给当前资源加上锁, 防止另外的协程访问,等待解锁后其他协程才能够访问。
互斥锁
互斥锁,当一个goroutine获得锁之后其他的就只能等待当前goroutine执行完成之后解锁后才能访问资源。对应的方法有上锁Lock()和解锁Unlock()。
在使用互斥锁的时候一定要进行解锁,否则会造成程序的死锁。
读写锁
互斥锁是用来控制多个协程在访问同一个资源的时候进行加锁控制,保证了数据安全,但同时也降低了性能,如果说多个goroutine同时访问一个数据,只是读取一下数据,并没有对数据进行任何修改操作,那么不管多少个goroutine来读取都应该是可以的。主要问题在于修改。修改的数据就需要加锁操作,来保证数据在多个goroutine读取的时候统一。 读取和读取之间是不需要互斥操作的,所以我们用读写锁专门针对读操作和写操作的互斥锁。
与普通的互斥锁的区别在于他能分别针对读操作和写操作进行锁定和解锁。读写锁的控制规则是,他允许任意多个读操作同时进行,但是只允许一个写操作进行,并且在某一个写操作的时候不允许读操作进行。