本篇文章给大家介绍Golang 标准库 tips之waitgroup的相关知识,包括使用 channel 实现 WaitGroup 的功能介绍,感兴趣的朋友跟随小编一起看看吧

WaitGroup 用于线程同步,很多场景下为了提高并发需要开多个www.qmia.cn协程执行,但是又需要等待多个协程的结果都返回的情况下才进行后续逻辑处理,这种情况下可以通过 WaitGroup 提供的方法阻塞主线程的执行,直到所有的 goroutine 执行完成。
本文目录结构:

WaitGroup 不能被值拷贝
Add 需要在 Wait 之前调用
使用 channel 实现 WaitGroup 的功能
Add 和 Done 数量问题
WaitGroup 和 channel 控制并发数
WaitGroup 和 channel 实现提前退出
WaitGroup 和 channel 返回错误
使用 ErrGroup 返回错误
使用 ErrGroup 实现提前退出
改善版的 Errgroup

WaitGroup 不能被值拷贝

wg 作为一个参数传递的时候,我们在函数中操作的时候还是操作的一个拷贝的变量,对于原来的 wg 是不会改变。
这一点可以从 WaitGroup 实现的源码定义的 struct 能能看出来,WaitGroup 的 struct 就两个字段,第一个字段就是 noCopy,表明这个结构体是不希望直接www.1818xinwen.com被复制的。noCopy 是的实现是一个空的 struct{},主要的作用是嵌入到结构体中作为辅助 vet 工具检查是否通过 copy 赋值这个 WaitGroup 实例,如果有值拷贝的情况,会被检测出来,我们一般的 lint 工具也都能检测出来。
在某些情况下,如果 WaitGroup 需要作为参数传递到其他的方法中,一定需要使用指针类型进行传递。

1

2

3

4

5

6

7

8

9

10

type WaitGroup struct {

    noCopy noCopy

    // 64-bit value: high 32 bits are counter, low 32 bits are waiter count.

    // 64-bit atomic operations require 64-bit alignment, but 32-bit

    // compilers do not ensure it. So we allocate 12 bytes and then use

    // the aligned 8 bytes in them as state, and the other 4 as storage

    // for the sema.

    state1 [3]uint32

}

可以用以下一个例子来说明:

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

// 错误的用法,函数传递 wg 是值拷贝

func main() {

    wg := sync.WaitGroup{}

    wg.Add(10)

    for i := 0; i < 10; i++ {

        go func(i int) {

            do(i, wg)

        }(i)

    }

    wg.Wait()

    fmt.Println("success")

}

func do(i int, wg sync.WaitGroup) { // wg 值拷贝,会导致程序

    fmt.Println(i)

    wg.Done()

}

// 正确的用法,waitgroup 参数传递使用指针的形式

func main() {

    wg := sync.WaitGroup{}

    wg.Add(10)

    for i := 0; i < 10; i++ {

        go func(i int) {

            do(i, &wg)

        }(i)

    }

    wg.Wait()

    fmt.Println("success")

}

func do(i int, wg *sync.WaitGroup) {

    fmt.Println(i)

    wg.Done()

}

Add 需要在 Wait 之前调用

WaitGroup 结构体提供了三个方法,Add、Done、Wait,Add 的作用是用来设置WaitGroup的计数值(子goroutine的数量);Done的作用用来将 WaitGroup 的计数值减 1,其实就是调用Add(-1);Wait 的作用是检测 WaitGroup 计数器的值是否为 0,如果为 0 表示所有的 goroutine 都运行完成,否则会阻塞等待计数器的值为0(所有的 groutine都执行完成)之后www.jsr9.com才运行后面的代码。
所以在 WaitGroup 调用的时候一定要保障 Add 函数在 Wait 函数之前执行,否则可能会导致 Wait 方法没有等到所有的结果运行完成而被执行完。也就是我们不能在 Grountine 中来执行 Add 和 Done,这样可能当前 Grountine 来不及运行,外层的 Wait 函数检测到满足条件然后退出了。

