Go LockOSThread


golang LockOSThread子协程绑定问题

https://www.brendangregg.com/blog/2017-03-16/perf-sched.html

解读Go语言的2018:怎么就在中国火成这样了?

曹春晖:谈一谈 Go 和 Syscall

深入理解 Go scheduler 调度器- GPM 源码分析?

面试必问:Golang高阶-Golang协程实现原理

Golang MGP 调度器

emp3r0r - Linux下的进程注入和持久化

Go 并发之 M,P,G 定义及状态转换

值得收藏调度器系列:GPM 到底是什么?(一)

面试必问:Golang高阶-Golang协程实现原理

Go中的LockOSThread函数用来将Go程与操作系统线程绑定,该函数签名如下:

1
func LockOSThread()

它的主要作用和使用场景如下:

  • 将Go程与操作系统线程绑定后,可以避免Go运行时在Go程间进行线程调度,这对需要长时间占用OS线程或依赖OS线程本地资源的场景很有用。

  • 比如需要长时间利用CPU,如执行CPU密集型任务;或需要访问OS线程本地存储如TLS(Thread Local Storage)。

  • Go web server的每个连接可能会对应一个Go程,如果对每个连接进行CPU密集操作,则将Go程绑定到线程可以避免不必要的上下文切换带来的开销。

示例:

1
2
3
4
5
6
7
8
9
10
11
func handler(w http.ResponseWriter, r *http.Request) {

LockOSThread()

// CPU 密集计算
doCalculation()

// 释放 OS 线程
runtime.UnlockOSThread()

}

这样会将每个请求的Go程与OS线程长时间绑定,进行计算时就不会受Go运行时线程调度影响。

所以总体来说,LockOSThread适用于需要长期占用OS资源或依赖线程本地数据的场景。


Go语言中的runtime.LockOSThreadruntime.UnlockOSThread函数允许开发者将当前的Go协程(goroutine)绑定到一个操作系统线程上。默认情况下,Go的运行时会自由地在可用的操作系统线程之间调度协程,这意味着协程在执行过程中可能会移动到不同的线程。使用runtime.LockOSThread可以防止这种情况,确保协程始终在同一个操作系统线程上执行。

作用

  • 确保协程的线程局部存储(Thread-Local Storage, TLS)稳定:一些操作或库要求在同一个操作系统线程上下文中执行,通过LockOSThread可以满足这一需求。
  • 调用需要与特定线程关联的操作系统或第三方库功能:有些操作系统API或第三方库要求调用者在特定的线程上下文中进行操作,例如GUI库或需要特定线程初始化的库。

使用场景

  • 图形用户界面(GUI)编程:很多GUI框架设计为单线程的,或者要求所有的GUI操作都在主线程(或特定的GUI线程)上执行。在Go中创建和操作GUI元素时,可能需要LockOSThread来确保所有的GUI调用都在同一个线程上执行。
  • 使用特定于线程的本地存储:如果Go代码需要使用线程局部存储(TLS)或者调用依赖于TLS的库,那么LockOSThread可以保证协程始终在同一个线程上执行,从而正确地使用TLS。
  • 性能优化:在某些特定的性能敏感场景下,开发者可能希望控制协程的执行线程来减少上下文切换或实现更细粒度的并发控制。

示例

假设你正在使用一个图形库来开发一个应用,这个库要求所有的图形调用都必须在创建GUI的那个线程上执行。以下是如何使用runtime.LockOSThread来确保这一点的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"runtime"
"your/gui/library" // 假设的GUI库
)

func main() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()

// 初始化GUI库
library.InitGUI()

// 创建和操作窗口等GUI元素
window := library.NewWindow("Example")
window.Show()

// 进入GUI的事件循环
library.MainLoop()
}

在这个例子中,main函数通过调用runtime.LockOSThread确保了所有的GUI操作都在同一个操作系统线程上执行。这是因为main函数启动时运行在一个操作系统线程上,通过锁定它,我们可以满足GUI库的线程要求。

总之,runtime.LockOSThread是一个强大的工具,它在需要精确控制协程执行线程的高级场景中非常有用。然而,它也需要谨慎使用,因为过度使用可能会导致性能问题,比如限制了程序的并行能力。




“性能优化:在某些特定的性能敏感场景下,开发者可能希望控制协程的执行线程来减少上下文切换或实现更细粒度的并发控制。” 针对这一点,能否举一个具体的例子,并给出性能压测

