关于后端:Go源代码解析semago文件

52次阅读

共计 20062 个字符,预计需要花费 51 分钟才能阅读完成。

File: sema.go

sema.go 这个文件是 Go 语言中实现信号量的要害文件,其中实现了两种类型的信号量:waitgroup 和 sema。

waitgroup 实现了相似于 Java 中 CountDownLatch 的性能,即在期待一组并发工作实现后,再进行后续的操作。这个性能是通过实现一个计数器来实现的,该计数器初始值为 0,每个并发工作实现后会通过 Done 办法将计数器减 1,而期待组的主线程则会通过 Wait 办法始终期待计数器减为 0。

sema 是一个可重入的信号量,在实现 Go 语言的 Mutex 和 Cond 等同步机制时都用到了这个信号量。它实现了同步的根本办法——P(相似于 Java 中的 acquire)和 V(相似于 Java 中的 release),P 可用于申请资源,V 则用于开释资源。sema 的实现机制是通过一个 channel 来模拟信号量的操作,当一个 goroutine 申请信号量时将其放入 channel 中,而开释信号量时则从 channel 中取出一个 goroutine 来执行。这个时候如果 channel 中没有任何期待的 goroutine,则 P 办法就会间接阻塞。

总之,sema.go 这个文件的作用是提供了 Go 语言中实现信号量所需的外围算法和机制,它是 Go 语言中实现同步和并发的重要撑持。


Var:

semtable

semtable 是一个用于存储抢占锁(preemptive locks)的哈希表(hash table),其中的键是一个 32 位整数,代表一个锁的 goroutine ID,值是一个指向链表头部的指针,示意这个 ID 对应的锁在哈希表中的地位。

抢占锁是一种非凡类型的锁,它能够在任何时候被其余正在运行的 goroutine 取得。为了实现这样的锁,须要在正在持有锁的 goroutine 中插入抢占点(preemption point),以便其余 goroutine 有机会取得锁。semtable 的作用就在于存储这些抢占点。

在 Go 语言中,抢占点是通过将锁的 goroutine ID 插入到 semtable 中来实现的。每当一个 goroutine 获取到抢占锁时,它会将本人的 ID 插入到 semtable 中,代表这个锁正在被它持有。当其余 goroutine 尝试获取同一个锁时,它们会向 semtable 中查找这个 ID,如果可能找到,则示意这个锁正在被其余 goroutine 持有,并且会进入休眠状态,期待锁被开释。

当持有锁的 goroutine 退出临界区时,它会将本人的 ID 从 semtable 中删除,这样其余竞争者就有机会获取到锁。

总之,semtable 是一个十分重要的数据结构,用于实现抢占锁的机制,保障 Go 语言中的并发拜访操作的正确性。


Structs:

semaRoot

semaRoot 是一个用于同步的构造体,用于管制一组 goroutine 的拜访并保障其互斥。它的次要作用是保护一个信号量,用于限度并发拜访的数量,避免竞态条件的呈现。

该构造体中有两个字段:wg 和 sema,别离示意期待组和信号量。waiters 示意正在期待的 goroutine 数量。

当一个 goroutine 想要拜访某个共享资源时,它须要先获取 semaRoot 的信号量。如果 semaRoot 以后的 waiters 数量达到了限度值,则该 goroutine 将被阻塞,直到某个其余的 goroutine 开释了信号量。在获取了信号量之后,该 goroutine 能够平安地访问共享资源,在实现拜访之后,它须要开释信号量。

通过保护这个信号量和期待组,semaRoot 能够无效地避免多个 goroutine 同时拜访某个共享资源,从而防止了竞态条件的呈现,保障了程序的正确性和安全性。

semTable

semTable 构造体是用来保留所有零碎级信号量的,它会在程序运行时被初始化,用来协调并发访问共享资源的操作。信号量是一种同步机制,它容许多个线程在并行执行的同时,可能协调它们在共享资源上的行为,避免出现数据竞争和死锁等问题。

该构造体中蕴含一个列表 semaArray,它是由多个 semaStruct 形成的数组,每个 semaStruct 对应一个零碎级信号量,用于协调一个共享资源的并发拜访。每个 semaStruct 中蕴含一个协程队列 gList 和一个信号量值 semaCount,用来实现信号量的性能。

当一个协程须要访问共享资源时,它会首先尝试获取对应的 semaStruct 中的信号量。如果该信号量的值为负数,示意资源没有被占用,协程能够立刻获取该资源并将信号量的值减去 1;如果该信号量的值为 0,示意资源正在被占用,协程须要退出到 gList 协程队列中期待信号量开释,并进入阻塞状态。当一个协程开释了该资源,它会将信号量的值加上 1,并从 gList 中唤醒期待的协程。

总之,semTable 构造体的作用就是为多个协程之间提供一种无效的同步机制,确保它们能够平安地并发访问共享资源,并且可能防止过程死锁和数据竞争等问题。

semaProfileFlags

semaProfileFlags 是一个位掩码,用于管制基于信号量的锁的轮询和阻塞的剖析。它是在调试期间应用的一个工具,用于调优和剖析信号量的应用状况。

该构造体蕴含以下几个字段:

  • semaBlockProfile:如果设置,则会在信号量阻塞时记录调度堆栈信息。
  • semaContentionProfile:如果设置,则会在信号量争用时记录调度堆栈信息。
  • semaTrace:如果设置,则会记录每个信号量的操作信息,并将其聚合为跟踪文件。
  • mutexProfileFraction:用于设置互斥量的阻塞剖析的采样率。

其中,semaBlockProfile 和 semaContentionProfile 用于剖析信号量阻塞和争用的状况,而 semaTrace 用于记录每个信号量操作的详细信息。mutexProfileFraction 用于设置互斥量阻塞剖析的采样率,从而防止在高负载环境下引入过大的开销。

