大家来解惑--这段代码有啥问题?

javasgl · 2017-09-17 10:08:16 · 3055 次点击 · 大约8小时之前 开始浏览    置顶
这是一个创建于 2017-09-17 10:08:16 的主题,其中的信息可能已经有所发展或是发生改变。

package main

import (
    "fmt"
    "time"
)

func main() {

    limiter := make(chan bool, 10)
    for i := 0; i < 100; i++ {
        limiter <- true
        go download(i, limiter)
    }
}

func download(index int, limiter chan bool) {
    time.Sleep(1 * time.Second)
    fmt.Println("start to download :", index)
    <-limiter
}

上面这段代码执行结果为什么最大打印值直到 89 ?

当把

time.Sleep(1 * time.Second)
fmt.Println("start to download :", index)

两行上下调换之后,变成以下的代码:

fmt.Println("start to download :", index)
time.Sleep(1 * time.Second)

就能正常打印到 99 了?


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

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

3055 次点击  
加入收藏 微博
32 回复  |  直到 2017-09-21 00:43:38
polaris
polaris · #1 · 8年之前

问题的关键在于 main goroutine 提前退出了。

不论是那种代码,最好在 main 函数最后等待所有 goroutine 执行完成,方法很多了,可以 Sleep,可以 sync.WaitGroup 等等。

javasgl
javasgl · #2 · 8年之前
polarispolaris #1 回复

问题的关键在于 main goroutine 提前退出了。 不论是那种代码,最好在 main 函数最后等待所有 goroutine 执行完成,方法很多了,可以 Sleep,可以 sync.WaitGroup 等等。

但是我只仅仅调换了下 sleep 和 print 语句的先后顺序,就能打印到 99 了。

javasgl
javasgl · #3 · 8年之前

按理说,调换 time 和 print的先后顺序是没法解决 main goroutinue 提前退出的问题的,但是为何程序却能正常完成99的打印?

javasgl
javasgl · #4 · 8年之前

说错了。。这两行代码的先后顺序无关,不论调换前后,结果都不能正常打印到99.

javasgl
javasgl · #5 · 8年之前

现在的问题是:在不使用 WaitGroup 和 在 main 主 goroutinue 强制 sleep 的方式外,怎样保证所有的 download goroutinue 都已经完成?意思是对于这个有缓冲的channel, 最终如何保证所有的goroutinue都已完成?

javasgl
javasgl · #6 · 8年之前

前提是 不能改变代码原有的逻辑:这段代码要实现的逻辑是控制并发数量,限制并发数量不能超过10。 所以在这种场景下,没法使用 sync.WaitGroup。

polaris
polaris · #7 · 8年之前

可以看看 http://books.studygolang.com/gopl-zh/ch8/ch8-06.html 关于控制 goroutine 并发数

polaris
polaris · #8 · 8年之前

供参考:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var (
        wg sync.WaitGroup
        maxGoroutine = 10
        curGNum = 0
    )

    for i := 0; i < 100; i++ {
        if curGNum <= maxGoroutine {
            go func(i int) {
                wg.Add(1)
                defer wg.Done()
                download(i)
            }(i)
        } else {
            download(i)
        }
    }
    wg.Wait()
}

func download(index int) {
    fmt.Println("start to download :", index)
}
javasgl
javasgl · #9 · 8年之前

这个 curGNum的没有自增操作么?如果要在goroutinue里面对curGNum自增操作的话,需要对这个进行加锁吧。

想通了,通过sync.WaitGroupchannel实现了控制并发数,代码如下:

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {

    wg := &sync.WaitGroup{}

    limiter := make(chan bool, 10)
    for i := 0; i < 100; i++ {
        wg.Add(1)
        limiter <- true
        go download(i, limiter, wg)
    }
    wg.Wait()
}

func download(index int, limiter chan bool, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Println("start to download :", index)
    time.Sleep(1 * time.Second)
    <-limiter
}
polaris
polaris · #10 · 8年之前

