跳转至

并发与系统编程

Golang调度机制浅析

最近在部门中做了一次技术分享,现将分享内容总结成博文发布出来,内容有删改。

Golang以并发见长,支持成千上万个协程调度。Golang中协程称为Goroutine,它是Go runtime调度中的最小执行单元,Goroutine的创建、管理、调度运行的机制采用的GMP模型。本次分享介绍的就是Golang调度机制的GMP模型。

并行 vs 并发

并行(Parallelism) 指的是一个CPU时间片内可以同时做多件事情。并行强调的是某一时间点内能够同时处理多件事情,并行需要多核CPU提供支持。并行是并发的子集

并发(Concurrency) 指的是是一种同时处理许多事情的能力,并行强调是某一时间段内能够同时处理多件事情

并发编程系列:谈谈锁的实现机制

最近读了《Operating Systems: Three Easy Pieces》一书,全书主要围绕虚拟化、并发和持久化这三个主题展开,其中并发部分中介绍锁的章节,行文风趣幽默,写得非常精彩。文中介绍了多种实现锁的方案,以及各种锁的适用场景和优缺点。本文基于该书中锁章节,以一个gopher的角度去分享、拓展书中介绍的锁,并尽量使用Go实现书中介绍的几款自旋锁。

锁的基本思想

锁(lock)的目的是给临界区(Critical Section)加上一层保护,以保证临界区中代码能够像单条原子指令一样执行。临界区指的是一个访问共享资源的程序片段,比如对全局变量的访问、更新。在Linux系统中保护临界区的机制除了锁之外,还有信号量,屏障,RCU等手段。

锁本质是一个变量,我们通过lock()和unlock()这两个语义函数来操作锁变量。当线程准备进入临界区时候,会调用lock()尝试获取锁,当该锁状态是未上锁状态时候,线程会成功获取到锁,从而进入到临界区,如果此时其他线程尝试获取锁而进入临界区,会阻塞或者自旋。获取锁并进入临界区的线程称为锁的持有者,当锁持有者退出临界区时候,调用unlock()来释放锁,那么阻塞等待的其他线程继续开始竞争这个锁。下面是获取锁和释放锁的代码示例:

lock_t mutex;
lock(&mutex); // 加锁
balance = balance + 1; // 临界区资源
unlock(&mutex); // 释放锁

并发编程系列:开篇

本文是并发编程系列博文的开篇。笔者将打算写一系列并发编程相关的博文。下面是具体索引,方便大家查看:

  • 并发编程系列:谈谈锁的实现机制

    • 介绍锁的几种实现机制,自旋锁,两阶段锁,读写锁,混合空间锁等知识
  • 并发编程系列:死锁

    • 介绍死锁的定义,死锁产生的原因,以及几种解决死锁的方案(活锁、银行家算法、乐光并发控制等)。
    • 以哲学家就餐问题为示例实现几种避免死锁的方案。
  • 并发编程系列:无锁编程之栈

    • 介绍CAS原子操作,并以此实现无锁并发安全的栈。介绍CAS存在的ABA问题,以及解决办法。
    • 介绍如何使用指数退避栈解决栈顶争用严重问题
  • 并发编程系列:无锁编程之队列

    • 介绍有界部分队列,无界完全队列这两种类型队列。
  • 并发编程系列:无锁编程之ring buffer

    • 介绍ring buffer, 以及MPMC、MPSC、SPMC、SPSC等模型的ring buffer无锁版本实现
  • 并发编程系列:无锁编程之优先级队列

    • 介绍优先级队列概念
    • 基于锁实现和无锁实现的优先级队列
  • 并发编程系列:缓存一致性协议、内存屏障

Golang并发调度的GMP模型

Golang的一大特色就是Goroutine。Goroutine是Golang支持高并发的重要保障。Golang可以创建成千上万个Goroutine来处理任务,将这些Goroutine分配、负载、调度到处理器上采用的是G-M-P模型。

什么是Goroutine

Goroutine = Golang + Coroutine。Goroutine是golang实现的协程,是用户级线程。Goroutine具有以下特点:

  • 相比线程,其启动的代价很小,以很小栈空间启动(2Kb左右)
  • 能够动态地伸缩栈的大小,最大可以支持到Gb级别
  • 工作在用户态,切换成很小
  • 与线程关系是n:m,即可以在n个系统线程上多工调度m个Goroutine