1

2

3

4

5

func main() {

    wg := sync.WaitGroup{}

    wg.Wait() // 直接调用 Wait() 方法是不会阻塞的,因为 wg 中 goroutine 计数器的值为 0

    fmt.Println("success")

}

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

// 错误的写法,在 goroutine 中进行 Add(1) 操作。

// 可能在这些 goroutine 还没来得及 Add(1) 就已经执行 Wait 操作了

func main() {

    wg := sync.WaitGroup{}

    for i := 0; i < 10; i++ {

        go func(i int) {

            wg.Add(1)

            fmt.Println(i)

            wg.Done()

        }(i)

    }

    wg.Wait()

    fmt.Println("success")

}

// 打印的结果,不是我们预期的打印 10 个元素之后再打印 success,而是会随机打印其中的一部分

success

1

0

5

2

// 正确的写法一

func main() {

    wg := sync.WaitGroup{}

    wg.Add(10) // 在 groutine 外层先把需要运行的 goroutine 的数量设置好,保障比 Wait 函数先执行

    for i := 0; i < 10; i++ {

        go func(i int) {

            fmt.Println(i)

            wg.Done()

        }(i)

    }

    wg.Wait()

    fmt.Println("success")

}

// 正确的写法二

func main() {

    wg := sync.WaitGroup{}

    for i := 0; i < 10; i++ {

        wg.Add(1) // 保障比 Wait 函数先执行

        go func(i int) {

            fmt.Println(i)

            wg.Done()

        }(i)

    }

    wg.Wait()

    fmt.Println("success")

}

使用 channel 实现 WaitGroup 的功能

如果想要实现主线程中等待多个协程的结果都返回的情况下才进行后续调用,也可以通过带缓存区的 channel 来实现,实现的思路是需要先知道等待 groutine 的运行的数量,然后初始化一个相同缓存区数量的 channel,在 groutine 运行结束之后往 channel 中放入一个值,并在主线程中阻塞监听获取 channel 中的值全部返回。

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

func main() {

    numGroutine := 10

    ch := make(chan struct{}, numGroutine)

    for i := 0; i < numGroutine; i++ {

        go func(i int) {

            fmt.Println(i)

            ch <- struct{}{}

        }(i)

    }

    for i := 0; i < numGroutine; i++ {

        <-ch

    }

    fmt.Println("success")

}

// 打印结果:

7

5

3

1

9

0

4

2

6

8

success

Add 和 Done 数量问题

需要保障 Add 的数量和 Done 的数量一致,如果 Add 数量小于 Done 数量的情况下,调用 Wait 方法会检测到计数器的值为负数,程序会报 panic;如果 Add 数量大于 Done 的数量,会导致 Wait 循环阻塞后面的代码得不到执行。
Add 数量小于 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

func main() {

    wg := sync.WaitGroup{}

    wg.Add(1) // Add 数量小于 Done 数量

    for i := 0; i < 10; i++ {

        go func(i int) {

            fmt.Println(i)

            wg.Done()

        }(i)

    }

    wg.Wait()

    fmt.Println("success")

}

// 运行结果,有两种结果

结果一:打印部分输出然后退出,这种情况是因为 Done 执行了一个只会,Wait 检测到刚好满足条件然后退出了

1

success

9

5

结果二:执行 Wait 函数的时候,计数器的值已经是负数了

0

9

3

panic: sync: negative WaitGroup counter

Add 数量大于 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

func main() {

    wg := sync.WaitGroup{}

    wg.Add(20)

    for i := 0; i < 10; i++ {

        go func(i int) {

            fmt.Println(i)

            wg.Done()

        }(i)

    }

    wg.Wait()

    fmt.Println("success")

}

// 执行结果:deadlock

0

9

3

7

8

1

4

2

6

5

fatal error: all goroutines are asleep - deadlock!

WaitGroup 和 channel 控制并发数