需要对 curGNum 自增!你这种方式,并没有真正控制 goroutine 数据。

javasgl
javasgl · #11 · 8年之前
polarispolaris #10 回复

需要对 curGNum 自增!你这种方式,并没有真正控制 goroutine 数据。

sync.WaitGroup 负责管理产生的goroutinue,而 channel负责当并发数达到阈值的时候阻塞,从而避免继续产生goroutinue。 这样实现不对?

javasgl
javasgl · #12 · 8年之前
polarispolaris #10 回复

需要对 curGNum 自增!你这种方式,并没有真正控制 goroutine 数据。

前面那段代码,并没有看到curGNum的自增操作啊

polaris
polaris · #13 · 8年之前

需要自增,我漏掉了而已

javasgl
javasgl · #14 · 8年之前
polarispolaris #13 回复

需要自增,我漏掉了而已

我这种 WaitGroup + channel 的实现方式有问题吗?我测试下来发现是可以的,既能控制并发数,又能正常完成所有的 goroutinue

polaris
polaris · #15 · 8年之前
javasgljavasgl #11 回复

#10楼 @polaris sync.WaitGroup 负责管理产生的goroutinue,而 channel负责当并发数达到阈值的时候阻塞,从而避免继续产生goroutinue。 这样实现不对?

你这样可以,刚才没看到 limiter <- true

qclaogui
qclaogui · #16 · 8年之前

有个小问题昂, 在所有worker goroutine们结束之后, 不是应该手动关闭limiter channel吗?

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {

    wg := &sync.WaitGroup{}

    limiter := make(chan bool, 10)
    for i := 0; i < 100; i++ {
        wg.Add(1)
        limiter <- true
        go download(i, limiter, wg)
    }
    wg.Wait()
   close(limiter) //这里
}

func download(index int, limiter chan bool, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Println("start to download :", index)
    time.Sleep(1 * time.Second)
    <-limiter
}
qclaogui
qclaogui · #17 · 8年之前

不好意思代码贴错了,这是我的版本

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {

    wg := &sync.WaitGroup{}

    limiter := make(chan bool, 10)
    for i := 0; i < 100; i++ {
        wg.Add(1)
        limiter <- true
        go download(i, limiter, wg)
    }

    go func() {
        wg.Wait()
        close(limiter)
    }()
}

func download(index int, limiter chan bool, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Println("start to download :", index)
    time.Sleep(1 * time.Second)
    <-limiter
}
javasgl
javasgl · #18 · 8年之前
qclaoguiqclaogui #17 回复

不好意思代码贴错了,这是我的版本 ``` package main import ( "fmt" "sync" "time" ) func main() { wg := &sync.WaitGroup{} limiter := make(chan bool, 10) for i := 0; i < 100; i++ { wg.Add(1) limiter <- true go download(i, limiter, wg) } go func() { wg.Wait() close(limiter) }() } func download(index int, limiter chan bool, wg *sync.WaitGroup) { defer wg.Done() fmt.Println("start to download :", index) time.Sleep(1 * time.Second) <-limiter } ```

在 main goroutinue 之中单独开一条 goroutinue 执行 wait和 close 是出于什么考虑呢?

qclaogui
qclaogui · #19 · 8年之前
javasgljavasgl #18 回复

#17楼 @qclaogui 在 main goroutinue 之中单独开一条 goroutinue 执行 wait和 close 是出于什么考虑呢?

我在go语言圣经里看到这样一段代码,关于并发循环的,

