《Go 语言并发之道》读后感 - 第一章

前言

人生路漫漫,总有一本书帮助你在某条道路上打通任督二脉,《Go 语言并发之道》就是我作为一个 Gopher 道路上的一本打通任督二脉的书。说说我和它的偶遇,在一次 B 站云原生社区一次分享会上,众多大佬同推荐,并决定一起去读《Kubernetes 源码刨析》一书。我听到后心潮澎湃,冲到当当准备下单买下一本《Kubernetes 源码刨析》,但是发现竟然要邮费,那么我凑个单吧,在众多推荐中突然看见《Go 语言并发之道》,它作为树叶映衬着花朵次日便到了我楼下的快递柜。万万没想到,我对树叶的喜爱,远超花朵。

性能瓶颈

在 1965 年,戈丁·摩尔写了一篇三页的论文,成就了后期人们耳熟能详的摩尔定律。看到 Intel 如同牙膏一样的挤单核的频率,我们就可想物理性能极限的天花板或许已经到来了,所以开始推出多核 CPU ,多核多线程,以 Intel i9 为例已经是 8核16线程。

再以物理空间的举例,还记得5年前,我在一家传统行业龙头公司做桌面运维,在师父的指引下几乎将分公司所有的笔记本电脑拆解一同,炎炎夏日清理积灰。我就发现镶嵌在主板上的 CPU 是一个方方正正的放款。然而现在的 CPU 已经变成一个长方形躺在我们电脑主板上了。从形状的变化也可以看出 CPU 性能已经达到极限。

并发之苦

众所周知,并发代码是很难正确构建的。它通常需要完成几个迭代才能让它按预期的方式工作,即使这样,在某些时间点(更高的磁盘利用率,更多的用户登录到系统等)到达之前,Bug 在代码中存在数年的事情也不少见,以至于以前未发现的 Bug 在后面先露出来。

在书中提到了以下几种问题,在完成并发代码时常常遇见:

竞争条件

当两个或多个操作必须按正确的顺序执行,而程序并未保证这个顺序,就会发生竞争。例如:多个线程,进程同时修改一块内存空间,需要想办法确保修改的线后顺序,或正确性。

// 一个例子
var data int
go func(){
data++
}()
if data == 0{
fmt.Printf("The values is %v \n",data)
}

上面的代码有三种输出结果:

  • 不打印任何东西
  • 打印 “The values is 0"
  • 打印 ”The values is 1"

你会发现上面的代码执行顺序乱了,这个需要亲自做实验,多执行几遍。你可以用一个 for{} 试一下。

为了解决以上的问题,我们可以让程序在执行过程中暂停几秒,试着等待看程序是否会恢复正常。

// 一个例子
var data int
go func(){
data++
}()
// 暂停 3s
time.Sleep(3 * time.Second)
if data == 0{
fmt.Printf("The values is %v \n",data)
}

但是在实际生产,生活中我们程序所需要的执行时间是不固定的,有可能当前网速快,请求就变快;有肯能服务器磁盘有坏道,写盘卡住很长时间;较大的 JSON 数据在序列化与反序列化上花费了过多时间。当这个时候你怎么确定 time.Sleep() 时间呢?

原子性

当某些东西被认为是原子的,或具有原子性的时候,这就以为者它运行的环境中,它是不可分割的或不可中断的。

第一件非常重要的事情就是 “上下文”。你的程序,操作系统,硬件,都存在上下文。操作的原子性可以根据当前定义的范围而改变。

书中举了一个非常有趣的例子,我们大家应该都玩过游戏,游戏的外挂就是修改了游戏程序的内存中的上下文从而加强了你的角色。这对于游戏开发者来说,他们的程序没有问题,健康良好的运行,但是外挂修改了游戏程序在操作系统环境中的上下文。

不可分割(indivisible)和不可中断(uninterruptible)这些术语在你锁定义的上下文中,原子的东西将被完整运行,例如:

i++

但是以上原子操作又可以拆分成三步:

  1. 检索 i 的值
  2. 增加 i 的值
  3. 存储 i 的值

我的理解,针对于我们所写代码的操作,和想要出现的结果,需要原子性。但是再对一个函数细分,它可能就不是原子性的。

内存访问同步

假设有这样一个数据竞争:两个并发进程视图访问相同的内存区域,他们访问内存的方式不是原子的。就会出现竞争。这里需要提出一个新的名词,叫临界区(critical section)。举个例子:

var data int
go func(){ data++ }()
if data == 0{
fmt.Println(data)
}else{
fmt.Println(data)
}

例子中有三个临界区:

  • goroutine 正在使数据变量递增
  • if 语句,它检查数据的值是否为 0
  • fmt.Println() 语句,在检索并打印变量的值