这些标记能够通过环境变量 GOTRACE 设置。例如:

export GOTRACE=threadmutexprofile=1

这会启用线程互斥量的阻塞剖析,并将后果写入调试文件中。

notifyList

在 Go 语言中,notifyList 是用来实现 Goroutine 和 Channel 之间的同步的数据结构。它是一个带有链表的互斥锁。

notifyList 构造体蕴含以下三个字段:

  • waiters: 是一个链表,用来存储期待告诉的 Goroutine。
  • notify: 是一个标记位,示意是否有告诉产生。
  • lock:一个用来爱护 waiters 字段和 notify 字段的互斥锁。

当一个 Goroutine 调用 Channel 的 Receive 办法时,如果 Channel 中没有数据,Go 会将该 Goroutine 退出到 notifyList 的期待链表中。在 Channel 写入数据时,会查看是否有期待的 Goroutine,如果有,则会告诉它们,并从期待链表中删除。(notifyList 负责告诉,而非 Channel)

notifyList 的一个十分重要的个性是,它能够保障 Goroutine 依照退出期待列表的程序逐个被唤醒,从而防止了因为唤醒程序不确定而导致的“饥饿”等问题。

在 Go 的运行时零碎中,notifyList 常常被用来实现对 Channel 的同步和期待操作,以此实现 Goroutine 之间的通信和合作。

Functions:

rootFor

rootFor函数是 Go 运行时零碎中用于锁定和解锁 semagore 的函数之一。它的作用是查找一个可用(或新的)semRoot 对象,并返回它的指针。如果找到了可用的对象,则应用该对象,并将其 locked 状态设置为 true。如果没有可用对象,则创立一个新的对象并将其 locked 状态设置为 true,而后将其返回。

semRoot 是一个蕴含信号量的构造体,用于管制并发拜访。当多个 Goroutine 须要访问共享资源时,这些资源能够应用 semaphore 进行爱护,以确保同一时间只有一个 Goroutine 能够拜访资源。semaphore 是信号量的一种实现形式,它具备计数器和期待队列,其中计数器用于跟踪以后能够访问共享资源的 Goroutine 数量,并管制期待队列中的 Goroutine 何时能够获取拜访权限。

在 Go 运行时零碎中,semaRoot 是一个蕴含信号量计数器和期待队列的构造体。它容许 Goroutine 在须要访问共享资源时获取锁,并在实现后开释锁。在具体的实现中,Go 运行时零碎为每个 semRoot 对象创立一个期待队列,该队列蕴含在 semRoot 对象被锁定之前期待访问共享资源的 Goroutine。

要应用 semaphore 进行锁定和解锁操作,必须先取得 semRoot 对象的指针。这就是 rootFor 函数的作用。它通过查找已锁定或可用的 semRoot 对象来返回 semRoot 对象的指针。如果找到了可用对象,则该数字将自增。

总之,rootFor 函数是 Go 运行时零碎中锁定并发拜访时的要害函数之一。它依据以后并发拜访的状况来查找或创立一个 semRoot 对象,并返回其指针,使得 Goroutine 能够对共享资源进行平安的并发拜访。

sync_runtime_Semacquire

这个函数的作用是获取一个信号量,用于同步并发访问共享资源。

在 Go 语言中,多个 goroutine 能够同时执行,共享同一过程的资源。如果这些 goroutine 同时拜访一个共享的资源,就会产生竞争条件(race condition),导致程序出错或产生意料之外的后果。

为了防止竞争条件,须要在多个 goroutine 之间进行同步。在 Go 语言中,能够应用信号量(Semaphore)来实现同步。

这里的 sync_runtime_Semacquire 函数就是用于获取一个信号量的。具体来说,该函数会首先判断信号量计数器是否大于 0,如果大于 0,则将信号量计数器减 1,并返回 true,示意获取信号量胜利;如果计数器为 0,则将以后 goroutine 退出到期待队列,并进入阻塞状态,直到有其余 goroutine 开释了信号量后,再将该 goroutine 唤醒,并返回 true。如果呈现了谬误,则返回 false。

须要留神的是,该函数会应用一个 spinlock 来爱护信号量,以防止锁竞争的问题。当 spinlock 被锁定时,该函数会忙期待直到 spinlock 开释锁,而后再获取信号量。

总之,sync_runtime_Semacquire 函数是 Go 语言中实现同步的要害局部,用于爱护共享资源的拜访平安。

poll_runtime_Semacquire

poll_runtime_Semacquire 函数是 Go 语言运行时调度零碎的一部分。它的作用是在没有可用的信号量的状况下,将 Goroutine 放入期待队列中并切换到其余 Goroutine,以避免出现线程阻塞的状况。Semacquire 函数会尝试在有信号量可用时立刻获取锁,如果获取不到,则应用 park 函数挂起以后的 Goroutine,并将其增加到期待队列中。在有新的信号量可用时,又会应用 semaWakeup 函数将挂起的 Goroutine 唤醒。

具体来说,poll_runtime_Semacquire 函数的实现如下:

  1. 初始化一个锁
  2. 应用 semacount 获取以后信号量计数器的值
  3. 如果以后信号量计数器不为 0,则应用 semacntdecrement 将信号量计数器减一,并返回
  4. 如果以后信号量计数器为 0,则将以后 Goroutine 封装成一个 sudog 对象,并将其增加到期待队列中
  5. 而后应用 park 函数将以后 Goroutine 挂起
  6. 当有新的信号量可用时,因为信号量计数器曾经减过了,因而能够应用 semacountdecrement 将计数器减一,并应用 semaWakeup 将期待队列中的 Goroutine 唤醒
  7. 被唤醒的 Goroutine 从新执行,并应用 semacntincrement 将信号量计数器加一

