程序不会打印也不会退出,为啥??

victorl · 2018-01-28 02:08:32 · 1291 次点击 · 大约8小时之前 开始浏览    置顶
这是一个创建于 2018-01-28 02:08:32 的主题,其中的信息可能已经有所发展或是发生改变。

package main

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

func main() {
    var ok bool
    cpuNu := runtime.NumCPU()
    for i := 0; i < cpuNu; i++ {
        go func() {
            for {
                if ok {
                    // to do
                }
            }
        }()
    }

    time.Sleep(3 * time.Second)
    fmt.Println("Not print...why???")
}

有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1291 次点击  
加入收藏 微博
8 回复  |  直到 2018-01-29 18:27:39
channel
channel · #1 · 7年之前

死循环里不能啥也不干,会导致 CPU 飙升。

victorl
victorl · #2 · 7年之前
channelchannel #1 回复

死循环里不能啥也不干,会导致 CPU 飙升。

在写个限速的库,原本是用锁来实现的一些操作,后来改成原子操作,跑基准测试的时候发现的。试过其他语言不会这样,我想这种特殊场景暴露出goroutine调度的问题,可能是个坑。

simple
simple · #3 · 7年之前

你这个写法很有趣,要想解释清楚原因,需要对Go的runtime的抢占调度机制有个清晰的认识。
你起了当前运行时可以最大并行运行的goroutine数目,然后让main goroutine sleep之后,就是你for循环中启动的那些goroutines在实际的运行,并且占满了你的cpu core数,当你在goroutine中是一个for死循环,没有任何函数调用
我们知道如果是OS线程的话,OS一般是基于时间片发起抢占调度(通过中断的方式),让运行时间太长的线程调度出cpu,以保证其他线程有机会被执行。同样的go语言的runtime调度器也支持抢占调度(将运行时间过长的goroutine调度出去,避免其一直运行),但由于是在用户态发起的抢占,并不能把当前正在运行的goroutine直接中断掉,所以go runtime调度器是通过为当前正在运行的goroutine设置一个flag标志其应该被抢占了(运行时间太长的原因),然后goroutine是在自己有函数调用的时候,判断一下自己的抢占flag是否被set,如果被set就主动出让CPU。

所以你的代码正好抓住了go runtime调度器的缺点,你可以在你的goroutine中的for死循环中加一个fmt.Print()试验一下。

victorl
victorl · #4 · 7年之前
simplesimple #3 回复

你这个写法很有趣,要想解释清楚原因,需要对Go的runtime的抢占调度机制有个清晰的认识。 你起了当前运行时可以最大并行运行的goroutine数目,然后让main goroutine sleep之后,就是你for循环中启动的那些goroutines在实际的运行,并且占满了你的cpu core数,当你在goroutine中是一个for死循环,**没有任何函数调用**。 我们知道如果是OS线程的话,OS一般是基于时间片发起抢占调度(通过中断的方式),让运行时间太长的线程调度出cpu,以保证其他线程有机会被执行。同样的go语言的runtime调度器也支持抢占调度(将运行时间过长的goroutine调度出去,避免其一直运行),但由于是在用户态发起的抢占,并不能把当前正在运行的goroutine直接中断掉,所以go runtime调度器是通过为当前正在运行的goroutine设置一个flag标志其应该被抢占了(运行时间太长的原因),然后goroutine是在**自己有函数调用**的时候,判断一下自己的抢占flag是否被set,如果被set就主动出让CPU。 所以你的代码正好抓住了go runtime调度器的缺点,你可以在你的goroutine中的for死循环中加一个fmt.Print()试验一下。

并不是有函数调用就行,需要runtime.Gosched()。我贴一下我当时的代码,循环体内有调用函数的。

func (c *RateLimitPerSec) Check() (ok bool) {
    count := c.countRate
    if count < c.rate {
        ok = atomic.CompareAndSwapUint64(&c.countRate, count, count+1)
    }
    return ok
}
       cpuNu := runtime.NumCPU()
    for i := 1; i < cpuNu; i++ {
        go func() {
            for {
                ra.Check()
            }
        }()
    }
wuhanchunqiu
wuhanchunqiu · #5 · 7年之前
victorlvictorl #4 回复

#3楼 @simple 并不是有函数调用就行,需要runtime.Gosched()。我贴一下我当时的代码,循环体内有调用函数的。 ```go func (c *RateLimitPerSec) Check() (ok bool) { count := c.countRate if count < c.rate { ok = atomic.CompareAndSwapUint64(&c.countRate, count, count+1) } return ok } ``` ```go cpuNu := runtime.NumCPU() for i := 1; i < cpuNu; i++ { go func() { for { ra.Check() } }() } ```

要看函数是否会被优化成内联函数,不产生具体的函数调用,是不会发生调度的

victorl
victorl · #6 · 7年之前

上面调用的代码不对,改了一下

cpuNu := runtime.NumCPU()
    for i := 0; i < cpuNu; i++ {
        go func() {
            for {
                ra.Check()
            }
        }()
    }
victorl
victorl · #7 · 7年之前
wuhanchunqiuwuhanchunqiu #5 回复

#4楼 @victorl 要看函数是否会被优化成内联函数,不产生具体的函数调用,是不会发生调度的

去掉内联优化也一样 go build -gcflags="-N -l"

sheepbao
sheepbao · #8 · 7年之前
package main

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

//go:noinline
func addTest(a, b int) int {
    return a + b
}

func call() {
    addTest(1, 3)
}

func main() {
    cpuNu := runtime.NumCPU()

    for i := 0; i < cpuNu; i++ {
        go func() {
            for {
                call() // 改为 addTest(1, 3) 依然不会打印
            }
        }()
    }

    time.Sleep(3 * time.Second)
    fmt.Println("Not print...why???")
}

golang所谓的“有函数调用,就有了进入调度器代码的机会”,实际上是go编译器在函数的入口处插入了runtime.morestack_noctxt的函数调用,这个函数会检查是否扩容连续栈,并进入抢占调度的逻辑中。如果你调用的函数是直接可以确定函数栈的,没有插入runtime.morestack_noctxt,则不会被抢占。知道这些确实要对golang的调度器有很深的了解。

添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传