Coder Social home page Coder Social logo

tearupgo's People

Contributors

robintsai avatar

Watchers

 avatar

tearupgo's Issues

翻译并解读 sync/rwmutex.go 源码

使用原则

  • RWMutex 是一个读/写互斥锁。
  • RWMutex 可以被 任意多个读者一个写者 拥有。
  • RWMutex 的零值是一个未上锁的互斥量。
  • RWMutex 在初次使用后,一定不要复制它。
  • 如果一个协程有了 RWMutex 的读权限,此时另一个协程可能调用了 Lock 函数,此时没有协程可以再得到这个锁读的权限,直到这个读操作被释放。即阻塞的 Lock 调用,阻塞了其他读操作获取该锁。这种特性禁止了递归读锁。
  • 如果资源正在被读或写,此时 Lock() 会阻塞其他读写请求直到锁被释放。
  • RLockRUnlock 对应,LockUnlock 对应
  • 不该(能用但不应该用)递归得使用 RLockLock不能 (不能编译通过)被递归使用。
  • 如果一个互斥量未进行 RLock/Lock 就进行 RUnlock/Unlock,则会报一个运行时错误。
  • 可以在一个线程中加锁 RLock/Lock,而另一个线程中释放锁 RUnlock/Unlock

翻译并解读 sync/mutex.go 源码

sync/mutex

使用原则

  • mutex 是个公平的互斥锁。
  • 它的使用后不应该被复制。
  • mutex 有两个模式,正常模式和饥饿模式。
  • 正常模式下,等待者进入一个 FIFO 队列。但首个等待者会和正在进行的协程竞争获取锁。正在进行的协程由于正在 CPU 上运行,所以更有优势,如果它获得了锁,就会排在队列的最前方。
  • 当一个等待者等待超过 1ms 锁的状态就会变成 饥饿 模式。
  • 饥饿模式下,锁的拥有权会总是按队列移交,新来的协程会直接排在队列末尾,且不再自旋(spin)。
  • 饥饿模式下,当队列中只有最后一个等待者等待者的等待时间小于 1ms 时,就会转换为正常模式。
  • 正常模式的性能优于饥饿模式,因为避免了切换线程的开销,正在执行的协程可以多次请求 mutex。
  • 饥饿格式对于尾延迟(tail latency)的病态情况很重要。(??)
  • 当调用 Unlock 时,如果此锁没有被上锁(事先未调用 Lock),会报一个运行时错误。
  • 允许在一个协程中上锁,而在另一个协程中解锁。
  • 锁的自旋(spin):是一种不暂停当前线程,而快速重试获取锁的操作。这里用的是保守的(有条件的)自旋锁。(详见runtime/proc.go@sync_runtime_canSpin

代码解析

基本定义

Mutex 结构上只有两个方法,LockUnlock。同时,将实现这两个方法定义为一个接口 Locker

Mutex 结构有两个属性,state 表示当前状态, sema 表示信号(semaphore)。

type Mutex struct {
	state int32
	sema  uint32
}

state 用一个 int32 类型,存储了两部分内容。一部分是状态,位于二进制的后三位,存了四个状态;另一部分是等待队列,其他位。

state 中标记状态的四个值分别为:

  • 默认零值表示未被上锁(... 000);
  • 常量 mutexLocked 表示被锁定状态,用十进制数 1 表示(... 001);
  • 常量 mutexWoken 表示被唤醒状态,用十进制数 2 表示(... 010);
  • 常量 mutexStarving 表示饥饿状态,用十进制数 4 表示(... 100)。

Lock() 的逻辑

首先,判断当前是否空闲,若是,则直接设置为上锁状态,并退出。执行 atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) 方法,此方法比较 m.state 的值和 0,如果相同,就交换两个值,即设置 m.state1

if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
    return
}

这里省略了内部的 race.Enable 部分,这是用来竞争检测的。详细可以参考 internal/race 包文档介绍,在 build 或 run 时,加上 -race 参数(go run -race xxx.go)可以启用竞争检测。

