Go 并发编程
概述
简而言之,所谓并发编程是指在一台处理器上“同时”处理多个任务。
宏观的并发:是指在一段时间内,有多个程序在同时运行。
微观的并发:是指在同一时刻只能有一条指令执行,但多个程序指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但是在微观上并不是同时执行的,只是把时间分为若干段,使多个程序快速交替的执行。
并行和并发
- 并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行
- 并发(concurrency):通过 cpu 时间片轮转使多个进程快速交替的执行。
- 以咖啡机的例子来解释并行和并发的区别:
- 并行是两个队列同时使用两台咖啡机(真正的多任务)
- 并发是两个队列交替使用一台咖啡机(假的多任务)
常见的并发编程技术
进程并发
程序与进程
程序:是指编译好的二进制文件,在磁盘上,占用磁盘空间,不占用系统资源(cpu、锁…)。
进程:运行起来的程序。占用系统资源。
把程序比作剧本,进程比作戏(舞台、演员…),同一个剧本可以在多个舞台同时上演。同样,同一个程序也可以加载为不同的进程(彼此之间互不影响)。
如:同时开两个终端。各自都有一个 bash 但彼此 ID 不同。
在 windows 系统下,通过查看“任务管理器”,可以查看相应的进程。运行起来的程序就是一个进程。
进程状态
进程基本的状态有 5 种:初始态、就绪态、运行态、挂起态与终止态。其中初始态为进程准备阶段,常与就绪态结合来看
进程并发会出现的问题
- 系统开销比较大,占用资源比较多,开启进程数量比较少。
- 在 unix/linux 系统下,还会产生
孤儿进程
和僵尸进程
。
在 unix/linux 系统中,正常情况下,子进程是通过父进程 fork 创建的,子进程再创建新的进程。并且父进程永远无法预测子进程到底什么时候结束。当一个进程完成他的工作终止之后,它的父进程需要调用系统调用子进程的终止状态。
windows 下的进程和 Linux 下的进程是不一样的,它比较懒惰,从来不执行任何东西,只是为线程提供执行环境。然后由线程负责执行包含在进程的地址空间中的代码。当创建一个进程的时候,操作系统会自动创建这个进程的第一个线程,成为主线程。
孤儿进程:父进程先于子进程结束,则子进程成为孤儿进程,自进程的父进程成为 init 进程,称为init 进程领养孤儿进程。
僵尸进程:进程终止,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。
线程并发
什么是线程
- LWP:light weight process 轻量级的进程,本质仍是进程(Linux下)
- 进程:独立地址空间,拥有 PCB
- 线程:有独立的 PCB,但没有独立的地址空间(共享)
- 区别:在于是否共享地址空间。独居(进程);合租(线程)
- 线程:最小的执行单位
- 进程:最小的分配资源单位,可看成是一个现成的进程。
在 Windows 系统下,可以直接忽略进程的概念,只谈线程。因为线程是最小的执行单位,是被系统独立调度和分派的基本单位。而进程只是给线程提供执行环境。
线程同步
同步即协同步调,按预约的先后次序运行。
线程同步,只一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其他线程为保证数据一致性,不能调用该功能。
- 举例1:银行存款 5000。同一时刻,柜台存折取3000,提款机用卡取3000.剩余:2000。
产生的现象叫做“与时间有关的错误”(time related)。为了避免这种数据混乱,线程需要同步。
同步的目的是为了避免数据混乱,解决与时间有关的错误。实际上,不仅线程间需要同步,进程间、信号间等等都需要同步机制。
因此,所有多个控制流,共同操作一个共享资源的情况,都需要同步。
线程同步机制 - 锁的应用
- 互斥锁 mutex(也称之为互斥量)。每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。但是同一时刻,只能有一个线程持有该锁,没有拿到锁的线程,阻塞等待。等到拿到锁的线程释放锁。
-
读写锁:与互斥锁类似,但读写锁允许更高的并行性。其特征为:写独占,读共享。读锁、写锁并行阻塞,写锁优先级高。
- 读写锁也叫共享-独占锁。当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。写独占,读共享。
协程并发
协程:coroutine。也叫轻量级线程。
一个线程中可以有任意多个协程,但某一时刻只能有一个协程在运行,多个协程分享该线程分配到的计算机资源。
在协程中,调用一个任务就像调用一个函数一样,消耗的系统资源最少,但能达到进程、线程并发相同的结果。
在一次并发任务中,进程、线程、协程可以实现。从系统资源消耗的角度出发来看,进程相当多,线程次之,协程最少。
比较
进程、线程、协程都可以完成并发。进程稳定性强、线程节省资源、协程效率高。
Go 并发
Go 在语言级别支持协程,叫 goroutine
。Go语言标准库提供的所有系统调用操作(包括所有同步的 IO 操作),都会出让 CPU 给其他 goroutine。这让轻量级线程的切换管理不依赖于系统的线程和进程,也不需要依赖于 CPU 的核心数量。
Go 从语言层面就支持并行。同时,并发程序的内存管理有时候是非常复杂的,而 Go 语言提供了自动垃圾回收机制。
Go 语言为并发编程而内置的上层 API 基于顺序通信进程模型 CSP(communicating sequential process)。这就意味着显示锁是都可以避免的,因为 Go 通过相对安全的通道发送和接收数据以实现同步,这大大的简化了并发程序的编写。
Go 语言中的并发程序主要使用两种手段来实现:goroutine
和 channel
。