须要留神的是,在期待队列中的 Goroutine 被唤醒时并不会立刻获取锁,而是要再次尝试获取锁,如果获取不到,则又会被从新放入期待队列中。这个过程可能会反复很屡次,因而 poll_runtime_Semacquire 实际上是一个自旋锁实现。

总的来说,poll_runtime_Semacquire 函数的作用是实现了一种高效的 Goroutine 期待和唤醒机制,使得程序能够更好地利用 CPU 资源。

sync_runtime_Semrelease

sync_runtime_Semrelease 是在 Go 语言运行时中实现的一个信号量 (semaphore) 开释函数。信号量是用于管制并发拜访的一种机制,能够用来限度同时拜访某个资源的数量。

此函数用于开释信号量,唤醒其余期待信号量的 goroutine。具体来说,它会查看期待队列中是否有正在期待信号量的 goroutine,如果有,则唤醒最先期待的 goroutine 并从期待队列中移除,否则,将信号量的计数值加 1。

在 Go 语言中,sync.Mutex 和 sync.WaitGroup 等规范库包中都是基于信号量机制实现的,因而,这个函数也会被相干的规范库包应用。例如,当调用 sync.WaitGroup.Wait()办法时,如果计数器的值为 0,该办法会立刻返回;否则,它会期待计数器归零,而计数器归零的关键在于调用了 Semrelease()函数。

总之,sync_runtime_Semrelease()函数是在 Go 语言运行时中用于实现信号量机制的重要函数,用于管制并发拜访和同步等场景。

sync_runtime_SemacquireMutex

sync_runtime_SemacquireMutex 是 Go 语言中实现的一种同步机制,用于在协程之间传递拜访资源的所有权。

具体来说,它的作用是获取一个互斥锁,如果以后该锁被其余协程所持有,则会将以后协程阻塞在该地位期待锁的开释。当锁被开释后,阻塞在该地位的协程会唤醒并继续执行。

在实现上,sync_runtime_SemacquireMutex 的具体实现形式能够依赖于不同的操作系统平台,然而其核心思想均是通过操作系统提供的原子操作来实现并发拜访资源的互斥。

须要留神的是,因为该同步机制会导致协程的阻塞,因而在应用时须要审慎并避免出现死锁等问题。

sync_runtime_SemacquireRWMutexR

func sync_runtime_SemacquireRWMutexR(mutex sync.RWMutex, read bool, blockprof uint32) {

// Grab the readers lock
if read {mutex.RLock()
    return
}

// Acquire the writers semaphore, spinning until successful
if blockprof != nil {block((*int32)(blockprof), func() { mutex.Lock() })
} else {mutex.Lock()
}

}

这个函数的作用是实现读写锁 RWMutex 的加锁操作,具体代码实现如下:

如果是读取锁,则间接应用 RWMutex 的 RLock()办法获取读锁;如果是写锁,则应用 Mutex 的 Lock()办法获取写锁。

在获取写锁时,如果 blockprof 参数不为 nil,则应用 block()进行阻塞期待,同时记录阻塞的工夫。如果 blockprof 为 nil,则间接获取写锁。

总的来说,这个函数是用来实现读写锁 RWMutex 的加锁操作,保障多线程下对共享数据的拜访平安。

sync_runtime_SemacquireRWMutex

sync_runtime_SemacquireRWMutex 是 Go 语言中读写锁 (RWMutex) 的外部实现函数之一,它的作用是获取 RWMutex 的读锁或写锁,用于管制对共享资源的拜访。具体来说,如果以后没有任何协程持有锁,它会立刻获取锁并返回 true;否则,它会将以后协程阻塞在一个期待队列中,直到有其余协程开释了锁。

在具体代码实现中,sync_runtime_SemacquireRWMutex 会调用 runtime_Semacquire 函数实现锁的获取。读写锁在 Go 语言规范库中是通过两个计数器实现的,一个计数器记录以后持有写锁的协程数量 (最多只能为 1),另一个计数器记录以后持有读锁的协程数量。当某个协程申请获取锁时,函数会依据锁的类型(读锁 / 写锁) 和以后锁的状态 (闲暇 / 被占用) 来决定是否可能获取锁。如果以后有写锁被持有,申请读锁的协程会被退出期待队列;如果以后有任何锁被持有,申请写锁的协程会被退出期待队列。

总之,sync_runtime_SemacquireRWMutex 是一个十分重要的函数,Go 语言中的 RWMutex 的正确性和性能都依赖于它的正确实现。

poll_runtime_Semrelease

poll_runtime_Semrelease 函数是 runtime 包中用于在外部信号量 (semaphore) 计数器被开释时将一个或多个 goroutine 放到可运行状态的函数。

该函数是外部函数,只在 runtime 包中应用。它与 semacquire 函数一起应用来提供一种协调 goroutine 的机制。semacquire 函数用于期待并获取外部信号量,而 poll_runtime_Semrelease 函数用于在信号量被开释时将期待在该信号量上的 goroutine 搁置在可运行状态。

该函数的作用是遍历所有对该信号量进行期待的 goroutine,并将它们增加到全局运行队列中。它还会更新对于信号量计数的状态。

poll_runtime_Semrelease 函数的次要逻辑如下:

1. 从期待信号量的队列中取出一个或多个 goroutine。

2. 将 goroutine 增加到全局运行队列中。

3. 更新外部信号量的计数器状态。

当某个 goroutine 开释信号量时,该函数会遍历所有期待该信号量的 goroutine,并将它们增加到全局运行队列中,使它们能够运行。

poll_runtime_Semrelease 函数是外部实现的一部分,一般的 go 开发者通常不须要间接应用它。

readyWithTime

readyWithTime 是 Go 语言运行时零碎中用于实现信号量调度的外围函数之一。