为了保证内存访问操作的正确性,我们通常的方式是通过 sync 包在临界区加锁,好了现在我们知道加锁可以保证内存访问同步。那么问题来了:

  • 我的临界区是否是频繁进入和退出?
  • 我的临界区应该有多大?

死锁,活锁和饥饿

死锁:

死锁是所有的并发进程彼此等待的。在这种情况下,没有外界干预,程序将无法恢复。死锁例子,我这里就偷个懒不写了,相信刚接触 channel 的小伙伴一定被 deadlock 困扰了很久,在尘封的记忆中找出那段代码回一下吧。

出现死锁有几个必要条件。1971年,Edgar Coffman 的论文给出指导意见,Coffman 条件如下:

-相互排他,并发进程同时拥有资源的独占权。

  • 等待条件,并发进程必须同时拥有一个资源,并等待额外的资源
  • 没有抢占,并发进程拥有的资源只能被该进程释放,即可满足这个条件
  • 循环等待,并发进程P1 必须等待一系列其他的并发进程 P2,这些并发进程同时也等待 P1 ,这样便满足了这个最终条件。

活锁:

活锁是正在主动执行并发操作的程序,但是这些操作无法向前推进程序的状态。我的理解就是各退一步,然后再退,这就是活锁。

书中用两个人从走廊的两头通过走廊是互退一步举例,我们生活中还有类似例子。例如:你骑自行车按照交通规则靠右行驶,迎面来一个二杆子没有遵守交通规则,这样的错车径历谁都经历过,很有可能就撞一起了。

饥饿:

饥饿是在任何情况下,并发进程都无法获得执行工作所需的所有资源。举个例子:《海贼王》近期路飞被凯多囚禁了,去工地板砖,但是他是一个贪婪的工人,把所有的砖都搬完了,获得了大量的饭票,其他工人没有饭票就得饿肚子。当然路飞还是会分享食物给其他工友,但是计算机中的程序可不会这么智能。

在日常的开发过程中,我们需要找到一个平衡点,同步访问内存是昂贵的,所以将我们的锁扩展到临界区之外是有利的。另一方面,这样做我们就得冒着饿死其他并发进程的风险。

还有来自外部的饥饿,例如:CPU,内存,文件句柄,数据库链接等,任何必须共享的资源都是有可能产生饥饿的原因。

确定并发安全

最后,我们来谈谈开发并发代码的最困难的地方,即所有其他问的根源——人。每一行代码后面至少有一个人。

注释,首次别这么严重的强调了一次,特别是在并发代码中,作者希望每一个负责并发的团队,或人,把每一个并发函数,接口(类),注释清楚。

  • 谁负责并发?
  • 如何利用并发原语解决这个问题?
  • 谁负责同步?

如果没有足够的注释,调用方,复查代码的人可能需要非常多的时间才能够正确的使用已完成的并发代码,当这些人遇见这种情况,他可能选择重构。反复造轮子,你就会发现 TMD 重复代码怎么这么多!

面对复杂的简单性

这也许是我选择 Go 语言作为我的主语言的原因,作为一个从 Python 到 Go 的运维开发工程师,写 Go 代码的时候无数次回想起 Python 操作列表,字典的便捷,而且在写代码的时候是如此优雅,就想我们在说话写文章一样,然而开心是有代价的。写时简单,部署难,是我对 Python 程序的总结。Go 的代码看起来虽然丑,写起来也觉得丑,但是写时难,部署易,这对于运维来说,so happy!

并发方面,Python 线程池,进程池,需要各导入不同的包才可使用,协程不在官方库内,此时苦瓜脸。然而 Go 从原语级别解决这个问题,启动 goroutine 只需 go 即可,多个协程间的通信,我们创建 channel 即可

没有用过其他的语言,比如:Java,C++, Rust 等,我也不好做比较。

再次声明,我并没有诋毁 Python ,作为一个运维,没有 Python 这个世界是不完整 。:)