若非空闲状态,则初始化一些变量,进入以下 for 循环。for 循环中:

  • 若互斥量已上锁,且不在饥饿状态,且可以自旋,则自旋(continue 再次执行 for)一下。这样可以不暂停线程就能再次请求一次锁。
  • 根据旧的状态量(old m.state)准备一下新的状态量(new m.state)
    • 若 old 不是饥饿状态,new 标记为上锁。
    • 若 old 为锁定状态或饥饿状态,new 中队列数量加 1。
    • 如果当前线程到达饥饿条件,且 old 仍为上锁状态,将 new 标记为饥饿。
    • 如果当前线程拿到了它 awoke 状态,则将 new 的唤醒标记位设为 0。
  • 比较 m.state 与 old 的值,没有变化,则可以替换为 new。此为原子操作,正式表示该线程获取了此锁。
    • 如果 old 即没有上锁,也没有饥饿,则直接退出,不再有其他处理。否则走下面。
    • 判断锁定时间 > 1ms或 old 已为饥饿状态,则标记此也为饥饿状态。
    • 若前一个状态为饥饿状态,且队列中只有自己排队,则置为正常状态。

Unlock() 的逻辑

  • 将 m.state 上锁定的位置为 0。
  • 判断当前状态是否在饥饿状态中
    • 若是, runtime_Semrelease(&m.sema, true)(移交给下一个等待者)。
    • 若否
      • 若没有等待者,或被上锁/被唤醒/或饥饿,直接返回(不释放)。
      • 判断 old 未变,则将 m.state 中记录的队列个数 -1,释放锁(runtime_Semrelease(&m.sema, false))。

学到的知识

  • const 中定义的 muexLocked = 1 << iota 在下一行中会延用公式,即下一行变为 1 << 1
  • const 中再次出现 iota 时不再延用公式
  • const 中出现新的 const 时,iota 变成了 0

关于 workpool 实现的一些思考

针对于包: workpool/workpool.go

设计上的思考:

  1. worker 最好不要全部退出,而是剩余固定数量的几个 idle 的,这样可以避免瞬时的大量任务爆发;但这和 p.Wait() 方法冲突,Wait 方法恰好是要等所有的 worker 全部退出。如果设计兼容的话(想法:)可以在 Wait 中做手脚,如在 Wait 调用内设置一个通知状态,所有 worker 中发现有 Wait 调用就需要全部退出,无 Wait 调用就不要全部退出。
  2. worker 没有优先级顺序,它的退出是在 maxIdleDuration 时间内抢不到任务的时候才会退出的,所以在任务执行比较快且 worker 未达到协程上限的时候,理论上 worker 数是多于任务数的(在某时刻)。(但这也无可厚非)
  3. 这里每个 worker 没存 workQueue,但其实也不需要,因为有任务执行时每个 worker 都不会歇着,只要 worker 数量没到上限,worker 是随任务的数量增加的。
  4. 对于上面的 3,反过来说没有 worker.workQueue 反而是一个好的设计,因为有队列的话就有执行的先后顺序,若前面某个任务会消耗很长很长的时间的话,已加入队列的任务将很难得到执行,或者为了解决这个问题需要设计个偷任务的机制(类似Go GMP模型中偷协程的机制)。
  5. 对于任务的执行可以加上 panic 恢复的机制,在底层做会好一点。也可以由调用方自己去保证,还有长时间阻塞在问题亦然。
  6. ElasticBuf 没有内存泄漏问题,之前我还在想底层数组一直被占用得不到释放,但其实在扩容时会发生 copy(扩容就是为切片分配一块新的内存空间并将原切片的元素全部拷贝进去——《Go语言设计与实现》)。
  7. ElasticBuf 若存有大量数据会有性能问题。因为扩容时,会发生新内存申请和拷贝。若存的数据太多,拷贝的性能损失不可忽略。想法:用一个 buf2 做备用的缓存切片,如果数据量过大,就一个仅用于读,另一个做缓存存入,等那个读完后直接赋值交换。这种做法相当于处理了大切片的拷贝,但还有小切片的拷贝频繁的问题,这就要结合预申请空间设计方案了。
  8. 在什么时机进行 spawnOneWorker 的问题上,现在的做法是通过抢占 pool.elasticJobBuf.Out 通道来判断时机,但其实这样打破了任务执行的顺序性,比如在有些要求时序的场景上就会出现问题(如打日志可能会造成后发生的日志先打印),但一律进行排队的话就不好判断 spawnOneWorker 的时机了。想法:可以通过获取 worker 个数结合 idle 状态个数来设计一下这个问题的解决方案。

