关于 go:golang sync.WaitGroup 永远不会完成


golang sync.WaitGroup never completes

我有以下代码,它获取 URL 列表,然后有条件地下载文件并将其保存到文件系统。文件被同时获取,主 goroutine 等待所有文件被获取。但是,在完成所有请求后,程序永远不会退出(并且没有错误)。

我认为正在发生的事情是,不知何故,WaitGroup 中的 go 例程的数量要么增加了太多(通过 Add),要么减少了足够多(没有发生 Done 调用).

我明显做错了什么吗?我将如何检查 WaitGroup 中当前有多少 go 例程,以便更好地调试正在发生的事情?

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package main

import (
   "fmt"
   "io"
   "io/ioutil"
   "net/http"
   "os"
   "strings"
   "sync"
)

func main() {
    links := parseLinks()

    var wg sync.WaitGroup

    for _, url := range links {
        if isExcelDocument(url) {
            wg.Add(1)
            go downloadFromURL(url, wg)
        } else {
            fmt.Printf("Skipping: %v \
", url)
        }
    }
    wg.Wait()
}

func downloadFromURL(url string, wg sync.WaitGroup) error {
    tokens := strings.Split(url,"/")
    fileName := tokens[len(tokens)-1]
    fmt.Printf("Downloading %v to %v \
", url, fileName)

    content, err := os.Create("temp_docs/" + fileName)
    if err != nil {
        fmt.Printf("Error while creating %v because of %v", fileName, err)
        return err
    }

    resp, err := http.Get(url)
    if err != nil {
        fmt.Printf("Could not fetch %v because %v", url, err)
        return err
    }
    defer resp.Body.Close()

    _, err = io.Copy(content, resp.Body)
    if err != nil {
        fmt.Printf("Error while saving %v from %v", fileName, url)
        return err
    }

    fmt.Printf("Download complete for %v \
", fileName)

    defer wg.Done()
    return nil
}

func isExcelDocument(url string) bool {
    return strings.HasSuffix(url,".xlsx") || strings.HasSuffix(url,".xls")
}

func parseLinks() []string {
    linksData, err := ioutil.ReadFile("links.txt")
    if err != nil {
        fmt.Printf("Trouble reading file: %v", err)
    }

    links := strings.Split(string(linksData),",")

    return links
}


这段代码有两个问题。首先,您必须将指向 WaitGroup 的指针传递给 downloadFromURL(),否则该对象将被复制并且 Done()main() 中将不可见。

见:

1
2
3
4
5
func main() {
    ...
    go downloadFromURL(url, &wg)
    ...
}

其次,defer wg.Done() 应该是 downloadFromURL() 中的第一个语句,否则如果您从该语句之前的函数返回,它将不会得到"注册"并且不会被调用。

1
2
3
4
func downloadFromURL(url string, wg *sync.WaitGroup) error {
    defer wg.Done()
    ...
}


Go 中的参数总是按值传递。当参数可能被修改时使用指针。此外,请确保始终执行 wg.Done()。例如,

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package main

import (
   "fmt"
   "io"
   "io/ioutil"
   "net/http"
   "os"
   "strings"
   "sync"
)

func main() {
    links := parseLinks()

    wg := new(sync.WaitGroup)

    for _, url := range links {
        if isExcelDocument(url) {
            wg.Add(1)
            go downloadFromURL(url, wg)
        } else {
            fmt.Printf("Skipping: %v \
", url)
        }
    }
    wg.Wait()
}

func downloadFromURL(url string, wg *sync.WaitGroup) error {
    defer wg.Done()
    tokens := strings.Split(url,"/")
    fileName := tokens[len(tokens)-1]
    fmt.Printf("Downloading %v to %v \
", url, fileName)

    content, err := os.Create("temp_docs/" + fileName)
    if err != nil {
        fmt.Printf("Error while creating %v because of %v", fileName, err)
        return err
    }

    resp, err := http.Get(url)
    if err != nil {
        fmt.Printf("Could not fetch %v because %v", url, err)
        return err
    }
    defer resp.Body.Close()

    _, err = io.Copy(content, resp.Body)
    if err != nil {
        fmt.Printf("Error while saving %v from %v", fileName, url)
        return err
    }

    fmt.Printf("Download complete for %v \
", fileName)

    return nil
}

func isExcelDocument(url string) bool {
    return strings.HasSuffix(url,".xlsx") || strings.HasSuffix(url,".xls")
}

func parseLinks() []string {
    linksData, err := ioutil.ReadFile("links.txt")
    if err != nil {
        fmt.Printf("Trouble reading file: %v", err)
    }

    links := strings.Split(string(linksData),",")

    return links
}

正如@Bartosz 提到的,您需要传递对您的 WaitGroup 对象的引用。他在讨论 defer ws.Done()

的重要性方面做得很好

我喜欢WaitGroup的简洁。但是,我不喜欢我们需要将引用传递给 goroutine,因为这意味着并发逻辑将与您的业务逻辑混合。

所以我想出了这个通用函数来为我解决这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Parallelize parallelizes the function calls
func Parallelize(functions ...func()) {
    var waitGroup sync.WaitGroup
    waitGroup.Add(len(functions))

    defer waitGroup.Wait()

    for _, function := range functions {
        go func(copy func()) {
            defer waitGroup.Done()
            copy()
        }(function)
    }
}

所以你的例子可以这样解决:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func main() {
    links := parseLinks()

    functions := []func(){}
    for _, url := range links {
        if isExcelDocument(url) {
            function := func(url string){
                return func() { downloadFromURL(url) }
            }(url)

            functions = append(functions, function)
        } else {
            fmt.Printf("Skipping: %v \
", url)
        }
    }

    Parallelize(functions...)
}

func downloadFromURL(url string) {
    ...
}

如果你想使用它,你可以在这里找到它 https://github.com/shomali11/util