最近在研究go语言中,高性能缓存的实现,发现需要考虑到很多因素。(go语言中groupcache缓存库的源码解析可以看这一篇golang中cache组件的使用之groupcache(三))
这片文章主要讲解 golang 中的GMP。
开篇
提到GMP, 我们需要从最基础的进程、线程讲起。
网络上有很多进程线程的文章,本篇另辟蹊径,用故事举例的方式帮助大家理解。
本篇文章有不当之处希望大家可以指出。
进程、线程
并发与并行
大家都知道,在特定场景下(比如向1w台服务器发送指令),使用多线程或多进程会提升执行效率,那么提升效率的原因是什么呢?
简单点来说, 一个线程(或进程)相当于一个人
。假设我们现在需要种树,多个人在工具
(锹、铲子)足够的情况下,肯定会比一个人快,但是人多了容易把活干乱,这时就需要一个指挥者
。
那么,线程(或进程)相当于人, 刚才说的工具和指挥者相当于什么呢?
没错, 就是`cpu核心`和`调度器`。
在常见操作系统中,线程才是cpu真正的调度单位。进程中至少会包含一个线程。
我们现在的电脑或者服务器,都是多核心
的。
上文举了工人
和工具
的例子,帮助大家简单了解。那么更细致一点呢?
我们考虑下面的这种情况:
工人
有两个, 但是工具
只有一个。老板是个好心人,不忍心赶走多余的工人
老板思前想后, 多余的工人该怎么办呢?
有了,工人总需要吃饭、上厕所
,一个去吃饭、上厕所
,另一个顶上;或者两班倒,一个睡觉
了,另一个干活
。这样不就不会浪费了么。
这里的`吃饭、上厕所、睡觉`,指的就是`费时`的`io`操作。
单核cpu场景下,在运行的线程(进程)需要不停的切换,即“同一时刻”只有一个线程(进程)
在运行。这种场景下的多线程(进程), 称之为“并发”。
另外:现在的cpu都具有超线程能力,即1核cpu相当于2核cpu。
工人
有两个, 但是工具
有两个。
这种情况下,老板很开心,一个萝卜一个坑,两个工人可以同时干活,可以按照自己的心愿工作。甚至指挥者
都可以辞退了。
每个线程(进程)“绑定”一个cpu核心(这里涉及到cpu亲和性,不再本文的范围内),
同一时刻可以有多个线程(进程)同时运行,这种场景下多线程(进程), 称之为“并行”。
正常情况下,操作系统中的线程(进程)数量是大于核心数的。
所以调度器(指挥者)是铁定不能辞退的~~
上文是基本的概念,主要有以下几点:
- “同一时刻”只有一个线程(进程)在运行称之为
并发
,“同一时刻”有多个线程(进程)在运行称之为并行
- cpu需要不停的在线程(进程)间
切换
- 进程中至少包含一个线程
进程、线程的使用场景
上文我们模糊了进程和线程的区别,现在我们来区分它们。
工人们
同属于一个施工队
,由一个包工头带领,做着同一个种树的项目
。
这里我们把`工人`理解为线程,`项目`是我们`程序`的功能,`施工队`是工作`进程`。
这里的场景我们认为这是一个“单进程”,“多线程”的程序,即一个进程中有多个线程。
工人们
都在一个施工队
中干活,低头不见抬头见,所以关系十分要好,常常互相分享
吃的喝的。和隔壁
的施工队
却十分疏远
同一个进程中的线程,会共享代码段,数据段,堆,
每个线程在进程的栈空间创建一个属于自己的栈空间。
有一天施工队
突然工作产能跟不上了,老板只好又新找了一个施工队
,两个施工队
准备实施两班倒
,但是因为两队之间关系疏远,每次交接工作都十分麻烦。
两个`施工队`即为两个进程,cpu在进程间切换十分费时,
并且进程间的数据在默认情况下不能共享,只能通过其他外部工具或方法进行数据交换。
所以总结一下进程和线程的区别:
- 线程之间切换成本小,并且数据共享,方便数据修改。并且线程的创建和销毁速度较快。但是线程间会有资源竞争,需要加锁,一个线程崩坏有可能影响同一个进程下的其他线程。
- 进程之间切换成本大,因为资源隔离所以不存在资源竞争,进程间也不会相互影响。但数据共享比较麻烦。
需要操作同一个对象(如变量), 需要根据某些条件频繁创建销毁,优先使用线程,反之使用进程。