Go Book / 1 Go Basics / 13 Go并发编程初探

13 Go并发编程初探

一、Go并发编程概述

Go以并发性能强大著称,在在语言级别就原生支持,号称能实现百万级并发,并以此独步江湖,本专题我们开始学习Go并发编程。

为深入了解到Go并发的各种特性,该专题会分四部分展开:

简单来说,Go的并发编程由协程(goroutine)和管道(channel)两个设计机制实现,其中协程可以理解为微线程,管道也叫通道。这两个核心机制实现了go并发的基本运行单元和通信机制。而为go高并发性能提供支持的,是go运行时内部的协程调度器,其基于MPG调度模型。当然,Go并发编程也是有陷阱的,稍有不慎就会造成死锁退出,这方面我们需要着重避免。另外并发也有一定的安全问题,对资源无控制的访问肯定会造成数据混乱,Go提供sync包实现同步机制,让一些不适合并发的场景有同步的支持。最后介绍几种Go并发编程的范式,让你在实战中能够巧妙的构建程序。

二、并发编程初探

并发是由来已久的概念,多个任务同时执行能够为程序得到指数级增长的效率,在讲Go的并发编程前我们需要理清几个概念,以让我们更了解Go并发的实现。

1.异步与同步
  • 同步 sync:同步就是顺序执行,执行完一个再执行下一个,需要等待、协调运行;
  • 异步 async:与同步相对,异步就是彼此独立,在等待某事件的过程中继续做自己的事,不需要等待这一事件完成后再工作;

go的异步由开启goroutine实现,go程是独立运行的执行单元,而处理异步任务中的同步操作,go有专门的sync包。

在过程式编程中,大多数程序的执行方式都是按照编码顺序,一步一步执行到程序结束。在早期cpu单核心时代,是没有真正的并发概念的,因为单核心cpu在同一时间只能执行一项任务,也就是串行执行。随着时代的发展,一颗cpu已经可以集成多个核心,这让cpu在同一时间并发执行多项任务有了物理基础。

2. 阻塞block

某个并发任务由于拿不到cpu资源没法工作,只能等待其他任务完成让出cpu资源。

3. 异步回调 async callback

A线程唤起B线程,令其独立执行,同时给B一个回调函数,命令B在干完活以后执行这个回调函数,这个回调函数会与A线程发生交互。

4. 进程 - 线程 - 协程
  • 进程

==进程是系统资源分配的最小单位==, 系统由一个个进程(程序)组成, 一般情况下,包括文本区域(text region)、 数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码, 数据区域存储变量和进程执行期间使用的动态分配的内存, 堆栈区域存储着活动过程调用的指令和本地变量。

进程间切换:
1.切换页目录以使用新的地址空间;
2.切换内核栈;
3.切换硬件上下文;

通信问题: 由于进程间是隔离的,各自拥有自己的内存内存资源, 因此相对于线程比较安全, 所以不同进程之间的数据只能通过 IPC(Inter-Process Communication) 进行通信共享。

  • 线程

==线程是程序执行流的最小单元==。一个标准的线程由线程ID, 当前指令指针(PC),寄存器集合和堆栈组成。线程属于进程, 线程共享进程的内存地址空间。

线程间切换:
1.切换内核栈
2.切换硬件上下文

通信问题: 进程相当于一个容器,而线程而是运行在容器里面的,因此对于容器内的东西,线程是共同享有的,因此线程间的通信可以直接通过全局变量进行通信,但是由此带来的例如多个线程读写同一个地址变量的时候则将带来不可预期的后果,因此这时候引入了各种锁的作用,例如互斥锁等。

  • 协程

==协程是属于线程的。协程程序是在线程里面跑的,因此协程又称微线程和纤程等。== 协没有线程的上下文切换消耗。协程的调度切换是用户(程序员)手动切换的,因此更加灵活,因此又叫用户空间线程。由于协程是用户调度的,所以不会出现执行一半的代码片段被强制中断了,因此无需原子操作锁。

5. 共享内存

在许多基于线程并发的语言中,共享内存是常见的做法,多个并发线程通过共享内存的方式交互数据,通常这会导致很多线程安全问题:AB间共享的数据地址可能被C并发修改。

6.同步锁/资源锁

为了解决共享内存所导致的线程安全问题,共享的内存地址在特定时间段被特定线程锁定,而加锁期间,其它线程却无法访问,这带来低效率问题。

7.死锁

A锁住B要的资源,B锁住A要的资源,AB同时阻塞。

8.线程池与线程并发

在多线程并发中,每个线程都抢占cpu资源,导致线程任务不断切换上下文,cpu也会分出部分算力处理线程调度。因此线程不是越多越好,为了避免无度创建线程(内存溢出OutOfMemory),一般在一个池中创建一堆线程,各任务循环利用这些线程,用完以后重置并丢回池中。

利:避免了无度创建线程,降低了OOM的风险
弊:用不用都占去了一大块内存开销

由此可见基于线程的并发是把双刃剑:首先开线程占内存, 啥也不干就拿走1M栈空间,1024条线程就占用1G内存;其次,线程切换占CPU的调度资源;再次,内存共享不安全,加了锁效率又低下;最后,有可能因为各种异步回调导致开发难度加大。

9.堆栈

堆和栈的概念非常容易造成混淆,根本原因是在计算机内存结构和软件编程中的数据结构都有相同的名称。

编程中数据结构的堆和栈:

栈:栈是一种先进后出的线性结构,可以使用数组或链表(单向链表、双向链表或循环链表)作为底层数据结构;
堆:堆是一种常用的树形结构,是一种特殊的完全二叉树,其底层数据结构都用数组;

计算机内存结构中的堆栈:

栈
    变量和对象的名称
    引用堆地址
堆
    杂乱无章地堆放各种数据
    没有栈对其进行引用时,就由nil进行引用
    被nil引用的堆地中的内容随时可能被垃圾回收器回收
垃圾回收
    一块堆内存如果没有被栈引用,就会被0号栈(空nil)所引用
    一切被nil引用的对内存,会随时被垃圾回收器(GarbageCollector=GC)回收
10. CSP并发模型

与共享内存的并发通信模型不同,CSP(CommunicatingSequentialProcess 可通信的序列化进程)模型是基于管道(channel)进行任务间的消息传递。Go就是根据CSP模型实现并发任务的通信。

共享内存 VS 通道
内存共享:通过内存共享通信
通道:通过通信共享内存

介绍完这些基础概念后,我们开始展开Go并发编程!