英文:
Golang fmt.Println() causes game crash
问题
我有一个使用Go语言编写的简单OpenGL程序。
当我编译并运行它时,在主游戏循环中大约进行了9次迭代后,程序崩溃并显示分段错误。
如果我在shouldRender
函数中移除基于时间的逻辑,程序可以进行大约28-29次迭代后崩溃。
如果我在draw
函数中移除对gl.Clear()
的调用,程序可以进行到90次左右的迭代后崩溃。
如果我在shouldRender
中移除对fmt.Println()
的调用,游戏可以正常运行而不会崩溃。(我已经测试了将近2到3分钟,大约有1万帧)
这使我怀疑fmt.Println()
的调用可能导致了分段错误。我是否误读了这些迹象?如果没有,为什么像Println()
这样的核心函数会如此不稳定?
以下是你提供的代码:
package main
import (
f "fmt"
"github.com/go-gl/gl"
glfw "github.com/go-gl/glfw3"
"math"
"time"
)
var (
numRendered = 0
lastDraw = time.Now()
fps = 60
seconds = time.Now()
attr gl.AttribLocation
)
func main() {
if !glfw.Init() {
f.Println("Failed to init glfw")
panic("Cannot initialize glfw library")
}
defer glfw.Terminate()
//glfw.WindowHint(glfw.DepthBits, 16)
window, err := glfw.CreateWindow(300, 300, "Wander", nil, nil)
if err != nil {
panic(err)
}
window.SetFramebufferSizeCallback(reshape)
window.SetKeyCallback(key)
window.MakeContextCurrent()
glfw.SwapInterval(1)
width, height := window.GetFramebufferSize()
reshape(window, width, height)
if gl.Init() != 0 {
panic("Failed to init GL")
}
prog := setupProgram()
defer prog.Delete()
prog.Use()
attr = prog.GetAttribLocation("offset")
setup()
for !window.ShouldClose() {
if shouldRender() {
draw()
}
animate()
window.SwapBuffers()
glfw.PollEvents()
}
}
func setupProgram() (prog gl.Program) {
vertexSource := `
#version 430 core
layout (location = 0) in vec4 offset;
const vec4 vertecies[3] = vec4[3](
vec4(0.25, 0.5, 0.5, 1.0),
vec4(-0.25, 0.5, 0.5, 1.0),
vec4(-0.25, -0.5, 0.5, 1.0)
);
void main(){
gl_Position = vertecies[gl_VertexID] + offset;
}`
fragmentSource := `
#version 430 core
out vec4 color;
void main(){
color = vec4(1.0, 0.0, 0.0, 0.0); // red, blue, green, ??
}`
vert, frag := gl.CreateShader(gl.VERTEX_SHADER), gl.CreateShader(gl.FRAGMENT_SHADER)
defer vert.Delete()
defer frag.Delete()
vert.Source(vertexSource)
frag.Source(fragmentSource)
vert.Compile()
frag.Compile()
prog = gl.CreateProgram()
prog.AttachShader(vert)
prog.AttachShader(frag)
prog.Link()
prog.Use()
f.Println(prog.GetInfoLog())
return
}
func key(window *glfw.Window, k glfw.Key, s int, action glfw.Action, mods glfw.ModifierKey) {
if action != glfw.Press {
return
}
switch glfw.Key(k) {
case glfw.KeyEscape:
window.SetShouldClose(true)
default:
return
}
}
func reshape(window *glfw.Window, width, height int) {
gl.Viewport(0, 0, width, height)
}
func draw() {
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
gl.DrawArrays(gl.TRIANGLES, 0, 3)
}
func shouldRender() bool {
if int(time.Since(lastDraw)*time.Second) >= 1000/fps {
//f.Println("rendering for the ", numRendered, " time")
numRendered++
lastDraw = time.Now()
return true
}
return false
}
func animate() {
now := float64(time.Since(seconds))
offset := [4]float32{
float32(math.Sin(now)),
float32(math.Cos(now)),
0.0, 0.0}
attr.Attrib4fv(&offset)
red := gl.GLclampf(math.Sin(now)*0.25 + 0.75)
blue := gl.GLclampf(math.Cos(now)*0.25 + 0.75)
green := gl.GLclampf(time.Since(seconds))
_ = green
gl.ClearColor(red, blue, 0.2, 0.0)
}
请注意,我只会翻译你提供的代码,不会回答关于翻译的问题。
英文:
I have this simple OpenGL program in Go.
When I compile and run it, the main game-loop gets through about 9 iterations before crashing with a segmentation violation.
rendering for the 0 time
rendering for the 1 time
rendering for the 2 time
rendering for the 3 time
rendering for the 4 time
rendering for the 5 time
rendering for the 6 time
SIGSEGV: segmentation violation
PC=0x7fdab95a0e29
signal arrived during cgo execution
runtime.cgocall(0x414f90, 0x7fdab9887e88)
/usr/lib/go/src/pkg/runtime/cgocall.c:149 +0x11b fp=0x7fdab9887e70
github.com/go-gl/gl._Cfunc_glClear(0xc200004100)
github.com/go-gl/gl/_obj/_cgo_defun.c:340 +0x31 fp=0x7fdab9887e88
github.com/go-gl/gl.Clear(0x4100)
/mnt/data/Dropbox/Coding/Go/src/github.com/go-gl/gl/gl.go:161 +0x25 fp=0x7fdab9887e98
main.draw()
/home/josh/Coding/Go/src/github.com/JoshWillik/Wander/wander.go:120 +0x25 fp=0x7fdab9887eb8
main.main()
/home/josh/Coding/Go/src/github.com/JoshWillik/Wander/wander.go:52 +0x300 fp=0x7fdab9887f48
runtime.main()
/usr/lib/go/src/pkg/runtime/proc.c:220 +0x11f fp=0x7fdab9887fa0
runtime.goexit()
/usr/lib/go/src/pkg/runtime/proc.c:1394 fp=0x7fdab9887fa8
goroutine 3 [syscall]:
runtime.goexit()
/usr/lib/go/src/pkg/runtime/proc.c:1394
rax 0x0
rbx 0x7fdab9887e88
rcx 0x7fdab9887e88
rdx 0x7fdab9887e20
rdi 0x4100
rsi 0xc210001900
rbp 0xc21002a000
rsp 0x7fdab2a4ddd8
r8 0xc210001120
r9 0x7fdab9887e20
r10 0x0
r11 0x286
r12 0x0
r13 0x7fdab9a74000
r14 0x0
r15 0x7fdab2a4e700
rip 0x7fdab95a0e29
rflags 0x10202
cs 0x33
fs 0x0
gs 0x0
If I remove the time-based logic in the shouldRender
function, it makes it to about 28-29 iterations before crashing.
If I remove the call to gl.Clear()
in the draw
function, it lasts into the 90s before crashing.
If I remove the call to fmt.Println()
in shouldRender
, the game runs as expected without crashing. (I've tested it up to about 2 or 3 minutes, so almost 10 thousand frames)
This makes me suspect that the call to fmt.Println()
is somehow responsible for the segmentation violation. Am I misreading the signs? If not, how is a core function like Println()
so unstable?
package main
import (
f "fmt"
"github.com/go-gl/gl"
glfw "github.com/go-gl/glfw3"
"math"
"time"
)
var (
numRendered = 0
lastDraw = time.Now()
fps = 60
seconds = time.Now()
attr gl.AttribLocation
)
func main(){
if !glfw.Init(){
f.Println("Failed to init glfw")
panic("Cannot initialize glfw library")
}
defer glfw.Terminate()
//glfw.WindowHint(glfw.DepthBits, 16)
window, err := glfw.CreateWindow(300, 300, "Wander", nil, nil)
if err != nil{
panic(err)
}
window.SetFramebufferSizeCallback(reshape)
window.SetKeyCallback(key)
window.MakeContextCurrent()
glfw.SwapInterval(1)
width, height := window.GetFramebufferSize()
reshape(window, width, height)
if gl.Init() != 0 {
panic("Failed to init GL")
}
prog := setupProgram()
defer prog.Delete()
prog.Use()
attr = prog.GetAttribLocation("offset")
setup()
for !window.ShouldClose() {
if shouldRender(){
draw()
}
animate()
window.SwapBuffers()
glfw.PollEvents()
}
}
func setupProgram()(prog gl.Program){
vertexSource := `
#version 430 core
layout (location = 0) in vec4 offset;
const vec4 vertecies[3] = vec4[3](
vec4(0.25, 0.5, 0.5, 1.0),
vec4(-0.25, 0.5, 0.5, 1.0),
vec4(-0.25, -0.5, 0.5, 1.0)
);
void main(){
gl_Position = vertecies[gl_VertexID] + offset;
}`
fragmentSource := `
#version 430 core
out vec4 color;
void main(){
color = vec4(1.0, 0.0, 0.0, 0.0); // red, blue, green, ??
}`
vert, frag := gl.CreateShader(gl.VERTEX_SHADER), gl.CreateShader(gl.FRAGMENT_SHADER)
defer vert.Delete()
defer frag.Delete()
vert.Source(vertexSource)
frag.Source(fragmentSource)
vert.Compile()
frag.Compile()
prog = gl.CreateProgram()
prog.AttachShader(vert)
prog.AttachShader(frag)
prog.Link()
prog.Use()
f.Println(prog.GetInfoLog())
return
}
func key(window *glfw.Window, k glfw.Key, s int, action glfw.Action, mods glfw.ModifierKey) {
if action != glfw.Press {
return
}
switch glfw.Key(k){
case glfw.KeyEscape:
window.SetShouldClose(true);
default:
return
}
}
func reshape(window *glfw.Window, width, height int){
gl.Viewport(0, 0, width, height)
}
func draw(){
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
gl.DrawArrays(gl.TRIANGLES, 0, 3)
}
func shouldRender() bool{
if int(time.Since(lastDraw) * time.Second) >= 1000/fps{
//f.Println("rendering for the ", numRendered, " time")
numRendered++
lastDraw = time.Now()
return true
}
return false;
}
func animate(){
now := float64(time.Since(seconds))
offset := [4]float32{
float32(math.Sin(now)),
float32(math.Cos(now)),
0.0,0.0}
attr.Attrib4fv(&offset)
red := gl.GLclampf(math.Sin(now) * 0.25 + 0.75)
blue := gl.GLclampf(math.Cos(now) * 0.25 + 0.75)
green := gl.GLclampf(time.Since(seconds))
_ = green;
gl.ClearColor(red, blue, 0.2, 0.0)
}
答案1
得分: 12
我在我的机器上运行了你的代码——64位Windows上的MinGW-w64。虽然我的带有println的代码运行时间比你的长得多(在崩溃之前超过30,000次调用),但我观察到了相同的行为。
堆栈跟踪报告了对gl函数的调用,这让我有所猜测:也许错误与OpenGL上下文有关。
确实,如果你添加以下代码:
import (
//...
"runtime"
//...
)
和以下代码:
runtime.LockOSThread()
到main函数的顶部,错误就会消失(或者至少在我的机器上,通过锁定线程,它运行了几分钟,显然我不能证明它永远不会崩溃)。
当Goroutine在执行某些任务(如IO)时被阻塞时,Go运行时会偶尔分离出额外的线程来保持程序运行。
我怀疑发生的情况是,有时在调用Println时,Goroutine在系统调用上被阻塞,所以运行时通过在不同的线程上运行主Goroutine来“帮助”你。由于OpenGL上下文绑定到线程,这导致你的程序在GL调用上崩溃,因为你在错误的线程上调用它们。
在main函数的顶部添加runtime.LockOSThread()会强制主Goroutine始终在同一个线程上执行,从而保持所有的GL调用在正确的上下文中。
我应该补充说明,我简要地浏览了fmt包的源代码,从我所看到的来看,我不能证明Goroutine/线程的混乱绝对发生;然而,根据我对Go运行时的了解以及LockOSThread似乎可以修复问题的事实,我强烈怀疑这是问题的原因。
无论如何:当你使用依赖于将上下文绑定到单个线程的C库(如OpenGL或OpenAL)时,请确保始终使用runtime.LockOSThread将其运行的Goroutine锁定到一个线程上。
更新:我在Go调度器上找到了这份文档,如果我理解正确的话,它证实了我的怀疑。诸如打印之类的系统调用可以调用新线程来允许调用的Goroutine在程序在IO上被阻塞时继续执行。
英文:
I ran your code on my machine -- MinGW-w64 on 64-bit Windows. While my code with the println ran much longer than yours (over 30k calls before crashing) I observed the same behavior.
The stack trace reported it on calls to gl functions, this gave me an inkling: perhaps the error was related to the OpenGL context.
Indeed, if you add
import (
//...
"runtime"
//...
)
And the line
runtime.LockOSThread()
To the top of the main function, the error goes away (or at least with the thread locked it ran several minutes on my machine, obviously I can't prove it will never crash).
When Goroutines are blocked doing certain tasks such as IO, the Go runtime will occasionally split off extra threads to keep the program moving.
I suspect that what it happening is that sometimes on a call to Println, the Goroutine becomes blocked on a syscall, so the runtime "helps" you by running your main goroutine on a different thread. Since OpenGL contexts are bound to a thread, this is causing your program to crash on GL calls because you're calling on the wrong thread.
Adding runtime.LockOSThread() to the top of main forces the main Goroutine to always execute on the same thread, and thus keeps all the GL calls in the correct context.
I should add that I briefly skimmed the source the fmt package and from what I saw I can't prove that the goroutine/thread nonsense is definitely happening; however, I strongly suspect that this is the case based on what I know about the Go runtime and the fact that LockOSThread seems to fix it.
Either way: when you're using a C library such as OpenGL or OpenAL that depends on having a context bound to a single thread, make sure to always lock the Goroutine it's running on to one thread with runtime.LockOSThread.
Update: I found this document on the Go scheduler, which, if I'm reading it correctly, confirms my suspicions. Syscalls such as printing can call new threads to be spawned to allow the calling goroutine to continue while the program is blocked on IO.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论