使用 waitgroup 可以控制一组 groutine 同时运行并等待结果返回之后再进行后续操作,虽然 groutine 对资源消耗比较小,但是大量的 groutine 并发对系统的压力还是比较大,所以这种情况如果需要控制 waitgroup 中 groutine 并发数量控制,就可以使用缓存的 channel 控制同时并发的 groutine 数量。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

func main() {

    wg := sync.WaitGroup{}

    wg.Add(200)

    ch := make(chan struct{}, 10) // 控制最大并发数是 10

 

    for i := 0; i < 200; i++ {

        ch <- struct{}{}

        go func(i int) {

            fmt.Println(i)

            wg.Done()

            <-ch

        }(i)

    }

    wg.Wait()

    fmt.Println("success")

}

根据使用 channel 实现 WaitGroup 的功能的思路,我们上面的代码也可以通过两个 channel 进行改造来实现。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

func main() {

    numGroutine := 200 // 运行的 groutine 总数量

    numParallel := 10  // 并发的 groutine 数量

    chTotal := make(chan struct{}, numGroutine)

    chParallel := make(chan struct{}, numParallel)

    for i := 0; i < 200; i++ {

        chTotal <- struct{}{}

        go func(i int) {

            fmt.Println(i)

            <-chTotal

            chParallel <- struct{}{}

        }(i)

    }

    for i := 0; i < numGroutine; i++ {

        <-chParallel

    }

    fmt.Println("success")

}

WaitGroup 和 channel 实现提前退出

用 WaitGroup 协调一组并发 goroutine 的做法很常见,但 WaitGroup 本身也有其不足:
WaitGroup 必须要等待控制的一组 goroutine 全部返回结果之后才往下运行,但是有的情况下我们希望能快速失败,也就是这一组 goroutine 中只要有一个失败了,那么就不应该等到所有 goroutine 结束再结束任务,而是提前结束以避免资源浪费,这个时候就可以使用 channel 配合 WaitGroup 实现提前退出的效果。

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

func main() {

    wg := sync.WaitGroup{}

    wg.Add(10)

    ch := make(chan struct{}) // 使用一个 channel 传递退出信号

    for i := 0; i < 10; i++ {

        go func(i int) {

            time.Sleep(time.Duration(i) * time.Second)

            fmt.Println(i)

            if i == 2 { // 检测到 i==2 则提前退出

                ch <- struct{}{}

            }

            wg.Done()

        }(i)

    }

    go func() {

        wg.Wait()        // wg.Wait 执行之后表示所有的 groutine 都已经执行完成了,而且没有 groutine 往 ch 传递退出信号

        ch <- struct{}{} // 需要传递一个信号,不然主线程会一直阻塞

    }()

    <-ch // 阻塞等待收到退出信号之后往下执行

    fmt.Println("success")

}

// 打印结果

0

1

2

success

WaitGroup 和 channel 返回错误

WaitGroup 除了不能快速失败之外还有一个问题就是不能在主线程中获取到 groutine 出错时返回的错误,这种情况下就可以用到 channel 进行错误传递,在主线程中获取到错误。

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

76

77

78

79

80

81

// 案例一:groutine 中只要有一个失败了则返回 err 并且回到主协程运行后续代码

func main() {

    wg := sync.WaitGroup{}

    wg.Add(10)

    ch := make(chan error) // 使用一个 channel 传递退出信号

    for i := 0; i < 10; i++ {

        go func(i int) {

            time.Sleep(time.Duration(i) * time.Second)

            if i == 2 { // 检测到 i==2 则提前退出

                ch <- fmt.Errorf("i can't be 2")

                close(ch)

                return

            }

            fmt.Println(i)

            wg.Done()

        }(i)

    }

    go func() {

        wg.Wait() // wg.Wait 执行之后表示所有的 groutine 都已经执行完成了,而且没有 groutine 往 ch 传递退出信号

        ch <- nil // 需要传递一个 nil error,不然主线程会一直阻塞

        close(ch)

    }()

    err := <-ch

    fmt.Println(err.Error())

}

// 运行结果:

/*

0

1

i can't be 2

*/