针对性能优化的场景,一个典型的例子是当你需要确保某些任务在同一个操作系统线程上顺序执行以利用线程局部存储(Thread-Local Storage, TLS)或避免线程切换开销时。这种情况下,runtime.LockOSThread能确保当前的Go协程不会被调度到其他线程上执行。

示例场景

假设我们有一个需要频繁访问TLS的任务,或者需要调用某些只能在创建它们的线程上调用的外部库函数(这在调用某些特定的GUI库或系统调用时很常见)。在这种情况下,锁定操作系统线程可以避免潜在的数据竞争或性能下降。

示例代码

假设我们需要调用一个C库函数,该函数要求在同一个线程上初始化和调用。我们可以使用runtime.LockOSThread来确保调用在同一个线程上执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

/*
#include <stdio.h>
void printMessage() {
printf("Hello from C\n");
}
*/
import "C"
import (
"runtime"
"time"
)

func initAndCall() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()

// 假设这里是一些需要在同一个线程上初始化和调用的操作
C.printMessage()

// 模拟一些处理过程
time.Sleep(time.Second)
}

func main() {
go initAndCall()
// 等待goroutine完成
time.Sleep(2 * time.Second)
}

性能压测

对于性能压测,我们可以使用Go的内置测试框架来进行。但请注意,性能测试的关键在于比较有无LockOSThread调用时的性能差异,而不是单独的执行时间。对于上述示例,由于它调用了C函数和time.Sleep,并不直接适合进行性能测试。性能测试应当针对实际的业务逻辑或计算任务进行。

如果你有一个具体的计算密集型任务或高频调用外部库的场景,可以通过编写一个基准测试(benchmark),然后使用go test -bench命令来进行性能测试,比较锁定线程与否的性能差异。

请注意,runtime.LockOSThread的使用是一个较低层次的优化手段,应当在确认它能带来明显好处时才使用。在多数情况下,Go的默认调度器已经能够很好地管理协程的执行,无需手动干预。



“性能优化:在某些特定的性能敏感场景下,开发者可能希望控制协程的执行线程来减少上下文切换或实现更细粒度的并发控制。” 请不要调用C函数,就举一个用纯Go,但是对性能特别敏感的例子

在纯Go的场景下,一个典型的使用runtime.LockOSThread进行性能优化的例子是在处理图形界面(GUI)或游戏开发中,特别是在需要与操作系统级别的图形渲染或事件处理API交互时。虽然这些操作不直接调用C函数,但由于GUI库或渲染循环常常需要在特定的线程上执行,使用runtime.LockOSThread可以确保Go协程与这些需求兼容。

示例场景

考虑一个游戏渲染循环的例子,其中渲染操作必须在主线程上执行(这是许多桌面操作系统和图形API的要求)。我们可以通过锁定一个Go协程到主线程,来确保所有的渲染调用都在正确的线程上执行,同时还能利用Go协程进行游戏逻辑的并行计算。

示例代码

下面是一个简化的示例,演示如何在主线程上执行渲染循环,同时在另一个协程中处理游戏逻辑。这里没有使用具体的GUI库,而是通过打印信息来模拟渲染和逻辑处理的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
"fmt"
"runtime"
"time"
)

func main() {
// 锁定主线程
runtime.LockOSThread()

go gameLogic()

// 在主线程上运行渲染循环
for i := 0; i < 5; i++ {
renderFrame()
time.Sleep(100 * time.Millisecond) // 模拟渲染时间
}
}

func gameLogic() {
for i := 0; i < 10; i++ {
fmt.Println("Game Logic")
time.Sleep(50 * time.Millisecond) // 模拟逻辑处理时间
}
}

func renderFrame() {
fmt.Println("Rendering Frame")
}

性能考量

在这个例子中,runtime.LockOSThread用于确保渲染循环始终在同一个线程(主线程)上执行,这对于一些要求渲染操作在主线程上进行的GUI框架或图形API是必要的。通过将游戏逻辑放在另一个协程中运行,我们可以实现渲染和游戏逻辑的并行处理,从而优化性能。

性能测试

对于这类使用场景,基准测试(benchmarking)可能不如实际的性能分析(profiling)来得直接有效,因为关键的性能指标不仅仅是执行速度,还包括渲染的平滑度和响应时间。使用Go的pprof工具可以帮助开发者识别和优化渲染循环或游戏逻辑中的性能瓶颈。

请注意,这个例子主要是为了说明runtime.LockOSThread的使用场景,并非特定于性能敏感的优化策略。在实际开发中,是否需要使用runtime.LockOSThread取决于具体的库、框架要求,以及应用的性能目标。