// makeThumbnails6 makes thumbnails for each file received from the channel.
// It returns the number of bytes occupied by the files it creates.
func makeThumbnails6(filenames <-chan string) int64 {
    sizes := make(chan int64)
    var wg sync.WaitGroup // number of working goroutines
    for f := range filenames {
        wg.Add(1)
        // worker
        go func(f string) {
            defer wg.Done()
            thumb, err := thumbnail.ImageFile(f)
            if err != nil {
                log.Println(err)
                return
            }
            info, _ := os.Stat(thumb) // OK to ignore error
            sizes <- info.Size()
        }(f)
    }

    // closer
    go func() {
        wg.Wait()
        close(sizes)
    }()

    var total int64
    for size := range sizes {
        total += size
    }
    return total
}

http://books.studygolang.com/gopl-zh/ch8/ch8-05.html

javasgl
javasgl · #20 · 8年之前

@polaris 大神来解释下,为何要新开单独goroutinue来wait和close而不是直接wait和close? :smiley:

javasgl
javasgl · #21 · 8年之前

如果等待操作被放在了main goroutine中,在循环之前,这样的话就永远都不会结束了,如果在循环之后,那么又变成了不可达的部分,因为没有任何东西去关闭这个channel,这个循环就永远都不会终止。

找到了,文中有说明,为何要这样写

polaris
polaris · #22 · 8年之前

Go 圣经中最后会 range sizes,所以有 close。 这里如果只是:

go func() {
    wg.Wait()
    close(limiter)
}()

则 main goroutine 不是提前退出了吗?

lwldcr
lwldcr · #23 · 8年之前
qclaoguiqclaogui #19 回复

#18楼 @javasgl 我在go语言圣经里看到这样一段代码,关于并发循环的, ``` // makeThumbnails6 makes thumbnails for each file received from the channel. // It returns the number of bytes occupied by the files it creates. func makeThumbnails6(filenames <-chan string) int64 { sizes := make(chan int64) var wg sync.WaitGroup // number of working goroutines for f := range filenames { wg.Add(1) // worker go func(f string) { defer wg.Done() thumb, err := thumbnail.ImageFile(f) if err != nil { log.Println(err) return } info, _ := os.Stat(thumb) // OK to ignore error sizes <- info.Size() }(f) } // closer go func() { wg.Wait() close(sizes) }() var total int64 for size := range sizes { total += size } return total } ``` [http://books.studygolang.com/gopl-zh/ch8/ch8-05.html](http://books.studygolang.com/gopl-zh/ch8/ch8-05.html)

sizes := make(chan int64)

这是无缓冲channel,如果不开goroutine来sync.wait(), makeThumbnails6函数会阻塞在wg.wait()这里等待所有goroutine结束,但是在goroutine中对sizes写数据的操作又会阻塞, 因为是无缓冲channel,而

    for size := range sizes {
        total += size
    }

这个读channel的操作永远得不到执行,因此会形成死锁

但是在你的程序里,在main中直接wg.Wait()即可,不需要另开一个goroutine,否则你的main会很快退出,你可能无法看到输出

lwldcr
lwldcr · #24 · 8年之前
lwldcrlwldcr #23 回复

#19楼 @qclaogui ``` sizes := make(chan int64) ``` 这是无缓冲channel,如果不开goroutine来sync.wait(), makeThumbnails6函数会阻塞在wg.wait()这里等待所有goroutine结束,但是在goroutine中对sizes写数据的操作又会阻塞, 因为是无缓冲channel,而 ``` for size := range sizes { total += size } ``` 这个读channel的操作永远得不到执行,因此会形成死锁 但是在你的程序里,在main中直接wg.Wait()即可,不需要另开一个goroutine,否则你的main会很快退出,你可能无法看到输出

上面格式不对, 编辑一下

sizes := make(chan int64)

这是无缓冲channel,如果不开goroutine来sync.wait(), makeThumbnails6函数会阻塞在wg.wait()这里等待所有goroutine结束,但是在goroutine中对sizes写数据的操作又会阻塞, 因为是无缓冲channel,而

for size := range sizes {
    total += size
}

这个读channel的操作永远得不到执行,因此会形成死锁