// 案例二:等待所有的 groutine 都运行完成再回到主线程并捕获所有的 error

func main() {

    wg := sync.WaitGroup{}

    wg.Add(10)

    ch := make(chan error, 10) // 设置和 groutine 数量一致,可以缓冲最多 10 个 error

    for i := 0; i < 10; i++ {

        go func(i int) {

            defer func() {

                wg.Done()

            }()

            time.Sleep(time.Duration(i) * time.Second)

            if i == 2 {

                ch <- fmt.Errorf("i can't be 2")

                return

            }

            if i == 3 {

                ch <- fmt.Errorf("i can't be 3")

                return

            }

            fmt.Println(i)

        }(i)

    }

    wg.Wait() // wg.Wait 执行之后表示所有的 groutine 都已经执行完成了

    close(ch) // 需要 close channel,不然主线程会阻塞

    for err := range ch {

        fmt.Println(err.Error())

    }

}

// 打印结果:

0

1

4

5

6

7

8

9

i can't be 2

i can't be 3

使用 ErrGroup 返回错误

正是由于 WaitGroup 有以上说的一些缺点,Go 团队在实验仓库(golang.org/x)增加了 errgroup.Group 的功能,相比 WaitGroup 增加了错误传递、快速失败、超时取消等功能,相对于通过 channel 和 WaitGroup 组合实现这些功能更方便,也更加推荐。
errgroup.Group 结构体也比较简单,在 sync.WaitGroup 的基础之上包装了一个 error 以及一个 cancel 方法,err 的作用是在 goroutine 出错的时候能够返回,cancel 方法的作用是在出错的时候快速失败。
errgroup.Group 对外暴露了3个方法,WithContext、Go、Wait,没有了 Add、Done 方法,其实 Add 和 Done 是在包装在了 errgroup.Group 的 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

// A Group is a collection of goroutines working on subtasks that are part of

// the same overall task.

//

// A zero Group is valid and does not cancel on error.

type Group struct {

    cancel func()

    wg sync.WaitGroup

    errOnce sync.Once

    err     error

}

func WithContext(ctx context.Context) (*Group, context.Context) {

    ctx, cancel := context.WithCancel(ctx)

    return &Group{cancel: cancel}, ctx

}

// Wait blocks until all function calls from the Go method have returned, then

// returns the first non-nil error (if any) from them.

func (g *Group) Wait() error {

    g.wg.Wait()

    if g.cancel != nil {

        g.cancel()

    }

    return g.err

}

// Go calls the given function in a new goroutine.

//

// The first call to return a non-nil error cancels the group; its error will be

// returned by Wait.

func (g *Group) Go(f func() error) {

    g.wg.Add(1)

    go func() {

        defer g.wg.Done()

        if err := f(); err != nil {

            g.errOnce.Do(func() {

                g.err = err

                if g.cancel != nil {

                    g.cancel()

                }

            })

        }

    }()

}

以下是使用 errgroup.Group 来实现返回 goroutine 错误的例子:

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

func main() {

    eg := errgroup.Group{}

    for i := 0; i < 10; i++ {

        i := i // 这里需要进行赋值操作,不然会有闭包问题,eg.Go 执行的 groutine 会引用 for 循环的 i

        eg.Go(func() error {

            if i == 2 {

                return fmt.Errorf("i can't be 2")

            }

            fmt.Println(i)

            return nil

        })

    }

    if err := eg.Wait(); err != nil {

        fmt.Println(err.Error())

    }

}

// 打印结果

9

6

7

8

3

4

1

5

0

i can't be 2

需要注意的一点是通过 errgroup.Group 来返回 err 只会返回其中一个 groutine 的错误,而且是最先返回 err 的 groutine 的错误,这一点是通过 errgroup.Group 的 errOnce 来实现的。

使用 ErrGroup 实现提前退出