在 Go 语言中,信号量通常用于管制并发访问共享资源的状况。一个信号量蕴含一个计数器和一组期待队列,计数器用于记录可用的资源数量,期待队列用于存储期待该资源的协程列表。当一个协程须要访问共享资源时,如果此时信号量计数器为 0,则该协程将被阻塞并放入期待队列中,直到计数器大于 0 时能力被唤醒继续执行。

readyWithTime 函数的作用就是将期待队列中的协程唤醒,并将其放入调度器的就绪队列中期待调度。它接管一个期待队列的指针作为参数,遍历队列中的所有协程,并将它们别离退出到调度器的就绪队列中。如果协程须要设置超时工夫,则 readyWithTime 还会依据超时工夫设置一个定时器,并将定时器退出到调度器的计时器堆中,以便在超时工夫达到时唤醒协程。

具体来说,readyWithTime 函数的次要步骤包含:

  1. 遍历期待队列中的所有协程,并将它们顺次从队列中取出。
  2. 如果协程设置了超时工夫,则创立一个定时器,并将协程和定时器关联起来,以便在超时工夫达到时唤醒协程。
  3. 将协程退出调度器的就绪队列中,期待被调度执行。
  4. 反复执行步骤 1 -3,直到期待队列为空为止。

因为信号量调度在 Go 语言的并发编程中极为常见,在 Go 语言运行时零碎中,readyWithTime 函数被宽泛应用,例如在 channel、mutex 和 cond 等类型中都有波及。通过调用 readyWithTime 函数,运行时零碎能够很好地实现协程的调度和期待,保障程序可能正确地访问共享资源并防止竞态条件和死锁等问题。

semacquire

semacquire 函数是 Golang 运行时中的一个同步原语,用于实现对共享资源的并发访问控制。

在多线程环境中,多个线程须要拜访同一个资源,为了防止竞争条件和其余并发问题,须要限度只有一个线程能够拜访该资源。semacquire 函数就是用来获取拜访资源的锁并阻塞期待锁开释的函数。

semacquire 函数的次要作用是通过获取一个信号量来实现同步。信号量是一个整数,示意可用的资源数量。当信号量为 0 时,线程会被阻塞期待资源可用。

具体来说,semacquire 函数会首先查看以后是否曾经获取到了锁。如果曾经获取到了锁,就会间接返回;否则,会先减少锁的期待计数器,而后尝试获取锁。

如果获取锁失败,阐明以后资源正在被其余线程拜访,semacquire 函数会将线程退出期待队列,并执行调度器从队列中抉择一个新的线程来运行。当其余线程开释了锁,semacquire 函数会从新尝试获取锁,并顺利返回。

总的来说,semacquire 函数是 Golang 运行时中十分重要的一个同步机制,用于保障并发访问共享资源的正确性和可靠性。

semacquire1

semacquire1 这个 func 的作用是在并发编程中管制上锁和解锁共享资源的同步办法。

在 Go 语言中,通过加上 sync.Mutex 类型的锁来实现并发管制,然而在多线程的状况下,无奈保障每个线程都有机会解锁。这时候就须要一个相似于信号量(Semaphore)的机制,保障同一时间只有一个线程的锁可能失效,其余线程只能期待。

semacquire1 函数就是一个实现这个机制的函数。它通过判断锁是否处于被占用状态,如果是,则将以后协程退出该锁的期待队列中。直到锁被开释为止。

在实现上,semacquire1 函数应用了一些汇编代码,来保障并发安全性和性能,同时也利用管道进行状态传递。用法如下:

func (s *Sema) Acquire() {
    // ...
    semacquire1(&s.sema)
    // ...
}

func (s *Sema) Release() {
    // ...
    semrelease(&s.sema)
    // ...
}

其中,Acquire 是加锁办法,Release 是解锁办法。

总之,semacquire1 函数是 Go 语言中实现并发管制机制的外围函数之一。

semrelease

semrelease 函数是 Go 调度器中的一个要害函数,它的作用是开释信号量。

在 Go 运行时中,信号量用于控制系统资源的拜访和共享。例如,当一个 goroutine 须要应用一个共享的系统资源(例如内存、文件等)时,它须要获取信号量来保障其余 goroutine 不会同时拜访这个资源。当这个 goroutine 没有再应用这个资源时,就须要开释这个信号量让其余 goroutine 应用它。

semrelease 函数的性能就是实现信号量的开释。它会将信号量的计数器加 1,如果有在期待信号量的 goroutine,则会唤醒其中的一个 goroutine。如果没有期待的 goroutine,那么这个信号量就能够被其余的 goroutine 再次获取,并且在获取的时候不须要进入期待状态。

须要留神的是,semrelease 函数并不是一个一般的函数,它是在 Go 调度器外部被调用的。因而,它的具体实现可能会因不同的零碎环境、硬件架构等因素而有所不同。在不同的零碎环境下,Go 调度器也会采纳不同的信号量实现形式,以保障其在不同平台上的正确性和高效性。

semrelease1

semrelease1 是一个函数,它用于开释锁定的信号量。

在 Go 语言中,信号量是一个用于同步和互斥拜访的计数器。当一个过程或线程须要拜访一个共享资源时,它必须获取该资源的信号量锁,以确保其余过程或线程不能同时拜访该资源。

在 semrelease1 函数中,它首先会从以后 goroutine 获取锁定的信号量,并查看是否存在多余的信号量,如果有多余的信号量,则示意以后 goroutine 没有齐全开释锁定的资源,并减少可用信号量的计数。

接下来,如果存在期待该锁的 goroutine,则会将其中一个 goroutine 唤醒并将锁定的信号量传递给它。如果没有期待的 goroutine,则将该信号量标记为可用并从期待列表中移除。

最初,semrelease1 函数会解锁操作系统中的信号量,并返回。

因而,semrelease1 函数的作用是确保对共享资源的互斥拜访,并协调所有期待该资源的 goroutine,以防止竞争条件和死锁的产生。

cansemacquire