但是在你的程序里,在main中直接wg.Wait()即可,不需要另开一个goroutine,否则你的main会很快退出,你可能无法看到输出

javasgl
javasgl · #25 · 8年之前

嗯,确实如此,感觉不论后续是否有其他操作( 除 sleep 强制等待 和 range channel 阻塞 之外),如果在 main 中新开 goroutinuewg.wait的话,都会有可能会导致main 比 负责waitgoroutinue 提前结束。

qclaogui
qclaogui · #26 · 8年之前
polarispolaris #22 回复

Go 圣经中最后会 range sizes,所以有 close。 这里如果只是: ```go go func() { wg.Wait() close(limiter) }() ``` 则 main goroutine 不是提前退出了吗?

嗯嗯,不能这样写 image.png

qq724487380
qq724487380 · #27 · 8年之前
package main

import (
    "fmt"
    "sync"
)

func main() {
    wg := sync.WaitGroup{}
    wg.Add(100)
    limiter := make(chan bool, 10)
    for i := 0; i < 100; i++ {
        limiter <- true
        go download(&wg, i, limiter)
    }
    wg.Wait()
}

func download(wg *sync.WaitGroup, index int, limiter chan bool) {
    fmt.Println("start to download :", index)
    <-limiter
    wg.Done()
}
bucktooth
bucktooth · #28 · 8年之前

请问这样写有不合适的吗?

package main

import (
    "fmt"
    "time"
)

func main() {

    limiter := make(chan bool, 10)
    for i := 0; i < 100; i++ {
        limiter <- true
        go download(i, limiter)
    }
    for i := 0; i < 10; i++ {
        limiter <- true
    }
}

func download(index int, limiter chan bool) {
    time.Sleep(1 * time.Second)
    fmt.Println("start to download :", index)
    <-limiter
}
yuanrr
yuanrr · #29 · 8年之前
package main

import (
    "fmt"
    "sync"
    "time"
)

var wg sync.WaitGroup

func main() {
    limiter := make(chan int, 10)
    wg.Add(20) //这里个数要和下载任务个数一致
    for i := 0; i < 20; i++ {
        limiter <- i
        go download(limiter)
    }
    wg.Wait()
    fmt.Println("结束的收尾工作")
}
func download(limiter chan int) {
    defer wg.Done()
    i, ok := <-limiter
    if !ok {
        fmt.Println("all download")
        return
    }
    time.Sleep(1 * time.Second)
    fmt.Println("start to download :", i)
}
yuanrr
yuanrr · #30 · 8年之前

25C753CA-AC3B-4833-B9E3-AE1412697EBD.png

yuanrr
yuanrr · #31 · 8年之前
yuanrryuanrr #29 回复

```go package main import ( "fmt" "sync" "time" ) var wg sync.WaitGroup func main() { limiter := make(chan int, 10) wg.Add(20) //这里个数要和下载任务个数一致 for i := 0; i < 20; i++ { limiter <- i go download(limiter) } wg.Wait() fmt.Println("结束的收尾工作") } func download(limiter chan int) { defer wg.Done() i, ok := <-limiter if !ok { fmt.Println("all download") return } time.Sleep(1 * time.Second) fmt.Println("start to download :", i) } ```

wg.Add(20)表示要等待通道的个数~~~~

javasgl
javasgl · #32 · 8年之前
bucktoothbucktooth #28 回复

请问这样写有不合适的吗? ``` package main import ( "fmt" "time" ) func main() { limiter := make(chan bool, 10) for i := 0; i < 100; i++ { limiter <- true go download(i, limiter) } for i := 0; i < 10; i++ { limiter <- true } } func download(index int, limiter chan bool) { time.Sleep(1 * time.Second) fmt.Println("start to download :", index) <-limiter } ```

这样写会导致main可能提前退出,因为 每次download 的时候,limiter都在消费,所以最后的limiter未必是10个

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