使用 errgroup.Group 实现提前退出也比较简单,调用 errgroup.WithContext 方法获取 errgroup.Group 对象以及一个可以取消的 WithCancel 的 context,并且将这个 context 方法传入到所有的 groutine 中,并在 groutine 中使用 select 监听这个 context 的 Done() 事件,如果监听到了表明接收到了 cancel 信号,然后退出 groutine 即可。需要注意的是 eg.Go 一定要返回一个 err 才会触发 errgroup.Group 执行 cancel 方法。

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

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

// 案例一:通过 groutine 显示返回 err 触发 errgroup.Group 底层的 cancel 方法

func main() {

    ctx := context.Background()

    eg, ctx := errgroup.WithContext(ctx)

    for i := 0; i < 10; i++ {

        i := i // 这里需要进行赋值操作,不然会有闭包问题,eg.Go 执行的 groutine 会引用 for 循环的 i

        eg.Go(func() error {

            select {

            case <-ctx.Done():

                return ctx.Err()

            case <-time.After(time.Duration(i) * time.Second):

            }

            if i == 2 {

                return fmt.Errorf("i can't be 2") // 需要返回 err 才会导致 eg 的 cancel 方法

            }

            fmt.Println(i)

            return nil

        })

    }

    if err := eg.Wait(); err != nil {

        fmt.Println(err.Error())

    }

}

// 打印结果:

0

1

i can't be 2

// 案例二:通过显示调用 cancel 方法通知到各个 groutine 退出

func main() {

    ctx, cancel := context.WithCancel(context.Background())

    eg, ctx := errgroup.WithContext(ctx)

    for i := 0; i < 10; i++ {

        i := i // 这里需要进行赋值操作,不然会有闭包问题,eg.Go 执行的 groutine 会引用 for 循环的 i

        eg.Go(func() error {

            select {

            case <-ctx.Done():

                return ctx.Err()

            case <-time.After(time.Duration(i) * time.Second):

            }

            if i == 2 {

                cancel()

                return nil // 可以不用返回 err,因为手动触发了 cancel 方法

                //return fmt.Errorf("i can't be 2")

            }

            fmt.Println(i)

            return nil

        })

    }

    if err := eg.Wait(); err != nil {

        fmt.Println(err.Error())

    }

}

// 打印结果:

0

1

context canceled

// 案例三:

// 基于 errgroup 实现一个 http server 的启动和关闭 ,以及 linux signal 信号的注册和处理,要保证能够 一个退出,全部注销退出

// https://lailin.xyz/post/go-training-week3-errgroup.html

func main() {

    g, ctx := errgroup.WithContext(context.Background())

    mux := http.NewServeMux()

    mux.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {

        w.Write([]byte("pong"))

    })

    // 模拟单个服务错误退出

    serverOut := make(chan struct{})

    mux.HandleFunc("/shutdown", func(w http.ResponseWriter, r *http.Request) {

        serverOut <- struct{}{}

    })

    server := http.Server{

        Handler: mux,

        Addr:    ":8080",

    }

    // g1

    // g1 退出了所有的协程都能退出么?

    // g1 退出后, context 将不再阻塞,g2, g3 都会随之退出

    // 然后 main 函数中的 g.Wait() 退出,所有协程都会退出

    g.Go(func() error {

        return server.ListenAndServe()

    })

    // g2

    // g2 退出了所有的协程都能退出么?

    // g2 退出时,调用了 shutdown,g1 会退出

    // g2 退出后, context 将不再阻塞,g3 会随之退出

    // 然后 main 函数中的 g.Wait() 退出,所有协程都会退出

    g.Go(func() error {

        select {

        case <-ctx.Done():

            log.Println("errgroup exit...")

        case <-serverOut:

            log.Println("server will out...")

        }

        timeoutCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second)

        // 这里不是必须的,但是如果使用 _ 的话静态扫描工具会报错,加上也无伤大雅

        defer cancel()

        log.Println("shutting down server...")

        return server.Shutdown(timeoutCtx)

    })

    // g3

    // g3 捕获到 os 退出信号将会退出

    // g3 退出了所有的协程都能退出么?

    // g3 退出后, context 将不再阻塞,g2 会随之退出

    // g2 退出时,调用了 shutdown,g1 会退出

    // 然后 main 函数中的 g.Wait() 退出,所有协程都会退出

    g.Go(func() error {

        quit := make(chan os.Signal, 0)

        signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)

        select {

        case <-ctx.Done():

            return ctx.Err()

        case sig := <-quit:

            return errors.Errorf("get os signal: %v", sig)

        }

    })

    fmt.Printf("errgroup exiting: %+v\n", g.Wait())

}

