V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
phpcyy
V2EX  ›  Go 编程语言

求解释一个 Golang 并发 Chan 的问题

  •  
  •   phpcyy · 2020-08-24 20:03:21 +08:00 · 2071 次点击
    这是一个创建于 1575 天前的主题,其中的信息可能已经有所发展或是发生改变。
    func Test_TarsSelect(t *testing.T) {
    	trueCount := 0
    	falseCount := 0
    	for i := 0; i < 100000; i++ {
    		if TarsThread() {
    			trueCount++
    		} else {
    			falseCount++
    		}
    	}
    
    	fmt.Println("trueCount:", trueCount)
    	fmt.Println("falseCount:", falseCount)
    }
    
    func TarsThread() bool {
    	someChan := make(chan bool)
    	var gotChan bool
    
    	go SomeFunc(someChan)
    	select {
        //10 毫秒超时
    	case <-time.After(10 * time.Millisecond):
    	case gotChan = <-someChan:
    	}
    
    	return gotChan
    }
    
    func SomeFunc(someChan chan bool) {
    	//休眠 10 微秒
    	time.Sleep(10 * time.Microsecond)
    	select {
    	case someChan <- true:
    	default:
    	}
    }
    

    如上所示的代码,在我这边执行 Test_TarsSelect 的结果是

    === RUN   Test_TarsSelect
    trueCount: 99961
    falseCount: 39
    

    一般情况下会认为应该全部为 true

    如何解释这种现象呢? golang 中所有并发的程序的先后性都是不保证的吗?

    我认为解决办法是 someChan 初始化时,可以修改为缓冲区长度为 1 。
    另外一个同事认为是将 SomeFunc 中的 default 逻辑去掉,改成 case <- time.After(time.MillionSecond * 10)

    二者都可以解决问题,你们推荐使用哪一种,或者有更合理的解决办法吗?

    17 条回复    2020-08-25 19:32:11 +08:00
    securityCoding
        1
    securityCoding  
       2020-08-24 20:08:15 +08:00
    2
    lance6716
        2
    lance6716  
       2020-08-24 20:27:59 +08:00 via Android   ❤️ 1
    go 也是只有 happens before 的吧,不能依靠其他假设
    whimsySun
        3
    whimsySun  
       2020-08-24 21:30:13 +08:00
    runtime.GOMAXPROCS(1) 你就可以能得到想要的结果了,🐶
    sujin190
        4
    sujin190  
       2020-08-24 23:01:49 +08:00
    并发本来就没有先后,有先后叫啥并发,队列才有先后
    goofool
        5
    goofool  
       2020-08-25 10:25:59 +08:00
    我猜测是 timer 性能问题,我记得 time.After 底层超时前都不会被 GC 回收,我这台电脑定时器精度只有 1ms
    phpcyy
        6
    phpcyy  
    OP
       2020-08-25 10:44:59 +08:00
    @goofool 精度高了才会有上边的问题吧,你在自己电脑上跑一下看看是不是都是 true
    phpcyy
        7
    phpcyy  
    OP
       2020-08-25 11:02:04 +08:00
    @whimsySun 不行的啊,我试了一下仍然出现很多 false
    phpcyy
        8
    phpcyy  
    OP
       2020-08-25 11:07:26 +08:00
    @whimsySun 问题只是缓解了,并没消除,退一步说这也不是解决问题的办法
    yianing
        9
    yianing  
       2020-08-25 13:16:34 +08:00 via Android
    感觉你这是在堵 timer 和 sleep 啊,和 chan 没关系吧
    sunxiansong
        10
    sunxiansong  
       2020-08-25 14:20:29 +08:00
    ```golang
    select {
    //10 毫秒超时
    case <-time.After(500 * time.Microsecond):
    fmt.Println("im here") // <---- 问题在这里
    case gotChan = <-someChan:
    }
    ```

    楼主你大概没发现问题在哪,所以实际上你的 2 个办法都不能解决问题。

    如果你只是想要个超时的话,一般用 context 吧,比如这样
    ```golang

    func ThreadWithContext(ctx context.Context) bool {
    someChan := make(chan bool)
    go SomeFunc(someChan)
    select {
    case <-ctx.Done():
    return false
    case x := <-someChan:
    return x
    }
    }
    ```
    phpcyy
        11
    phpcyy  
    OP
       2020-08-25 15:55:39 +08:00
    @sunxiansong 你这个并不能解决返回 false 的问题啊,我现在是想搞清楚为什么返回 false 的问题,context 的 withTimeout 和 withDeadline 在这里并不影响问题的逻辑。
    sunxiansong
        12
    sunxiansong  
       2020-08-25 16:07:57 +08:00
    @phpcyy 返回 false 是因为 `case <-time.After(10 * time.Millisecond):` ,return 了 gotChan 的默认值 false . goroutine 的调度是不能保证的,说白了你想要的代码还没调度执行到 就超时了(10 * time.Millisecond)

    不清楚你这个代码要实现什么目的,还以为是个简单的超时限制。不知道想做什么怎么用,那么也不知道怎么改,如果只是要 100%返回 true, 那么办法多的是。
    phpcyy
        13
    phpcyy  
    OP
       2020-08-25 18:12:49 +08:00
    @sunxiansong 我这个程序是对一个问题的抽象,可能抽象过度了,我说下原来的场景。

    这个程序原来是在 `go SomeFunc(someChan)` 这一句发送了一个 tcp 请求,然后主程序进入等待响应阶段; SomeFunc 这个 goroutine 会执行 tcp 请求,并将结果写入 someChan 。SomeFunc 里的 `time.Sleep(10 * time.MicroSecond)` 是模拟的请求过程的请求时间。

    这是我们使用的一个框架的底层源代码,我们发现了这个问题,并想着手解决,这就是前因后果,抱歉让你误解了。
    phpcyy
        14
    phpcyy  
    OP
       2020-08-25 18:20:41 +08:00
    @sunxiansong 这里的超时确实是使用 Context 包更好,但是框架的设计者不是这么做的,我们旨在解决请求未超时但是进入了 Timeout 的情况。我认为之所以出现了 false,是因为 SomeFunc 调度的时候,gotChan 还没有进入到读状态,这时候向 someChan 写入,会进入 default 逻辑,进而导致产生了超时。
    phpcyy
        15
    phpcyy  
    OP
       2020-08-25 18:25:05 +08:00
    @yianing 这个超时问题关键在向 chan 写入的时候,读还未就绪,从而进入了 select 中的 default 选项;当读就绪的时候,chan 已经写入过了,从而阻塞在读取状态,直至超时,跟 Sleep 没太大关系啊。
    yianing
        16
    yianing  
       2020-08-25 19:30:37 +08:00 via Android
    @phpcyy 那么是否应该考虑读者没准备好时写的阻塞呢?写者进到 default 消息就直接丢了
    phpcyy
        17
    phpcyy  
    OP
       2020-08-25 19:32:11 +08:00 via iPhone
    @yianing 所以我提出的解决方案是缓冲区或者写的时候设置个超时
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5128 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 07:05 · PVG 15:05 · LAX 23:05 · JFK 02:05
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.