其他细节:

  1. workpool/internal/sync 包名设计为 ext 或者 util 会更好一些。
  2. AddTask(work IWorkload) 函数中目前有两个 if 分支 if p.GetWaitCount() == 0 { 直接 spawn } else { 进行抢占 Out 判断是否 spawn},其实还应该加一个,在 p.GetWaitCount() == p.workerCount 时只去从 In 进入队列,不需要再判断新开一个 worker 了。

解读Go的入口源代码

Go 入口

  • go version go1.14.1 linux/amd64
  • $GOROOT/src/cmd/go/main.go

go:generate

init()

Go 在加载时先执行 init 函数(所以要想读 main(),要先读懂它),Go 入口文件中有两个 init 函数,一个在文件前面部分,另一个在文件末尾。

init 函数注册了许多 base.Commandbase.Go.Commands 变量中,可以看到 base.Command 是 go 所有命令的一个结构体。(参考src/cmd/go/internal/base/base.go 文件)

base.Go.Commands = []*base.Command{
    bug.CmdBug,
    // ...
}

base.Command 中定义了 command 的 Run, UsageLine, Long 等等,其还有一个关键 Commands []*Command,即它可以链式挂很多命令。

type Command struct {
    // Run runs the command.
    // The args are the arguments after the command name.
    Run func(cmd *Command, args []string)

    // UsageLine is the one-line usage message.
    // The words between "go" and the first flag or argument in the line are taken to be the command name.
    UsageLine string

    // Short is the short description shown in the 'go help' output.
    Short string

    // Long is the long message shown in the 'go help <this-command>' output.
    Long string

    // Flag is a set of flags specific to this command.
    Flag flag.FlagSet

    // CustomFlags indicates that the command will do its own
    // flag parsing.
    CustomFlags bool

    // Commands lists the available commands and help topics.
    // The order here is the order in which they are printed by 'go help'.
    // Note that subcommands are in general best avoided.
    Commands []*Command
}

可以看到 Go 也是一个 Command 类型,上面的命令都挂在了它上

var Go = &Command{
    UsageLine: "go",
    Long:      `Go is a tool for managing Go source code.`,
    // Commands initialized in package main
}

第一次知道,init() 还可以定义两个,下面这个 init() 函数会在第一个 init() 函数之后执行,它只是注册了一下命令的 Usage,下面来读 main() 函数。

main()

src/cmd/go/main.go

首先,有一行 _ = go11tag,发现是此字段是一个 bool 的 true。( // +build go1.1 的作用)

flag.Usage = base.Usage 中这个 base.Usage 是这样定义的 var Usage func()

然后解析输入的参数,判断输入的命令,如果是 help 会输出对应的文档信息。

会检查当前环境的配置是否正确。并且从配置文件中读配置信息。

当检查到可执行的命令时,执行 cmd.Run(cmd, args),最后退出。

关键点

  • 各个 Cmd 是怎么 Load 进来以及怎么设置的。
  • 知道了配置文件所在的位置,可以读一下配置信息 envcmd.MkEnv()
  • cmd.Run(cmd, args) 是关键。
  • 退出时有一个函数队列会顺序执行,它是什么?(atExitFuncs

学到的东西

  1. 通过 cfg 了解到 go envGOENV 指向了一个文件 ~/.config/go/env 它中定义了一些环境变量(非默认的会加在这里)

  2. //go:generate .mkalldocs.sh 的用法(注释与 go 之间不可以有空格)

  3. // +build go1.1 的用法

  4. 函数当做变量去赋值

var Usage func()
flagUsage = func() {}
flagUsage = Usage
  1. main.go 中有两个 init(),是依顺序执行的

  2. 向 stderr 中写数据,用了这样的方式

w := os.Stderr
bw := bufio.NewWriter(w)
bw.Write([]byte("abc"))
bw.Flush()
  1. os.Getwd() 获取当前目录

  2. filepath.Join(dir, "go.mod") 以文件连接符拼接路径

  3. fileinfo := os.Stat() 查看文件状态,返回一个对象,常用的有 fileinfo.Name()fileinfo.IsDir()fileinfo.Mode()(“drwxrwxr-x”)

  4. os.TempDir() 返回 os 的临时目录

  5. filepath.EvalSymlinks(path string) (string, error) 如果 path 是一个软链,则返回的链接文件的真实目录

  6. filepath.IsAbs(path string) bool 判断是否为绝对路径,它里面就是返回了一个 strings.HasPrefix(path, "/")

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.