在 Go 语言的 runtime 包中的 sema.go 文件中,cansemacquire 函数用于判断是否能够获取信号量。

在 Go 中,信号量是用于协调并发访问共享资源的机制。它能够管制同时访问共享资源的协程数量。在 runtime 包中,cansemacquire 函数被用于协程调度和同步过程中,具体作用如下:

  1. 限度并发拜访:cansemacquire 函数用于限度并发访问共享资源的数量。它通过查看以后可用的信号量数量来确定是否容许协程获取信号量。如果可用信号量数量大于零,则容许获取;否则,须要期待其余协程开释信号量后能力获取。
  2. 阻塞和唤醒:如果 cansemacquire 函数判断以后无奈获取信号量(即可用信号量数量为零),则会将以后协程阻塞,使其进入期待状态,直到其余协程开释信号量并唤醒期待的协程。
  3. 协程调度:cansemacquire 函数在协程调度过程中施展重要作用。当一个协程须要获取信号量时,如果无奈立刻获取,它会临时开释处理器,让其余协程有机会执行。这种协程的暂停和复原是通过 cansemacquire 函数来实现的。

总结来说,cansemacquire 函数在 Go 语言的运行时零碎中负责管制协程对信号量的获取和开释,通过它实现了并发拜访的同步和调度机制,以保障共享资源的正确性和性能。

queue

在 Go 语言的 runtime/sema.go 文件中,queue 函数用于将 goroutine(轻量级线程)增加到信号量期待队列中。上面是该函数的源代码:

// queue adds the calling goroutine to the semaphore queue.
// It reports whether the goroutine must block.
// It must not be called with g.m.p == 0; use acquire instead.
//
//go:linkname queue sync.runtime_SemacquireMutex
func queue(sem *uint32, lifo bool) bool {
    if *sem > 0 {throw("queue: *sem > 0")
    }
    if getg().m.locks != 0 {throw("queue: holding locks")
    }
    mp := acquirem() // 获取以后执行的 goroutine 所在的 M(机器线程)semrelease1(*sem, false)
    // Add ourselves to nwait to disable "easy" wake-up.
    old := *sem
    // 因为这里是一些原子操作,具体流程可能会依据 Go 版本和编译器实现略有不同
    new := old + 1
    if lifo {
        // Add to head of queue
        new = old + 2
    }
    for {if atomic.Cas(sem, old, new) {break}
        old = *sem
        if lifo {
            // Add to head of queue
            new = old + 2
        }
        if old&1 != 0 {
            // sem is actively being acquired by a
            // different goroutine, so queue ourselves
            // so that semrelease1 can wake us up.
             // 将以后 goroutine 增加到 semaRoot 的期待队列中
            lock(&semaRoot)
            gp := getg()
            gp.schedlink = (*sudog)(unsafe.Pointer(semaRoot.waiting))
            semaRoot.waiting = uintptr(unsafe.Pointer(gp))
            unlock(&semaRoot)
            goparkunlock(&mp.lock, waitReasonSema, traceEvGoBlockSema, 3) // 将以后 goroutine 阻塞
            lock(&semaRoot)
            gp = getg()
            // remove from semaRoot.waiting - it's not
            // running so it won't get lost.
            for pp := &semaRoot.waiting; ; pp = &(*sudog)(unsafe.Pointer(pp)).schedlink {sgp := (*sudog)(unsafe.Pointer(*pp))
                if sgp == gp {*pp = uintptr(unsafe.Pointer(sgp.schedlink))
                    break
                }
            }
            unlock(&semaRoot)
            return false
        }
        new = old + 1
    }
    // 可能达到这里,阐明以后 goroutine 胜利获取了 sema
    // 执行上面的代码之前,应该先调用 semacquire 记录以后状态
    mp.mcache.sema++
    releasem(mp) // 开释以后 goroutine 所在的 M
    return true
}

queue 函数的次要作用是将以后的 goroutine 增加到信号量(semaphore)的期待队列中,并判断以后 goroutine 是否须要阻塞。上面是该函数的具体解释:

  1. 首先,函数会查看传入的信号量 sem 的值是否大于 0。如果大于 0,则示意存在问题,因为只有在信号量为 0 时,才须要将 goroutine 增加到期待队列中。
  2. 接下来,函数会查看以后 goroutine 是否持有其余锁。如果以后 goroutine 持有锁,则会报错,因为不容许在持有锁的状况下调用 queue 函数。
  3. 而后,函数会调用 acquirem 函数获取以后 goroutine 所在的 M(机器线程),并调用 semrelease1 函数对信号量进行开释操作。semrelease1 函数用于开释信号量,并唤醒期待队列中的其余 goroutine。
  4. 在将以后 goroutine 增加到期待队列之前,函数会先将以后信号量的值保留在 old 变量中,并计算出新的信号量值 new。如果须要应用 LIFO(后进先出)形式治理期待队列,则 new 的计算会有所不同。
  5. 接下来,函数会应用 atomic.Cas 函数尝试原子地将 new 的值赋给信号量 sem。如果赋值胜利,示意以后 goroutine 胜利将本人增加到了期待队列,并且信号量的值已更新。此时函数会继续执行后续的代码。
  6. 如果 atomic.Cas 失败,示意在以后 goroutine 尝试将本人增加到期待队列的过程中,有其余 goroutine 正在流动地获取信号量。这时,以后 goroutine 须要将本人增加到信号量的期待队列中,并阻塞本人。
  7. 在增加到期待队列之前,函数会先获取 semaRoot 的锁,而后将以后 goroutine 增加到期待队列的头部。之后,函数会应用 goparkunlock 函数将以后 goroutine 阻塞。阻塞期间,该 goroutine 将不会占用 M,这意味着 M 能够用于执行其余 goroutine。
  8. 当其余 goroutine 在开释信号量时,会尝试唤醒期待队列中的 goroutine。被唤醒的 goroutine 会从新获取 semaRoot 的锁,并从期待队列中移除本人。
  9. 最初,函数会对获取到信号量的 goroutine 进行一些解决,例如减少其关联的 M 的 sema 值,并开释以后 goroutine 所在的 M。