改善版的 Errgroup

使用 errgroup.Group 的 WithContext 我们注意到在返回 eg 对象的同时还会返回另外一个可以取消的 context 对象,这个 context 对象的功能就是用来传递到 eg 需要同步的 groutine 中有一个发生错误时取消整个同步的 groutine,但是有不少同学可能会不经意将这个 context 传到其他的非 eg 同步的业务代码groutine 中,这样会导致非关联的业务代码莫名其妙的收到 cancel 信息,类似如下的写法:

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

func main() {

    ctx := context.Background()

    eg, ctx := errgroup.WithContext(ctx)

    for i := 0; i < 10; i++ {

        i := i // 这里需要进行赋值操作,不然会有闭包问题,eg.Go 执行的 groutine 会引用 for 循环的 i

        eg.Go(func() error {

            select {

            case <-ctx.Done():

                return ctx.Err()

            case <-time.After(time.Duration(i) * time.Second):

            }

            if i == 2 {

                return fmt.Errorf("i can't be 2") // 需要返回 err 才会导致 eg 的 cancel 方法

            }

            fmt.Println(i)

            return nil

        })

    }

    if err := eg.Wait(); err != nil {

        fmt.Println(err.Error())

    }

    OtherLogic(ctx)

}

func OtherLogic(ctx context.Context) {

    // 这里的 context 用了创建 eg 返回的 context,这个 context 可能会往后面更多的 func 中传递

    // 如果在该方法或者后面的 func 中有对 context 监听取消型号,会导致这些 context 被取消了

}

另外不管是 WaitGroup 还是 errgroup.Group 都不支持控制最大并发限制以及 panic 恢复的功能,因为我们不能保障我们通过创建的 groutine 不会出现异常,如果没有在创建的协程中捕获异常,会直接导致整个程序退出,这是非常危险的。
这里推荐一下 bilbil 开源的微服务框架 go-kratos/kratos 自己实现了一个改善版本的 errgroup.Group,其实现的的思路是利用 channel 来控制并发,并且创建 errgroup 的时候不会返回 context 避免 context 往非关联的业务方法中传递。

到此这篇关于Golang 标准库 tips之waitgroup详解的文章

