Once

once是什么 和singlefight有些相似,singlefight是并发执行时只有一个在执行, once也是并发时只有一个在执行,只不过,只执行一次,再次调用不会在执行 once怎么用 var A int var once = sync.Once{} func initA() int { once.Do(func() { //这里只会执行一次 A = 10 //A=10 只会执行一次,并且所有并发进来的,都需要等待A=10 完成后返回 }) return A // A=10 happens before 读取A, 所以initA()在所有gorutine里,都返回10 } 这个例子我们可以构造一个懒汉模式单例 源码阅读 Once结构 type Once struct { done uint32 m Mutex } Once结构很简单,只有两个字段, done来表示是否执行完成, m为互斥锁 Do函数 func (o *Once) Do(f func()) { //判断done 如果没完成,则执行doSlow函数,否则直接返回退出函数 if atomic.LoadUint32(&o.done) == 0 { o.doSlow(f) } } doSlow (第一次并发执行时才会进入的分支) func (o *Once) doSlow(f func()) { o.m.Lock() defer o.m.Unlock() //上锁 if o.done == 0 { defer atomic.StoreUint32(&o.done, 1) //完成标志设置为1 f() //执行传入的函数 } }

<span title='2022-06-25 20:50:16 +0800 +0800'>六月 25, 2022</span>

数据竞赛(Data Race)

没有安全的数据竞赛,不要使用各种炫技的无锁方式等骚操作! 什么是数据竞赛 并行程序在未使用同步方法(atomic, lock)的情况下, 并发读写共享资源就会造成数据竞争 数据竞赛检测原理 通过编译器注入检测代码, 检测代码会保存读写内存的 线程id, 时钟, 读写位置和长度, 是否写入等信息, 在运行的过程判断,读写内存是否有交叉,是否满足happens-before等条件,来判断数据竞赛 如何避免 使用同步方法去解决happens-before 比如使用atomic包, sync包 或者使用chan go检测数据竞赛方法 使用go工具链 go build -race 编译一个带有数据竞赛检测的可执行程序,会在编译期插入代码,这种程序消耗内存和CPU是不带检测的数倍到数十倍,不可大范围用于生产环境 go run -race 直接运行一个带检测的程序(内部也经过编译) go test -race 运行待检测的单元测试 go install -race 编译并安装一个待检测的可执行程序 注意数据竞赛检测是需要程序运行到有竞赛的代码才会检测到!运行不到的是不会检测出来的!

<span title='2022-06-23 16:39:21 +0800 +0800'>六月 23, 2022</span>

go内存模型

介绍 Go内存模型指定了一种条件,在这种条件下,可以保证读取一个goroutine中的变量,以观察不同goroutine中写入同一变量所产生的值。 建议 修改多个goroutine同时访问的数据必须序列化访问 序列化访问保护数据使用channel操作或者其他同步原语比如sync或者sync/atomic包 Happens before 在一个goroutine中,读写必须表现得就像它们按照程序指定的顺序执行一样;也就是说 在一个goroutine中,处理器和编译器可以重排读写的执行顺序,仅当重排后的行为不改变语言的设定.因为重排,一个goroutine观察到的执行顺序可能与另一个goroutine观察到的顺序不同.举个例子,如果一个goroutine执行a = 1; b = 2;,另一个可能会观察到b在a之前更新. 为了指定读写的需要,我们定义了在Go程序中执行内存操作的偏序(partial order)。如果事件$e_1$先发生于事件$e_2$我们说$e_2$后发生于$e_1$. 同样的如果$e_1$没有先发生于$e_2$并且$e_1$没有后发生于$e_2$那么我们说$e_1 e_2$同时发生. 在单个gorutine里, happens-before的顺序是程序表示的顺序. 如果下面两个条件成立,则允许变量v的读r 观察到对v的写w: r没有先发生于w 没有其他对v的写w’ ,后发生于w, 先发生于r (也就是在w 和 r之间不存在 w') 为了保证r能读到w的写,要确保w是允许r观察的唯一写入.也就是说,如果以下两个条件均成立,则保证r观察到w: w先发生于r 任何其他写入共享变量v都要先发生于w或者后发生于r 这对条件比第一对更严格,这一对要求没有其他的写同时发生于w或r 在单个gorutine中没有并发,这两个定义是等价的:读r观察最近写入w到v的值. 在多个gorutine访问一个共享变量v,必须使用同步事件来建立happens-before条件来确保读到期望的写. 变量v的类型为零值时,变量v的初始化行为就像在内存模型中写入一样。(初始化等同于写入) 读写超过机器字(machine word)的行为就和以未指定的顺序执行多个机器字大小的操作一样(超过machine word 每个machine word 读取和写入顺序可能不是期望的) 总之, 想要在一个gorutine中读到另一个gorutine的写就要保证, 写在读之前发生,如果我们不加控制,这个写先于读的顺序就很难保证,所以我们需要使用atomic或者和lock机制来保证顺序保证happens-before 同步 初始化 程序初始化在单个goroutine中运行,但该goroutine可能会创建其他并发运行的goroutine。 如果包p导入包q,则q的init函数的完成时间在任何p的开始之前。 函数main的开始。main在所有init函数完成后发生。 Goroutine创建 启动新goroutine的go语句发生在goroutine开始执行之前。 举例 var a string func f() { print(a) } func hello() { a = "hello, world" go f() } 调用hello将在将来hello, world,可能hello已经返回 Goroutine销毁 goroutine的退出不能保证先发生于任何事件,例如: ...

<span title='2022-06-23 10:33:29 +0800 +0800'>六月 23, 2022</span>

false sharing

false sharing我们一般说的是多核的问题,这个问题主要出现在CPU的缓存上,我们知道CPU有多级缓存,而CPU缓存单单位是行(主流一个缓存行是64Byte, 也就是8个int64) CPU加载缓存 当我们要操作一个变量A时会把A附近的64个字节都加载到缓存行中(空间局部性原理),这样在CPU的缓存里操作时要比在内存中要快 多核CPU缓存问题 在多核心中每个CPU核心都有自己的缓存,如果A和B变量是挨着的, 当CPU1要写A变量, CPU2读变量B, 因为AB挨着,CPU1和CPU2都把它们加载到自己的缓存中,并且AB在同一个缓存行中,CPU1改了A导致了CPU2中B缓存失效,CPUB就得重新从内存中加载缓存 这种情况就是"假共享"/“伪共享”/“false sharing” 说白了就是,CPU各自都有一份,因为邻近变量修改导致了其他核心缓存失效 例子 CASE1 type FS struct { X int64 Y int64 } func share() { var a FS wg := sync.WaitGroup{} wg.Add(2) start := time.Now() go func() { for i := 0; i < 100000000; i++ { a.X++ } wg.Done() }() go func() { for i := 0; i < 100000000; i++ { a.Y++ } wg.Done() }() wg.Wait() fmt.Println(time.Since(start)) } 这种就创造了两个变量挨着在同一缓存行的情况,这个代码在我本机运行时间为305.462012ms CASE 2 我们把上面结构体改一下,X和Y间隔56个字节, 使他们不在一个缓存行, 其他部分不变 type FS struct { X int64 _ [7]int64 Y int64 } 本地运行时间变为201.209402ms, 节省了100ms 注意区分false sharing和data race false sharing讲的是多个核心数据共享缓存失效的问题, 只是影响程序性能,并不影响程序正确性(解决这个问题可以使用内存填充,就像CASE2那样) data race讲的是并发访问同一个变量问题, 会导致意外的情况, 程序不能正确执行(没有安全的数据竞争,发生数据竞争要使用同步原语解决)

<span title='2022-06-22 15:04:38 +0800 +0800'>六月 22, 2022</span>

利特尔法则(等候理论,排队理论)

定义 在一个稳定的系统中,长期的平均顾客人数(L),等于长期的有效抵达率(λ),乘以顾客在这个系统中平均的等待时间(W); 或者,我们可以用一个代数式来表达: $L=λW$ 用白话说的话就是,在W时间内最多排多少人,能够让最后一个人也能在W时间内完成服务,也就是第一个人恰好出去,最后一个人恰好进来,这样最后一个人也能在W时间内出去 案例 就是说L的最后一名也可以在W时间内完成服务,按图上例子来说,就是4分钟内能同时服务多少个顾客 因为顾客的进入速度是2, 所以4分钟内最多也就是8个 如果进如速度是10那么4分钟就是40个 类比服务请求 如果一个请求的响应时长是1s, 系统的QPS是10/s, 那么系统同时处理请求的最佳个数是 10/s * 1s = 10个 如果系统里同时请求数超过10个,那么就会造成响应时间延长

<span title='2022-06-21 22:44:01 +0800 +0800'>六月 21, 2022</span>

指数加权平均(EWA)

EWA是什么 EWA是以指数式递减加权的移动平均, 是一种近似平均(也可以理解为一段时间的平均值,因为越久的数据对当前的影响越小,小到一定程度就可以忽略,可以理解为一段时间的平均值) 基本公式 $V_t=βV_{t-1} + (1-β)R_t$ $V_t$ 代表t时刻的平均值 $βV_{t-1}$代表t-1时刻的平均值 $R_t$ 是t时刻的真实值 $β$ 范围在0-1之间 平均天数为 $N=\frac {1} {1-β}$ $β=0.5$则平均个数N=2 $β=0.9$则平均个数N=$\frac{1}{1-0.9}=10$ 也就是平均最近10次的 可以做什么 计算$\frac {1} {1-β}$个数据的平均值,减少噪声影响,平滑数据 好处比其他平均的好处是 不需要保存最近N次的数据,只需要保存上次计算的平均值

<span title='2022-06-20 17:44:00 +0800 +0800'>六月 20, 2022</span>

hugo 支持github评论

先在utteranc的configuration部分找到安装utteranc app到仓库,选择一个仓库并安装 在页面Enable Utterances部分找到js代码 在自己用的主题上找到关于comment的layout,把js代码添加到里面 运行

<span title='2022-06-20 15:02:41 +0800 +0800'>六月 20, 2022</span>

go sync包之singleflight原理

singlefight是什么 singlefight 直译为"单飞"(雅名到底是啥我也不知道), 顾名思义就是只有一个跑了, 是用来对同一资源控制并发 多个goroutine访问同一个资源时,只有一个goroutine真正的进行访问,其他goroutine等待这一个goroutine返回后共享返回结果 为什么出现singlefight 这个包 上面是什么中已经交代,是为了控制访问同一个资源的并发数,举个例子:假设有个接口访问数据库中id为1的一条数据,如果我们没有控制并发,那么来一百个并发访问这个数据,那么这一百个请求全部取请求数据库(即使有缓存也是全部请求缓存) 如果我们使用了singlefight那么,100个并发讲只有一个请求去数据库,其他99个全部共享那1个返回的结果 怎么用 var g = singleflight.Group{} //初始化了一个singleflight func SharedRes(id int) (int, error) { key := fmt.Sprintf("id:%d", id) //同一个group上,相同key的,只会执行一次,也就是说用key标识一个共享资源 ret, err, _ := g.Do(key, func() (interface{}, error) { //调用共享资源 time.Sleep(time.Second) //这里睡1s是模拟资源执行的延迟 fmt.Println("xxxx") return 1, nil }) return ret.(int), err } func SingleFlight() { wg := sync.WaitGroup{} //为了等100个goroutine执行完,开启了一个WaitGroup for i := 0; i < 100; i++ { wg.Add(1) go func() { //模拟并发 ret, err := SharedRes(1) fmt.Println("ret", ret, err) wg.Done() }() } wg.Wait() //等待100个goroutine执行完 } 看一下打印结果 xxxx <----这里只输出了一次xxxx ret 1 <nil> <----这里是返回结果 ret 1 <nil> ret 1 <nil> ret 1 <nil> .... 我们从现象可以看出,真正的业务逻辑执行只输出了一次,其他的goroutine也都返回了结果 源代码分析 Group结构 type Group struct { mu sync.Mutex // 互斥锁 m map[string]*call // 用来保存共享的调用结构,等会分析这个的作用 } call结构 type call struct { wg sync.WaitGroup //用来让共享groutine等待用的 //我们传入Do的函数签名是 func() (interface{}, error) val interface{} //函数返回结果 err error //函数返回错误 forgotten bool //调用Forget会forgotten=true,并从group.m 中删除这个key对应的call dups int //有几个一起共享结果的 chans []chan<- Result //通过DoChan配合使用,通过返回chan的方式返回结果 } Do函数分析,这个是这个包中最精华部分一之 ...

<span title='2022-06-19 22:02:23 +0800 +0800'>六月 19, 2022</span>

redis分布式锁

redis分布式锁网上方案很多,这里简单的介绍一种 加锁步骤 1.创建锁对象,内部创建一个随机数 2.使用SET KEY VALUE NX EX xxxSecond, 如果成功创建了KEY 则证明加锁成功VALUE 就是第一步创建的随机数 3.如果未能成功加锁需要不断取重试,直到超时或者获取锁 解锁步骤 解锁需要去判断KEY对应的值是否是创建时的随机数,如果不是就不能删除,只有是的时候才能删除,因为如果不是自己的随机数可能是因为锁过期被别人加锁了,不能去删除别人的锁, 检查和删除必须是原子操作,所以我们可以使用lua脚本保证原子操作 if redis.call("GET", KEYS[1])==ARGV[1] then redis.call("DEL", KEYS[1]) return true else return false end 上面的lua脚本很容易懂,就是用来判断key对应的值是否是参数的值,如果是就删除key并返回成功,否则返回失败;失败的情况就是上述说的锁过期被其他程序加锁 优化 加锁是需要不断去重试,访问次数过多可能会给redis造成压力,比如100ms抢一次,一个线程1s钟要请求redis 10次, 如果是10线程抢锁,那么1s就是100次,抢锁的越多就会将redis请求数放大10倍 应对这种情况我们可以考虑,进程内部先去加互斥锁,解锁的时候去解互斥锁, 然后抢到锁的线程再去抢redis锁 优点: 这样如果有两个进程,各有5个线程去抢锁,则实际只有两个线程去访问redis,抢到锁后只有另一个进程的1个线程继续抢,这种已经在生产环境中得到实践 缺点: 造成锁竞争的不公平,同一个进程其他线程更容易抢到锁,因为互斥锁解锁同一个进程的其他线程可以更快的感知 还有一种想法未得到验证,通过redis的发布订阅来改进锁性能 锁过期问题,没有个安全的方法去估计过期时间 针对这种情况,可以考虑锁续期逻辑,比如默认过期时间是30s,我们到20s的时候去延长过期时间 可以考虑使用下面的续期逻辑 if redis.call("GET", KEYS[1]) == ARGV[1] then redis.call("EXPIRE", KEYS[1], ARGV[2]) return true else return false end 先去判断锁是否是自己的,如果是则进行续期 参考资料 redis set命令 从2.6.12版本开始,redis为SET命令增加了一系列选项 EX seconds – 设置键key的过期时间单位时秒 PX milliseconds – 设置键key的过期时间单位时毫秒 NX – 只有键key不存在的时候才会设置key的值 XX – 只有键key存在的时候才会设置key的值 redlock redlock 和 普通的redis lock的区别就是,redlock需要使用多个redis(奇数个),采用大多数原则, 锁住大多数redis就算上锁成功 ...

<span title='2022-06-18 21:24:37 +0800 +0800'>六月 18, 2022</span>

树莓派 连接WIFI

ubuntu 22.04 进入 /etc/netplan/ 文件夹 cd /etc/netplan/ 编辑里面唯一一个文件,大概是:50-cloud-init.yaml sudo vim 50-cloud-init.yaml, 需要使用sudo,因为这个文件是root用户的问题件,或者把文件选项改成可写的 添加WIFI配置 network: ethernets: eth0: dhcp4: true optional: true wifis: # <----添加wifi配置节点 wlan0: dhcp4: true optional: true access-points: "wifi_name": #<---- 这里填写填写你要连接的wifi名称 password: "xxxxx" #<-------这里填写wifi密码 version: 2 执行命令,生成网络配置sudo netplan generate 使网络配置生效sudo netplan apply 树莓派系统 在命令行中输入sudo raspi-config根选项配置即可(注意,可能不支持5G信号)

<span title='2022-06-18 21:15:30 +0800 +0800'>六月 18, 2022</span>