《Go 语言并发之道》读后感 - 第一章的更多相关文章

  1. 《Go 语言并发之道》读后感 - 第四章

    <Go 语言并发之道>读后感-第四章 约束 约束可以减轻开发者的认知负担以便写出有更小临界区的并发代码.确保某一信息再并发过程中仅能被其中之一的进程进行访问.程序中通常存在两种可能的约束: ...

  2. 《R语言入门与实践》第一章:R基础

    前言 本章介绍了 R 语言的基础知识 界面: 使用命令 “ R “进行命令行的实时编译 对象 定义: 用于储存数据的,设定一个名称 格式: a <- 1:6 命名规则: 规则1:不能以数字开头规 ...

  3. 《R语言实战》读书笔记--第一章 R语言介绍

    1.典型的数据分析过程可以总结为一下图形: 注意,在模型建立和验证的过程中,可能需要重新进行数据清理和模型建立. 2.R语言一般用 <- 作为赋值运算符,一般不用 = ,原因待考证.用-> ...

  4. 大道至简---软件工程实践者的思想------------java伪代码形式读后感第一章

    import.java.大道至简.*; 1.编程的精义----愚公移山 /* 原始需求的产生:惩山北之塞,出入之迂 项目沟通的基本方式:聚室而谋曰 项目的目标:毕力平险,指通豫南,达于汉阴 技术方案: ...

  5. 《Google软件测试之道》 第一章google软件测试介绍

    前段时间比较迷茫,没有明确的学习方向和内容.不过有一点应该是可以肯定的:迷茫的时候就把空闲的时间用来看书吧! 这本书,目前只是比较粗略的看了一遍,感触很大.以下是个人所作的笔记,与原文会有出入的地方. ...

  6. 《精通Spring4.X企业应用开发实战》读后感第一章

    Rod Johnson在2002年,编写了interface21框架,spring就是基于此.Spring于2004年3月24日发布了1.0 Spring遵循的理念“”好的设计优于具体实现,代码应易于 ...

  7. 【数据分析 R语言实战】学习笔记 第一章 数据分析导引

    1.1数据分析概述 1.1.1数据分析的原则 (1)数据分析是为了验证假设的问题,需要提供必要的数据验证.在数据分析中,分析模型构建完成后,需要利用测试数据验证模型的正确性. (2)数据分析是为了挖掘 ...

  8. 《大道至简》第一章读后感(java语言伪代码)

    中秋放假之际读了建民老师介绍的<大道至简>的第一章,其中以愚公移山的故事形象的介绍向介绍编程的精义.愚公的出现要远远早于计算机发展的历史,甚至早于一些西方国家的文明史.但是,这个故事许是我 ...

  9. 《大道至简》第一章——编程的精义_读后感(Java伪代码形式)

    <大道至简>第一章——编程的精义_读后感(Java伪代码形式)1.愚公移山//愚公为团体的项目组织者.团体经理.编程人员.技术分析师等//子孙荷担者三人为三名技术人员//遗男为外协//目标 ...

随机推荐

  1. 第15.23节 PyQt(Python+Qt)入门学习:Model/View架构中QListView视图配套Model的开发使用

    老猿Python博文目录 专栏:使用PyQt开发图形界面Python应用 老猿Python博客地址 一.概述 QListView理论上可以和所有QAbstractItemModel派生的类如QStri ...

  2. ActionResult的返回类型

    ActionResult是控制器方法执行后返回的结果类型,控制器方法可以返回一个直接或间接从ActionResult抽象类继承的类型,如果返回的是非ActionResult类型,控制器将会将结果转换为 ...

  3. Python术语对照表

    >>> 交互式终端中默认的 Python 提示符.往往会显示于能以交互方式在解释器里执行的样例代码之前. ... 可以是指:交互式终端中输入特殊代码行时默认的 Python 提示符, ...

  4. 图论补档——KM算法+稳定婚姻问题

    突然发现考前复习图论的时候直接把 KM 和 稳定婚姻 给跳了--emmm 结果现在刷训练指南就疯狂补档.QAQ. KM算法--二分图最大带权匹配 提出问题 (不严谨定义,理解即可) 二分图 定义:将点 ...

  5. AGC039D 题解

    题目描述 给定在笛卡尔坐标系的单位圆上的\(N\)个点(圆心为\((0, 0)\)).第\(i\)个点的坐标为\((cos(\frac{2 \pi T_i}{L}), sin(\frac{2 \pi ...

  6. 题解-CF436E Cardboard Box

    题面 CF436E Cardboard Box \(n\) 个关卡,对每个关卡可以花 \(a_i\) 时间得到 \(1\) 颗星,或花 \(b_i\) 时间得到 \(2\) 颗星,或不玩.问获得 \( ...

  7. spark中map和mapPartitions算子的区别

    区别: 1.map是对rdd中每一个元素进行操作 2.mapPartitions是对rdd中每个partition的迭代器进行操作 mapPartitions优点: 1.若是普通map,比如一个par ...

  8. Java NIO之Buffer(缓冲区)

    ​ Java NIO中的缓存区(Buffer)用于和通道(Channel)进行交互.数据是从通道读入缓冲区,从缓冲区写入到通道中的. ​ 缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存.这 ...

  9. Java 8 新特性:Lambda、Stream和日期处理

    1. Lambda 简介   Lambda表达式(Lambda Expression)是匿名函数,Lambda表达式基于数学中的λ演算得名,对应于其中的Lambda抽象(Lambda Abstract ...

  10. Docker(八): 安装ELK

    服务部署发展 传统架构单应用部署 应用程序部署在单节点中,日志资源同样输出到这台单节点物理机的存储介质中. 微服务架构服务部署 以分布式,集群的方式部署应用,应用分别部署在不同的物理机中,日志分别输出 ...