Golang并发编程基础

硬件

内存

作为并发编程一个基础硬件知识储备,首先要说的就是内存了,总的来说在绝大多数情况下把内存的并发增删改查模型搞清楚了其他的基本上也是异曲同工之妙。

内存芯片——即我们所知道的内存颗粒,是一堆MOS管的集合,在半导体称呼里面,很多MOS管组成一个半导体(组module),很多个module组成一个管芯(die),这个die即是内存颗粒,当然,更上一级即很多die组成的东西叫做晶圆(wafer)。

简单来说,每8个MOS管组成的电路可以表示一个字节,比如ASCII的‘A’,我们使用65表示,即0100 0001,那么8个MOS分别使用低-高-低-低-低-低-低-高电位即可表示字符A。

在对内存的写入和读取时,通常也是按照8个字开始作为一组进行操作,我们现在常用的CPU是64位,可以一次性处理64/8=8个字节的数据。

总线

首先明确一个概念:总线是线但是也不是线,以下是来自百科的解释:

总线(Bus)是计算机各种功能部件之间传送信息的公共通信干线,它是由导线组成的传输线束, 按照计算机所传输的信息种类,计算机的总线可以划分为数据总线、地址总线和控制总线,分别用来传输数据、数据地址和控制信号。总线是一种内部结构,它是cpu、内存、输入、输出设备传递信息的公用通道。

一个CPU要操作内存的数据,是通过总线来进行操作的,通常来说内存的读写操作不是一个CPU指令周期能完成的,如果多个程序在同时操作一个内存地址,则有各种意外的读写操作。

CPU

在单核CPU时期,硬件一次只能处理一个事情,在多任务的情况下不同的任务按需抢占CPU来执行它的代码,这里面就涉及到CPU调度工作,通常情况下,操作系统已经帮我们做了很多事,如果一个编程语言开启的并发操作是交给了操作系统的,那么调度这块不需要太关心,如果像Go这样有自己的协程调度器,还是需要专门了解下特有的调度方式的。

多核时期,基本原理也差不多,在对于硬件的理解上也可以完全参考单核。

CPU通过地址总线去寻找内存地址,比如0x00004567这种,64位CPU最大能操作的地址长度为264,32位操作系统则是232长度,所以为什么32位CPU最大只支持4GB内存呢?

几个代码示例

示例一

package main

import (
    "fmt"
)

var A int
func main() {
A = 0
for i:=0;i<100;i++{
    A++
}
fmt.Println(A)
}

示例二

package main

import (
    "fmt"
    "time"
)

var A int
func main() {
A = 0
for j:=0;j<100;j++{
    go add()
}
time.Sleep(1*time.Second)
fmt.Println(A)
}

func add(){
A++
return
}

示例一个示例二都将输出什么呢,直接告诉大家结果吧:绝大多数情况下都是100

那么go的协程难道这么听话,我们就完全很happy地编码了吗?先把示例二的100改成10000再试试吧_

我们再看看示例三和示例四:

示例三

package main

import (
    "fmt"
)

func main() {
for i:=0;i<10000;i++{
fmt.Println(i)
}
}

示例四

package main

import (
    "fmt"
    "time"
)

func main() {
for j:=0;j<10000;j++{
    go add(j)
}
time.Sleep(1*time.Second)
}

func add(j int){
fmt.Println(j)
return
}

示例三其实没太多好说的,单协程模型,输出也不会有什么意外,而示例四大家猜猜是按照1,2,3…9999这样的顺序呢还是其他输出顺序呢?

综上结果,我们会发现多协程模型里面的东西没有顺序性,对变量的操作也没有原子性。

示例五给出了Golang中最简单的加锁处理方式:

示例五

package main

import (
    "fmt"
    "time"
    "sync"
)

var A int
var LOCK *sync.Mutex

func main() {
A = 0
LOCK = new(sync.Mutex)
for j:=0;j<10000;j++{
    go add()
}
time.Sleep(1*time.Second)
fmt.Println(A)
}

func add(){
LOCK.Lock()
A++
LOCK.Unlock()
return
}

而关于多协程顺序性方面的实现方式,也可以比着葫芦画瓢写出来,这里就不再赘述了。

搬砖例子

假设在左边有三堆散乱的砖,我们需要将其从左边搬运到右边并堆放整齐,这样的一个工作我们从并发模型来看有哪些比较可执行的实现方式呢:

  1. 每堆砖头分配固定的人数,堆砖时为保证堆叠整齐度,采用排队的方式一个一个按先后顺序堆叠
  2. 拿一个人专职在左边递砖,若干人从左边的递砖人处拿砖,搬砖后在右边排队堆叠
  3. 左边专人递砖,右边专人堆砖,若干搬砖人只负责搬砖

这也是并发编程模型中比较常用的编程思路,在以后遇到类似问题的时候可以想想这个例子。

一个实际案例

我们以一个实际的案例作为结束,这个案例是导出某云平台所属设备信息的代码,里面包含有多协程拉取数据的实例,整体的流程如下:

  1. 参数初始化
  2. 定义一个接收协程结束的信息通道
  3. 开启N个协程
  4. 协程调用API获取信息,按分页参数每个协程获取(总数/N)信息,每次page=X+N
  5. 每次获取的信息放入excel缓冲区
  6. 当最后的分页获取不到信息时向通道写入东西表示该协程任务完成
  7. 主进程循环获取每个协程结束的信息,直到所有协程任务完成
  8. 将excel缓冲区数据写入excel文件
  9. 结束

连接如下:
//github.com/cm-heclouds/onenet_device_export/releases/tag/2018-latest
当然,这个案例在并发上其实还存在较大的提升空间,聪明的大家看看结合搬砖的例子来怎么提升呢。

Tags: