Go语言实战读书笔记(2):并发

前言

  • 本篇笔记基于《Go语言实战》
  • 本篇笔记主要记录一些gopl里没提到的,或者不同说法的地方
  • 这一部分都是关于并发的,比较重要,花了比较多功夫

并发

  • 并行是让不同的代码片段同时在不同的物理处理器(CPU)上执行
  • 并行是同时做很多事情
  • 并发是同时管理很多事情,这些事情可能只做了一半就被暂停去做别的事

线程(thread)与进程(process)

  • 当运行一个应用程序,操作系统会为这个程序启动一个进程,这个进程包含了应用程序在运行中需要用到和维护的各种资源(包括内存地址空间、文件和设备的句柄、线程等),相当于容器
  • 一个线程是一个执行空间,这个空间会被操作系统调度(这是操作系统的线程调度器,注意区分goroutine调度器)来运行函数中所写的代码
  • 每个进程至少包含一个线程,每个进程初始线程被称为主线程
  • 由于主线程的空间是应用程序本身的空间,所以主线程终止时,应用程序也会终止
  • 操作系统的线程调度器可将线程调度到某个处理器(CPU)运行
  • 操作系统的物理处理器(CPU)调度线程并运行该线程

goroutine概述

  • Go语言的并发同步模型来自一个叫做CSP(Communicating Sequential Processes)的范型(paradigm)
  • Go语言里的并发指的是能让某个函数独立于其它函数运行的能力
  • 当一个函数创建为goroutine时,Go会将其视为一个独立的工作单元
  • 这个单元会被调度(由goroutine调度器实现)到可用的逻辑处理器上执行
  • goroutine调度器可以管理被创建的所有goroutine并为其分配执行时间;可以将操作系统的线程与语言运行时的逻辑处理器绑定,并在逻辑处理器上运行goroutine

goroutine的并发与并行

  • 操作系统会在物理处理器上调度线程来运行,而Go语言程序运行时在逻辑处理器上调度goroutine运行
  • 每个逻辑处理器都分别绑定到单个操作系统线程
  • 在1.5版本上,Go语言运行时会为每个可用的物理处理器(CPU)分配一个逻辑处理器;在1.5版本之前,默认整个应用程序只分配一个逻辑处理器
  • 如果创建一个goroutine并准备运行,这个goroutine会被放到Go调度器全局运行队列中,之后Go调度器就将这个goroutine分配给一个逻辑处理器,这个逻辑处理器将其放入对应的本地运行队列
  • 本地运行队列的goroutine会一直等待知道被分配的逻辑处理器执行

  • 逻辑处理器会被绑定到唯一的操作系统线程
  • 正在运行的goroutine需要执行一个阻塞的系统调用时,线程和goroutine会从逻辑处理器分离,该线程会继续阻塞,等待系统调用的返回
  • 与此同时,逻辑处理器失去了用来运行的线程,这时Go调度器会创建一个新线程,并将该线程绑定到这个逻辑处理器
  • 之后,Go调度器会从本地运行队列里选择另一个goroutine来运行
  • 一旦被阻塞的系统调用返回,对应的goroutine会被放到本地运行队列,而之前的线程会保存好,以便后用

  • Go调度器对可以创建的逻辑处理器的数量没有限制,但Go语言运行时默认限制每个程序最多创建10000个线程
  • 可以通过runtime/debug包的SetMaxThreads方法来更改每个程序最多创建的线程数
  • 如果希望goroutine并行,必须使用多于一个逻辑处理器
  • 当有多个逻辑处理器时,Go调度器会将goroutine平等分配到每个逻辑处理器上,也就是让goroutine在不同的线程上运行
  • 不过要想真的实现并行的效果,用户需要让自己的程序运行在多个物理处理器(CPU)的机器上