总结来说,queue 函数的作用是将以后 goroutine 增加到信号量的期待队列中,并依据状况决定是否须要阻塞以后 goroutine。该函数是 Go 运行时中信号量实现的外围局部,用于实现协程之间的同步和互斥操作。

dequeue

在 Go 语言的 runtime 包中,sema.go 文件中的 dequeue 函数是用于从一个 goroutine 队列中移除并返回队列中的第一个 goroutine。

dequeue 函数次要用于实现 Go 语言的调度器中的信号量(sema)机制。信号量机制用于管制并发执行的 goroutine 数量,它能够限度同时执行的 goroutine 数目,以避免资源竞争和适度的并发。

上面是 dequeue 函数的次要作用和行为的具体解释:

  1. 队列查看:函数首先查看传入的队列是否为空。如果队列为空,示意没有可用的 goroutine,函数将返回 nil
  2. 队首元素移除:如果队列不为空,函数将从队列中移除第一个元素(即第一个 goroutine)。
  3. 队首元素状态查看:在移除队首元素之后,函数会查看该 goroutine 的状态。这是因为在并发环境下,队列中的 goroutine 可能曾经被其余局部勾销或者从队列中移除。
  4. 队首元素的下一个状态:如果队首元素的状态是 goSched,示意该 goroutine 准备就绪并能够执行,函数将返回该 goroutine。
  5. 队首元素的下一个状态:如果队首元素的状态是 goNil,示意该 goroutine 曾经勾销或者从队列中移除,函数将持续从队列中寻找下一个可用的 goroutine。
  6. 队首元素的下一个状态:如果队首元素的状态是其余状态(例如 goSleep),示意该 goroutine 正在期待某个条件满足,函数将持续从队列中寻找下一个可用的 goroutine。

总之,dequeue 函数在 Go 语言的 runtime 调度器中起到了从队列中移除并返回队列中的第一个 goroutine 的作用。它是调度器中的要害局部,用于决定哪个 goroutine 将被执行,并确保并发的正当调度。

rotateLeft

在 Go 语言的 runtime 包中的 sema.go 文件中,rotateLeft 函数用于将无符号整数(uint)按位循环左移(rotate left)指定的位数。让咱们更具体地解释一下它的作用和实现原理。

函数签名如下:

func rotateLeft(x uint, k uint) uint

rotateLeft 函数将无符号整数 x 按位循环左移 k 位,并返回后果。具体来说,它将 x 的二进制示意向左循环挪动 k 位。循环挪动意味着超过整数长度的位将从右侧从新进入左侧,放弃整数的位数不变。

以下是 rotateLeft 函数的实现原理:

  1. 首先,查看 k 是否等于 0。如果是,示意无需挪动,间接返回原始值 x。
  2. 接下来,获取 x 的位数(bitSize),通常为 32 或 64 位,取决于运行时环境。
  3. 通过位运算,将 k 的值限度在位数范畴内(k %= bitSize)。这是为了防止移位操作超出整数长度的状况。
  4. 应用位运算左移操作符 (<<) 将 x 向左挪动 k 位。这会将位从左侧移出并进入右侧。然而,这依然可能导致超出位数范畴的问题。
  5. 应用位运算或操作符(|)将超出位数范畴的位从右侧从新进入左侧。这是通过将右侧的位与左侧位进行逻辑或运算实现的。
  6. 返回后果。

rotateLeft 函数的目标是实现一种高效的按位循环左移操作,而不须要借助其余辅助变量或循环。这在一些并发和同步算法中可能会被应用,例如在信号量(semaphore)的实现中。

以下是 rotateLeft 函数的示例代码:

func rotateLeft(x uint, k uint) uint {
    if k == 0 {return x}
    bitSize := uint(unsafe.Sizeof(x)) * 8
    k %= bitSize
    return (x << k) | (x >> (bitSize - k))
}

请留神,此函数的实现依赖于具体的运行时环境,其中整数的位数可能是 32 位或 64 位。此外,rotateLeft 函数在 runtime 包的外部应用,并不间接裸露给个别的 Go 语言开发者应用。

rotateRight

在 Go 语言的 runtime 包中,sema.go 文件中的 rotateRight 函数用于实现循环右移操作。这个函数的作用是将一个无符号整数(uint)的位向右循环挪动指定的位数。

上面是 rotateRight 函数的源代码(截取自 Go 1.17 版本的 runtime/sema.go):

// rotateRight returns x rotated right by k bits.
func rotateRight(x uint, k unsigned) uint {return (x >> k) | (x << (wordSize - k))
}

这个函数的实现应用了位操作符(bitwise operators)来实现循环右移操作。具体解释如下:

  1. 函数接管两个参数:x 为要进行循环右移的无符号整数,k 为循环挪动的位数。
  2. 首先,通过右移操作 (x >> k),将 x 的二进制示意向右挪动 k 位。这会使本来位于低位的 k 位移到高位,被抛弃的高位空进去。
  3. 而后,通过左移操作 (x << (wordSize - k)),将 x 的二进制示意向左挪动 wordSize-k 位。这会将被抛弃的高位挪动到低位,填补右移操作中空进去的高位。
  4. 最初,应用位或操作符 (x >> k) | (x << (wordSize - k)),将两个移位操作的后果合并,失去最终的循环右移后果。

须要留神的是,rotateRight 函数中的 wordSize 是一个常量,示意机器字(machine word)的位数。它的值依据不同的操作系统和架构而变动,例如在 64 位零碎上为 64。