Golang 标准库 tips之waitgroup详解相关推荐

  1. Python 标准库之 sys 模块详解

    Python sys 模块详解 1. 简介 "sys"即"system","系统"之意.该模块提供了一些接口,用于访问 Python 解释器 ...

  2. 用于生成随机数的python标准库模块是_详解Python基础random模块随机数的生成

    详解Python基础random模块随机数的生成 来源:中文源码网    浏览: 次    日期:2019年11月5日 [下载文档:  详解Python基础random模块随机数的生成.txt ] ( ...

  3. python shutil_Python标准库shutil用法实例详解

    本文实例讲述了Python标准库shutil用法.分享给大家供大家参考,具体如下: shutil模块提供了许多关于文件和文件集合的高级操作,特别提供了支持文件复制和删除的功能. 文件夹与文件操作 co ...

  4. 【python标准库】os.path详解

    文章目录 os中的path 输入为路径字符串的单参函数 与文件信息相关的单参函数 输入为多个参数的函数 os中的path 查看源码会看到,在os.py中有这样几行 if 'posix' in _nam ...

  5. Python标准库--time模块的详解

    time模块 - - -时间获取和转换 在我们学习time模块之前需要对以下的概念进行了解: 时间戳:时间戳是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08 ...

  6. c语言程序如何调用标准库函数,如何调用C标准库的exit函数详解

    编译大于运算符 原定的计划中这一篇应当是要讲如何编译if表达式的,但是我发现没什么东西可以作为if的test-form的部分的表达式,所以觉得,要不还是先实现一下比较两个数字这样子的功能吧.说干就干, ...

  7. c语言stl模板,c/c++开发分享C++ 标准模板库 STL 顺序容器详解

    c++ 标准模板库 stl 顺序容器 容器 顺序性 重复性 支持迭代器 vector 动态数组 无序 可重复 随机访问迭代器 deque 双向队列 无序 可重复 随机访问迭代器 list 双向链表 无 ...

  8. Python中第三方库Requests库的高级用法详解

    Python中第三方库Requests库的高级用法详解 虽然Python的标准库中urllib2模块已经包含了平常我们使用的大多数功能,但是它的API使用起来让人实在感觉不好.它已经不适合现在的时代, ...

  9. python excel库pip install_超全整理|Python 操作 Excel 库 xlwings 常用操作详解!

    原标题:超全整理|Python 操作 Excel 库 xlwings 常用操作详解! 来源:早起Python 作者:陈熹.刘早起 大家好,我是早起. 在之前的文章中我们曾详细的讲解了如何使用openp ...

  10. Python|线程和进程|阻塞|非阻塞|同步|异步|生成器和协程|资源竞争|进程间通信|aiohttp库|daemon属性值详解|语言基础50课:学习(11)

    文章目录 系列目录 原项目地址 第34课:Python中的并发编程-1 线程和进程 多线程编程 使用 Thread 类创建线程对象 继承 Thread 类自定义线程 使用线程池 守护线程 资源竞争 G ...

最新文章

  1. 中国人长期“霸榜”GitHub,国外开发者发文控诉
  2. 【BZOJ3314】 [Usaco2013 Nov]Crowded Cows 单调队列
  3. git tag 功能笔记
  4. iOS学习笔记之typedef
  5. ReviewForJob——二叉堆优先队列的实现(三种堆节点类型——int + struct HeapNode + struct HeapNode*)
  6. 记录一次nginx配置vhost的小bug
  7. Kubernetes 入门教程
  8. PowerDesigner 数据建模技术视频教程
  9. ES6(一) —— 异步编程解决办法[从回调函数到promise,generator,async]
  10. 添加Android手机 CA系统根证书
  11. 什么情况下使用10分钟邮箱?8个临时邮箱推荐
  12. IIS的ISAPI接口
  13. 用python做乘法口诀表_如何用python编写乘法口诀表
  14. 和NeroBlack合作的流体教学在AboutCG发布
  15. 【GCN-CTR】剪枝特征:Detecting Beneficial Feature Interactions for Recommender Systems (AAAI‘21)
  16. 网上作业批改系统的设计与实现(JSP,MySQL)
  17. MATLAB对水下目标去噪声代码,水下目标噪声与背景噪声的建模与仿真
  18. hawk大数据基础知识总结(1)
  19. 如何使 SAP UI5 SmartField 在运行时渲染成超链接的形式并支持跳转的试读版
  20. 群晖服务器216j增加硬盘,群晖DS216j影视库搭建方法 | 群晖DS216j存储服务器怎么样_什么值得买...

热门文章

  1. HTML网页设计制作——响应式网页影视动漫资讯bootstrap网页(9页)
  2. 独立站运营到底要做什么!
  3. java压缩JPG 、PNG图片
  4. Android手机里的垃圾文件和文件夹清理
  5. 论文阅读《SHINE: Signed Heterogeneous Information Network Embedding for Sentiment Link Prediction》
  6. 倒计时21天,房贷利率新政要来了,有银行已率先公布报价
  7. 计算机硬件 系统安装维护教程 02安装基础篇-02:MBR与GPT、分区、活动分区
  8. 数理统计与描述性分析
  9. 自建RssHub本地服务
  10. 移动光猫怎么设置虚拟服务器设置,移动光猫如何设置自带的WIFI无线功能