概念小整理

  • 上述有关并发的概念都加粗处理了
  • 物理处理器,线程,进程,主线程,应用程序,操作系统线程调度器,Go调度器,逻辑处理器,全局运行队列,本地运行队列
  • 笔者的小结:
    1.当启动一个应用程序,操作系统会为其创建相应的进程
    2.进程拥有应用程序所需的各种资源,包括内存地址空间,线程
    3.每个进程至少有一个线程,每个进程的初始线程被称为主线程
    4.线程是一个执行空间,而主线程的空间就是应用程序的空间,因此主线程终止,应用程序也会终止
    5.操作系统线程调度器可以将线程调度到物理处理器(CPU)
    6.物理处理器(CPU)调度线程并运行
    7.对于Go语言1.5以上版本的程序(进程)而言,Go调度器默认会为每个可用物理处理器(CPU),分配一个逻辑处理器
    8.创建一个goroutine并运行,这个goroutine会被放在Go调度器全局运行队列
    9.Go调度器负责把这些队列中goroutine分配给一个逻辑处理器,并且将这个逻辑处理器绑定到唯一的操作系统线程
    10.逻辑处理器会将相应的goroutine放入本地运行队列
    11.本地运行队列中的goroutine会一直等待直到自己被分配到逻辑处理器执行
    12.当goroutine执行一个阻塞的系统调用,其对应线程会从逻辑处理器上分离,该线程被阻塞,等待系统调用的返回
    13.Go调度器会为失去线程的逻辑处理器创建一个新线程,并将新线程绑定到该逻辑处理器
    14.系统调用执行完成并返回,对应的goroutine会放回到本地运行队列,而之前的线程会被保存后,以便后用

goroutine

  • runtim包中的GOMAXPROCS(num)函数可以设置分配给Go调度器使用的逻辑处理器的数量
  • runtime包中的NumCPU()可以获取可以使用的物理处理器数量
  • sync包中WaitGroup类型,通过它的add方法可以计数,Done方法来减少计数,Wait方法来等待计数结束

竞争状态(race condition)

  • 定义:如果两个或多个goroutine在没有相互同步的情况下,访问某个共享的资源,并试图同时读和写这个资源,就处于相互竞争的状态
  • Go语言有一个工具可以在代码里检测竞争状态:go build -race
  • gopl-zh的翻译者翻译成了竞争条件,反而不好理解

锁住共享资源(竞争状态的处理方法)

  • sync/atomic包中原子函数
  • sync包中的互斥锁(gopl还提到了读写锁)
  • 通道方法:
    1. 无缓冲通道

    2. 有缓存通道

    3. 特别说明:当通道关闭后,goroutine依旧可以从通道接收数据,但是不能再向通道里发送数据

并发模式

runner

  • runner包用于展示如何使用通道来监视程序的执行时间,如果程序运行时间太长,也可以用runner包来终止运行
  • 笔者理解:使用Runner模式来执行一系列串行任务,可以设置运行的时间,还可以终止运行
  • corn作业 :crontab命令常见于Unix和类Unix的操作系统之中,用于设置周期性被执行的指令。该命令从标准输入设备读取指令,并将其存放于“crontab”文件中,以供之后读取和执行。该词来源于希腊语chronos(χρόνος),原意是时间。通常,crontab储存的指令被守护进程激活,crond常常在后台运行,每一分钟检查是否有预定的作业需要执行。这类作业一般称为cron jobs。

pool

  • pool包用于展示如何使用有缓冲的通道实现资源池,来管理可以在任意数量的goroutine之间共享及独立使用的资源
  • Go1.5版本并没有pool包,而1.6及以后版本中,实现标准库sync.Pool资源池,推荐使用

work

  • work包的目的是展示如何使用无缓冲的通道来创建一个goroutine池,这些goroutine执行并控制一组工作,让其并发执行

小结

  • 可以使用通道来控制程序的生命周期
  • 带default分之的select语句可以用来尝试向通道发送或者接收数据,而不会阻塞
  • 有缓冲的通道可以用来管理一组可复用的资源
  • 语言运行时会处理好通道的协作和同步
  • 使用无缓冲的通道来创建完成工作的goroutine池
  • 任何时间都可以用无缓冲的通道来让两个goroutine交换数据,在通道操作完成时一定保证对方接收到数据

猜你喜欢

转载自www.cnblogs.com/laiyuanjing/p/11291658.html