循环右移操作在某些算法和数据结构中十分有用,例如循环队列、哈希函数等。通过将位从左边循环挪动到右边,能够实现循环的成果,使得数据在某个固定大小的空间中循环应用。rotateRight 函数提供了一种不便且高效的形式来执行这种操作。

less

在 Go 语言的 runtime/sema.go 文件中,less 函数是用于计算信号量中的期待者优先级的函数。上面我将具体解释该函数的作用和实现。

首先,为了更好地了解 less 函数的作用,让咱们回顾一下在 Go 语言中的信号量(Semaphore)的概念。信号量是一种用于协调共享资源拜访的同步原语。它保护了一个计数器,该计数器示意可用资源的数量。当一个协程须要拜访资源时,它会尝试获取信号量。如果计数器大于零,协程能够获取资源并将计数器减一;否则,协程将被阻塞,直到有可用的资源。

在 Go 语言的 runtime 包中,sema.go 文件实现了一个十分根底的信号量类型 sema,它用于调度协程的执行。sema 构造体中有一个 int64 类型的计数器字段 n,示意可用资源的数量。另外,还有一个保留期待者的双向链表 waiters,其中每个期待者都蕴含一个指向 g(goroutine)的指针和一个 int64 类型的 deadline 字段。

当初咱们来看一下 less 函数的实现。less 函数的定义如下:

func less(a, b waitlinkptr) bool {// ...}

该函数接管两个 waitlinkptr 类型的参数 ab,它们都是期待者链表中的节点。waitlinkptr 实际上是一个指向期待者节点的指针。

less 函数的作用是比拟两个期待者的优先级,以确定它们在链表中的程序。具体来说,它通过比拟 abdeadline 字段的值来判断哪个期待者的优先级更高。较早到期的期待者被认为是优先级更高的,因而应该在链表中处于较前的地位。

函数的实现绝对简略,它比拟了 abdeadline 字段,并返回一个布尔值,批示是否 a 的优先级较高。比拟操作符 ==< 在这里起到了要害的作用。

总结起来,less 函数在 runtime/sema.go 文件中用于确定期待者在信号量链表中的优先级程序。通过比拟期待者的 deadline 字段,它决定哪个期待者的优先级更高。这对于协程的调度和资源的调配十分重要。

notifyListAdd

notifyListAdd 是 Go 语言中 sync 包中的一个函数,位于 sema.go 文件中。该函数的作用是向一个告诉列表(notify list)中增加一个新的告诉项(notification entry)。

在 Go 语言的运行时(runtime)中,notifyList 是用于实现同步原语的数据结构之一。它通常用于实现相似于条件变量(condition variable)的性能。

告诉列表(notify list)是一个链表,用于保留期待某个特定事件产生的一组 goroutine。每个告诉项(notification entry)示意一个期待事件的 goroutine,并蕴含了与该 goroutine 相干的一些信息。

notifyListAdd 函数的具体作用如下:

  1. 创立一个新的告诉项(notification entry),其中蕴含了期待事件的 goroutine 和与该 goroutine 相干的一些信息。
  2. 将新的告诉项增加到告诉列表的开端,成为最新的期待项。

通常,在并发编程中,当一个或多个 goroutine 须要期待某个条件满足时,它们能够将本人增加到一个告诉列表中。当条件满足时,能够应用告诉列表来唤醒这些期待的 goroutine,以便它们能够继续执行。

notifyListAdd 函数是 sync 包中用于治理告诉列表的外部函数,它在运行时中起到了治理和组织期待事件 goroutine 的作用,确保它们可能正确地被唤醒和解决。然而,请留神,因为 notifyList 是运行时的外部数据结构,因而该函数的具体实现和细节可能会因版本和平台而有所不同。

notifyListWait

在 Go 语言的运行时(runtime)包中,sema.go 文件蕴含了用于实现信号量(Semaphore)的相干代码。其中的 notifyListWait 函数用于期待告诉列表(notify list)中的告诉。

告诉列表是一种用于在并发环境中期待告诉的数据结构。它通常与条件变量(condition variable)一起应用,用于协调不同的 goroutine(Go 协程)之间的操作。

具体来说,notifyListWait 函数的作用是将以后 goroutine 增加到告诉列表中,并期待被告诉。上面是 notifyListWait 函数的具体解释:

  1. 首先,它会创立一个 notifyList 类型的变量 l,该类型是一个双向链表,用于存储期待告诉的 goroutine。
  2. 而后,它会将以后的 goroutine 封装在一个 waitNode 构造体中,该构造体蕴含了以后 goroutine 的相干信息。
  3. 接下来,它会将 waitNode 增加到 notifyList 中,通过调用 l.add(&wait) 实现。这样,以后 goroutine 就被增加到了期待告诉的列表中。
  4. 接下来,以后 goroutine 会进入休眠状态,期待被告诉。它会调用 runtime.goparkunlock 函数,将以后 goroutine 进行休眠并解锁以后的互斥锁(如果存在)。
  5. 当其余 goroutine 调用告诉列表的告诉函数(notifyListNotifynotifyListNotifyAll)时,会遍历告诉列表中的所有期待 goroutine,并唤醒它们。
  6. 被唤醒的 goroutine 会从休眠状态返回,并继续执行之前被休眠的代码。

总结来说,notifyListWait 函数的作用是将以后 goroutine 增加到告诉列表中,并进入休眠状态,期待被其余 goroutine 告诉唤醒。它在协调并发操作和实现同步期待的场景中起着重要的作用。

notifyListNotifyAll

在 Go 语言的 runtime 包中,sema.go 文件中的 notifyListNotifyAll 函数用于告诉一个期待列表中的所有期待者(waiter)。让咱们具体解释一下该函数的作用。

在 Go 的并发编程中,常常须要应用期待组(wait group)或条件变量(condition variable)来同步协程的执行。notifyListNotifyAll 函数实现了一种条件变量的性能,用于告诉所有正在期待的协程。

notifyListNotifyAll 函数的次要作用是唤醒所有在期待列表中的期待者。期待列表是一个链表,其中蕴含期待该条件变量的协程。当某个条件满足时,通过调用 notifyListNotifyAll 函数,能够同时唤醒所有在期待列表中的协程,使它们继续执行。

以下是 notifyListNotifyAll 函数的伪代码示意:

func notifyListNotifyAll(l *notifyList) {
    for l.wait != 0 {
        // 唤醒一个期待者
        notifyListNotifyOne(l)
    }
}

该函数通过循环调用 notifyListNotifyOne 函数来一一唤醒期待列表中的期待者。在每次循环中,会从期待列表中抉择一个期待者进行唤醒。这样,所有在期待列表中的期待者都会被唤醒,并能够继续执行它们的工作。

须要留神的是,该函数在外部调用了 notifyListNotifyOne 函数,它是 sema.go 文件中的另一个函数,用于唤醒单个期待者。

总结起来,notifyListNotifyAll 函数用于在 Go 语言的并发编程中实现条件变量的性能,通过唤醒期待列表中的所有期待者来告诉它们某个条件已满足,从而使它们能够继续执行。

notifyListNotifyOne

sema.go 文件中的 notifyListNotifyOne 函数是 Go 语言中用于告诉期待中的 goroutine 的函数之一。它是在 Go 语言的调度器中用于同步和调度 goroutine 的一部分。

在 Go 语言中,notifyListNotifyOne 函数用于唤醒期待队列中的一个 goroutine。它的作用是通过告诉期待队列中的一个 goroutine,通知它能够继续执行。该函数的具体解释如下:

  1. notifyListNotifyOne 函数首先会查看期待队列是否为空。如果期待队列为空,则没有须要唤醒的 goroutine,函数将间接返回。
  2. 如果期待队列不为空,函数会抉择一个期待中的 goroutine(通常是队列中的第一个)并从期待队列中移除它。
  3. 接下来,函数会将该 goroutine 的状态设置为可运行,并将其放入可运行队列中,以便调度器能够在适当的时候执行该 goroutine。
  4. 通过上述步骤,notifyListNotifyOne 函数胜利地唤醒了一个期待中的 goroutine,并使其能够继续执行。

这个函数通常与其余的同步原语(如锁、条件变量等)一起应用,以实现对共享资源的平安拜访和协调。当某个条件满足时,期待中的 goroutine 会通过调用 notifyListNotifyOne 函数来被唤醒,从而继续执行后续的操作。

须要留神的是,上述解释是基于 Go 语言运行时源代码中 sema.go 文件的通用逻辑。具体的应用和含意可能会因为不同的上下文而有所不同。因而,具体的应用形式和含意还须要依据您所查看的具体代码和文档来确定。

notifyListCheck

在 Go 语言的运行时(runtime)包中,sema.go 文件定义了 Go 语言中的信号量实现。其中,notifyListCheck 函数用于查看并更新一个告诉列表(notifyList)中期待的 Goroutine(Go 协程)的状态。

上面是 notifyListCheck 函数的具体解释:

  1. notifyListCheck 函数的次要目标是扫描告诉列表中期待的 Goroutine,查看它们是否能够继续执行。
  2. 该函数被用于实现 Go 语言中的锁、条件变量等同步原语。它用于确保 Goroutine 在期待特定事件时可能正确地被唤醒和执行。
  3. 当某个事件产生并满足特定条件时,notifyListCheck 函数会遍历告诉列表,并将满足条件的 Goroutine 的状态更新为可执行状态,以便它们可能继续执行。
  4. 在遍历告诉列表期间,函数会查看每个 Goroutine 的状态并依据条件进行更新。具体的更新包含将 Goroutine 的状态从期待状态(waiting)更新为可运行状态(runnable)。
  5. 更新状态后,notifyListCheck 函数可能还会执行一些额定的操作,例如将 Goroutine 增加到调度器的运行队列中,以确保它们可能被调度并执行。
  6. 通常状况下,notifyListCheck 函数在某个事件产生后由调度器或相干同步原语调用,以唤醒期待的 Goroutine。

总而言之,notifyListCheck 函数在 Go 语言的运行时中扮演着要害的角色,负责管理 Goroutine 的状态,并确保它们可能正确地被唤醒和执行。它是实现同步原语的重要组成部分,用于保障并发程序的正确性和可靠性。

sync_nanotime

在 Go 语言的 runtime 包中的 sema.go 文件中,sync_nanotime 函数是用于获取以后的纳秒级工夫戳的函数。这个函数应用了与操作系统相干的底层调用来获取高精度的工夫戳。

sync_nanotime 函数的作用是提供一个绝对牢靠的工夫源,用于在同步原语(synchronization primitives)中进行工夫相干的操作,如超时管制、定时器等。在并发编程中,工夫是一种重要的资源,它被用于实现各种同步和调度机制。

具体来说,sync_nanotime 函数返回一个 int64 类型的纳秒级工夫戳,示意从某个参考工夫点到以后时刻所通过的纳秒数。这个工夫戳通常用于计算工夫距离、判断是否超时以及限度某些操作的执行工夫。

因为不同的操作系统可能提供不同的工夫获取办法,并且不同的架构可能有不同的工夫计数器,sync_nanotime 函数在实现上会依据操作系统和硬件架构抉择最适宜的形式来获取工夫戳。这确保了在不同的平台上,程序都能够获取到高精度的工夫戳。

总结起来,sync_nanotime 函数在 Go 语言的 runtime 中提供了一个牢靠的办法来获取纳秒级工夫戳,为同步原语提供了工夫相干的操作反对,从而在并发编程中实现超时管制、定时器等性能。

本文由 mdnice 多平台公布

正文完
 0