原文:Step-by-step guide to concurrency
参考:Go并发编程基础(译)

内容涵盖:

  • 运行期并发线程(goroutines)
  • 基本的同步技术(通道和锁)
  • Golang中基本的并发模式
  • 死锁和数据竞争
  • 并行计算
![](https://res.cloudinary.com/twenty20/private_images/t_standard-fit/v1521838719/photosp/d59a4866-14cb-460f-a111-15fde2adc69c/d59a4866-14cb-460f-a111-15fde2adc69c.jpg) — [Nancy Hajjar](https://www.twenty20.com/photog84?cta=ft&cta_uid=07188173-8090-4f5e-86d6-52e263eb0b7e&utm_t20_source=partner&utm_t20_channel=reshot&utm_t20_campaign=gallery)

目录

协程-goroutine

A goroutine is a lightweight thread of execution. All goroutines in a single program share the same address space.

The go statement(声明, 陈述) runs a func­tion in a sepa­rate thread of execu­tion.

go 关键字声明了在一个独立的执行线程中运行一个函数

You can start a new thread of execution(执行线程), a goroutine, with the go statement. It runs a function in a different, newly created, goroutine. All goroutines in a single program share the same address space.

你可以用 go 关键字声明一个新的执行线程,即一个协程。它在一个不同的、新创建的协程中运行一个函数。所有的协程在一个单独的程序中共享相同的地址空间。

1
go list.Sort() // Run list.Sort in parallel; dont wait for it.

下面所示程序会输出“Hello from main goroutine”。也可能会输出“Hello from another goroutine”,具体依赖于两个goroutine哪个先结束。

1
2
3
4
5
6
func main() {
	go fmt.Println("Hello form another goroutine")
	fmt.Println("Hello form main goroutine")

	// 至此,程序执行结束,且所有活跃的 go程都将被 killed
}

接下来的程序大多数情况下都会输出 “Hello from main goroutine” and “Hello from another goroutine”. 输出顺序不确定。但还有另外一种情况, 就是第二个go程非常的慢,直到程序结束都没有消息输出。

1
2
3
4
5
6
7
8
func main() {
	go fmt.Println("Hello from another goroutine")
	fmt.Println("Hello from main goroutine")

	// main goroutine sleep 1s...
	// wait for other goroutine to finish
	time.Sleep(time.Second)
}

下面则是一个相对更加实际的例子,其中我们定义了一个函数使用并发来推迟一个事件

1
2
3
4
5
6
7
8
// Publish 函数在延迟delay到期后,打印text内容到标准输出
// 且该函数不会阻塞而是执行完成后立即返回
func Publish(text string, delay time.Duration) {
	go func() {
		time.Sleep(delay)
		log.Println("BREAKING NEWS:", text)
	}() // 注意这里的括号,必须调用匿名函数
}

你可能会这样使用Publish函数:

1
2
3
4
5
6
7
8
func main() {
    Publish("A goroutine starts a new thread of execution.", 5*time.Second)
    log.Println("Let’s hope the news will published before I leave.")

    // 等待发布新闻
    time.Sleep(10 * time.Second)
    log.Println("Ten seconds later: I’m leaving now.")
}

这个程序,绝大多数情况下,会输出以下三行,顺序固定,每行输出之间相隔5秒。

1
2
3
4
$ go run thread-of-execution.go
2018/06/18 20:00:29 Let’s hope the news will published before I leave.
2018/06/18 20:00:34 BREAKING NEWS: A goroutine starts a new thread of execution.
2018/06/18 20:00:39 Ten seconds later: I’m leaving now.

Note: 一般来说,通过睡眠的方式来编排线程间的相互等待是不太可能的. 下一章节会介绍Go语言中的一种同步机制 - 管道,并演示如何使用管道让一个goroutine等待另一个goroutine。

Go程实现

go程是轻量级的,开销比栈空间分配还要小。开始栈空间很小,之后随着需求不断分配和释放堆存储空间而变大。

内部实现上,goroutines 会在多个操作系统线程上多路复用。如果一个 goroutine 阻塞了一个操作系统线程,例如:等待输入,这个线程上的其他输入就会迁移到其他线程上,这样保证了能继续运行。开发者并不需要关心/担心这些细节。

Ref

返回目录

通道Channel

A channel is a mechanism(机制) for goroutines to synchronize execution and communicate by passing values.

管道是Go语言的一个构件,提供一种机制用于两个goroutine之间通过传递一个指定类型的值来同步运行和通讯。操作符<-用于指定管道的方向,发送或接收。如果未指定方向,则为双向管道。

1
2
3
chan Sushi        // 可用来发送和接收Sushi类型的值
chan<- float64    // 仅可用来发送float64类型的值
<-chan int        // 仅可用来接收int类型的值

管道是引用类型,基于make函数来分配。

1
2
3
4
ic := make(chan int)                // 不带缓冲的int类型管道
wc := make(chan *Work, 10)          // 带缓冲的Work类型指针管道
onlySendCh := make(chan<- string)   // 不带缓冲的仅用来发送的string类型管道
onlyRecvCh := make(<-chan int, 2)   // 带缓冲的仅用来接收的int类型管道

如果通过管道发送一个值,则将<-作为二元操作符使用。通过管道接收一个值,则将其作为一元操作符使用:

1
2
ic <- 3         // 往管道发送3
work := <-wc    // 从管道接收一个指向Work类型值的指针

缓冲和不带缓冲的通道

  • 如果管道不带缓冲,发送方会阻塞直到接收方从管道中接收了值。
  • 如果管道带缓冲,发送方则会阻塞直到发送的值被拷贝到缓冲区内;
  • 如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。接收方在有值可以接收之前会一直阻塞。

对于 channel 的一些思考

ch := make(chan int) 和 ch := make(chan int, 1) 有什么区别 ? 代码测试如下:

 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() {
	// unBufferedChan()
	oneBufferedChan()
}

func unBufferedChan() {
	ch := make(chan int) // or <=> ch := make(chan int, 0)
	// go func(i chan int) {
	// 	println(<-i)
	// }(ch)
	ch <- 1             // 阻塞,且会死锁;无缓冲的 chan 必须在另一个 goroutine 中读取该 chan 的数据,否则会造成死锁
	fmt.Println("send") // 不会输出
}

func oneBufferedChan() {
	ch := make(chan int, 1) // 可容纳一个元素
	ch <- 1                 // 不会阻塞, 且不会有死锁
	fmt.Println("send")     // 会输出:send
	fmt.Println(<-ch)       // 输出:1
	ch <- 2                 // 不会阻塞, 且不会有死锁
	fmt.Println("send")     // 会输出:send
	ch <- 3                 // 阻塞,且死锁
}

总结:从测试demo可以看到:当给一个无缓冲通道发送数据时,必须保证有另一个goroutine读取数据,否则会造成死锁;当给只有一个元素的缓冲通道发送数据时,程序在第二次发送数据的时候才会发生阻塞,因为可以容纳一个元素。

关闭通道

close 函数标志着不会再往某个管道发送值。注意,这是唯一一个有必要去关闭通道的条件,即如果接收者一直在寻找一个关闭的时候。

  • 再调用 close 之后,并且在之前发送的值都被接收后,接下来接收操作都会返回一个 零值(类型的初始值),不会阻塞。(Note: 关闭一个通道后,监听这个通道的所有goroutine都会收到该 chan 的一个零值)
  • 一个多返回值的接收操作会额外返回一个bool类型的值用来标识通道是否被关闭
  • 在一个已关闭的通道上,发送值或再次关闭将会造成运行时 panic, 关闭一个 nil channel 也会造成运行时 panic
 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() {
	ch1 := make(chan int)
	ch2 := make(chan string)

	go func() {
		ch1 <- 10
		ch2 <- "hello"
		close(ch1)
		close(ch2)
	}()

	log.Println("recv from ch1:", <-ch1) // 输出:10
	log.Println("recv from ch2:", <-ch2) // 输出:"hello"

	log.Println("recv finish, ch1 & ch2 has been closed")

	log.Println("recv form ch1:", <-ch1) // 输出零值:0, 不会阻塞
	log.Println("recv form ch2:", <-ch2) // 输出零值:""(空字符串), 不会阻塞
	log.Println("recv form ch1:", <-ch1) // 再次输出零值:0, 不会阻塞

	v, ok := <-ch2
	if !ok {
		// at this point: k is false
		log.Println("read on a closed chan")
	}
	log.Println("recv form ch2:", v) // 再次输出零值:""(空字符串), 不会阻塞
}

通过通道广播信号

When you close a channel, all readers receive a zero value. This can be used to broadcast a signal to several goroutines an a single channel.

Note that we use a channel of empty structs to indicate that the channel will only be used for signalling, not for passing data. This is how you might use the function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func main() {
	wait := Publish2("A goroutine starts a new thread of execution.", 2*time.Second)
	log.Println("Let's hope the news will published before I leave.")

    <-wait // block 等待发布新闻完成
    // wait has been closed, now received zero-value
    
    log.Println("Ten seconds later: I’m leaving now.")
}

// Publish prints text to stdout after the given time has expired.
// It closes the wait channel when the text has been published.
//
// wait 是一个只允许接收的通道类型
func Publish2(text string, delay time.Duration) (wait <-chan struct{}) {
	ch := make(chan struct{}) // ch 是一个双向通道类型
	go func() {
		time.Sleep(delay)
		log.Println("BREAKING NEWS:", text)
		close(ch)
	}() // 注意这里的括号,必须调用匿名函数

	return ch // Note: 双向通道类型可以赋值给单通道类型的
}

Ref

返回目录

通道选择-select 语句

A select statement allows you to wait for multiple send and receive operations simultaneously(同时)

Select 语句用于从一组可能的通信(发送或接收)中,选择其中一个进行处理。

  • 如果没有默认的分子语句,即 default, 则 select 声明会整体阻塞,直到其中有一个操作变为非阻塞
  • 如果有多个通信(case)可以处理, 则从中随机选择一个进行处理

Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// select 阻塞等待,直到 ch1 或 ch2 有数据发送过来
func main() {
    select {
    case <- ch1:
        println("received from chan 1")
    case <- ch2:
        println("received from chan 2")
    }
    println("unblocked from select")
}

nil 通道上, 发送和接收操作会永久阻塞, 这个可以用来在 select 的选择语句中禁用通道。

Example:

1
2
3
4
5
6
7
8
9
func main() {
    ch1 = nil // 禁用 ch1
    select {
    case <- ch1: 
        println("received from chan 1") // 这个case将不会被执行
    case <- ch2:
        println("received from chan 2")
    }
}

Default case

在其他所有的 case 都被阻塞的情况下,default case 总是能够被处理且继续运行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
func main() {
	ch1, ch2 := make(chan int), make(chan int)
	go Select(ch1, ch2)
	time.Sleep(2 * time.Millisecond) // 阻塞 2ms, 等待 go 程被调度
	log.Println("main end...")
}

func Select(ch1 chan int, ch2 chan int) {
	for {
		select {
		case <-ch1:
			log.Println("ch1")
		case <-ch2:
			log.Println("ch2")
		default:
			log.Println("nothing available")
		}
	}
}

Examples

  1. 一个无限的随机二进制序列

     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() {
        random := RandomBits()
        for i := 0; i < 10; i++ {
            print(<-random)
        }
        println()
            
        log.Println("main end...")
    }
    
    // RandomBits 生成无限的随机二进制序列
    func RandomBits() <-chan int {
        ch := make(chan int)
        go func() {
            for {
                select {
                case ch <- 1:
                case ch <- 2:
                }
            }
        }()
        return ch
    }
    1. 下面是相对更加实际一点的例子:如何使用select语句为一个操作设置一个时间限制。代码会输出变量news的值或者超时消息,具体依赖于两个接收语句哪个先执行:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    func main() {
    
        ch := make(chan string)
        go selectWithTimeout(ch)
        // ch <- "news: boom boom boom"
        time.Sleep(2 * time.Second) // 阻塞2秒,等待 go 程调度
    
        log.Println("main end...")
    }
    
    // selectWithTimeout 一个带有超时机制的阻塞通道
    func selectWithTimeout(ch chan string) {
        select {
        case s := <-ch:
            log.Println("[recv]", s)
        case <-time.After(time.Second): // 一秒超时
            log.Println("timeout")
        }
    }
  2. 死循环

    1
    2
    3
    
    func main() {
        select{}
    }

    返回目录

    数据竞争

    A data race is easily introduced(介绍、采用、引入) by mistack and can lead to situations(情况) that are very hard to debug. This article explains how to avoid this headcache(头痛).

    死锁也许听起来令人挺忧伤的,但伴随并发编程而来的真正灾难性的错误其实是数据竞争,相当常见,也可能非常难于调试

    当两个线程(goroutine)并发的访问同一个变量, 且其中至少有一个是写操作时,数据竞争就发生了

    下面的这个函数就有数据竞争问题,其行为是未定义的。例如,可能输出数值1。下面代码之后提供了一个可能性解释,来试图搞清楚这一切是如何发生得。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    func race() {
    wait := make(chan struct{})
    n := 0
    
    go func() {
    	n++ // func.1 goroutine: 一次访问要执行的操作:读,递增,写
    	wait <- struct{}{}
    }()
    
    n++ // main goroutine: 访问冲突
    <-wait
    log.Println("n =", n)
    }

代码中,有两个go程(假设main goroutine为 g1, go func(){}() 为 g2)参与了数据竞争,且我们无法获知操作将会以那种顺序发生,以下只是诸多可能性中的一种:

data race

data-race 这个名称有些误导的意思, 不仅操作的顺序是未定义的, 其实很难有保证。编译器和硬件为了得到更好的性能,经常都会对代码进行上下内外的顺序变换, 如果你看到一个线程处于中间行为状态时,那么当时的场景可能就像下图所示的一样: mid-action.jpg

如何避免数据竞争

避免数据竞争的唯一方式就是同步访问在线程间所共享的可变的数据。有好几种方式来实现这个目标。在 Go 中,我们通常使用 channel or lock 机制。(sync和sync/atomic包中还有更低层次的机制可供使用,但本文中不做讨论)。

在 Go 中处理并发数据访问的首选的方式就是使用 channel 从一个 goroutine 到下一个 goroutine 之间传递实际的数据。其宗旨就是:Don’t communicate by sharing memory; share memory by communicating.[不要通过共享内存来通信,而是通过通信来共享内存]

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// shareIsCaring 通过通信(channel)来共享内存
func shareIsCaring() {
	ch := make(chan int)

	go func() {
		n := 0 // 仅为一个 goroutine 可见的局部变量
		n++
		ch <- n // 数据从当前 goroutine 离开
	}()

	n := <-ch // 数据安全的到达了另一个 goroutine
	n++
	log.Println(n) // 输出:2
}

上面代码中的 channel 肩负两种责任:

  • 把数据从一个 goroutine 传递到另一个 goroutine
  • 扮演一个同步角色

发送方goroutine会等待另一个goroutine接收数据,接收方goroutine也会等待另一个goroutine发送数据。

Go 语言内存模型: 要保证一个 goroutine 中,对一个变量的读操作得到的值正好是另一个 goroutine 对同一个变量写操作产生的值,条件相当复杂, 但 goroutine 之间只要通过 channel 来共享所有可变数据, 那么久原理数据竞争了。

数据竞争检测

By starting your applications with the -race options, Go runtime might be able to detect and inform you aboud data races.

数据竞争很容易发生但是很难调试,幸运的是,Go 的运行时提供了帮助-数据竞争检测器

使用 -race 参数来开启内置的数据竞争检测器:

1
2
$ go test -race [packages]
$ go run -race [packages]

Example: 数据竞争Demo

 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
package main

import (
	"log"
)

func main() {
	race()
	shareIsCaring()
}

// race 两个go程数据竞争的demo
func race() {
	wait := make(chan struct{})
	n := 0

	go func() {
		n++ // func.1 goroutine: 一次访问要执行的操作:读,递增,写
		wait <- struct{}{}
	}()

	n++ // main goroutine: 访问冲突
	<-wait
	log.Println("n =", n)
}

开启 -race 参数运行该程序,会得到竞争检测的结果:main goroutine 在 datarace.go:29 行对变量 n 进行了写操作,另一个 goroutine 在 datarace.go:25 行进行了未同步的读操作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ go run -race datarace.go
==================                                                                       
WARNING: DATA RACE                                                                       
Read at 0x00c042056058 by goroutine 6:                                                   
  main.race.func1()                                                                      
      D:/code/Go_Path/src/instance.golang.com/concurrent-programming/datarace.go:25 +0x3f
                                                                                         
Previous write at 0x00c042056058 by main goroutine:                                      
  main.race()                                                                            
      D:/code/Go_Path/src/instance.golang.com/concurrent-programming/datarace.go:29 +0xd2
  main.main()                                                                            
      D:/code/Go_Path/src/instance.golang.com/concurrent-programming/datarace.go:15 +0x36
                                                                                         
Goroutine 6 (running) created at:                                                        
  main.race()                                                                            
      D:/code/Go_Path/src/instance.golang.com/concurrent-programming/datarace.go:24 +0xae
  main.main()                                                                            
      D:/code/Go_Path/src/instance.golang.com/concurrent-programming/datarace.go:15 +0x36
==================                                                                       
2018/06/21 14:36:59 n = 2                                                                
2018/06/21 14:36:59 2                                                                    
Found 1 data race(s)                                                                     
exit status 66 

Note: 竞争检测器只能发现在运行期发生的数据竞争

死锁检测

The Go runtime can often detect when a program freezes(冻结) because of a deadlock. This article explains how to debug and solve such issues.

当一组 goroutines 彼此等待且其中没有一个能够继续时就会发生死锁。

一个例子:PublishOk 是一个正确的函数,在 PublishWithDeadlock 中引入一个 bug:

 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
package main

import (
    "log"
    "time"
)

func main() {
    wait := PublishOk("PublishOk", time.Second)
    <-wait
    
    waitNoCloseReturn := PublishWithDeadlock    ("PublishWithDeadlock", time.Second)
    <-waitNoCloseReturn
    
    log.Println("main end...")
}

// PublishOk
func PublishOk(text string, delay time.Duration) (wait <-chan struct{}) {
    ch := make(chan struct{})
    time.AfterFunc(delay, func() {
    	log.Println("[NEWS]", text)
    	// 发布成功后,通知 wait 结束等待
    	close(ch)
    })
    return ch
}

// PublishWithDeadlock
func PublishWithDeadlock(text string, delay time.Duration) (wait <-chan struct{}) {
    ch := make(chan struct{})
    time.AfterFunc(delay, func() {
    	log.Println("[NEWS]", text)
    	// 注释 close(ch), 即 wait 通道会永久阻塞等待,造成死锁
    	// close(ch)
    })
    return ch
}

Note: 上面程序中,wait 通道等待新闻发布完成后,取消阻塞,继续运行主程序;而 waitNoCloseReturn 会一直阻塞直到新闻发布完成,且该通值有值返回时才结束阻塞,但是我们在这里注释了 close(ch),所以 waitNoCloseReturn 会一直阻塞,此刻之后,程序无法再继续往下执行。众所周知,这种情形即为死锁。

死锁是线程之间相互等待,其中任何一个都无法继续向前运行的情形。

Go语言对于 运行时的死锁检测 具备良好的支持。当没有任何goroutine能够往前执行的情形发生时,Go程序通常会提供详细的错误信息。以下就是我们的问题程序的输出:

1
2
3
4
5
6
7
8
9
$ go run deadlock.go
2018/06/23 16:27:01 [NEWS] PublishOk
2018/06/23 16:27:02 [NEWS] PublishWithDeadlock
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
        D:/code/Go_Path/src/instance.golang.com/concurrent-programming/deadlock.go:20 +0x9a
exit status 2

调试提示

一个 goroutine 可能会因为下面两个原因被卡住:

  • It’s waiting for a channel
  • It’s waiting for one of locks int the sync package

常见的原因是:

  • no other goroutine has access the channel or the locks
  • a group of goroutines are waiting for each other and none of them is able to proceed

当前,Go 只能在整个程序冻结的情况下检测到死锁, 而不是在一组协程中的部分被卡住时。

使用通道,很容易找出是什么造成了死锁。另一方面,大量使用互斥锁的程序可能很难调试

互斥锁

A sync.Mutex is used to synchronize data by explicit locking in Go.

有时,通过显式加锁, 而不是使用通道, 来同步数据访问, 可能更加便捷。Go语言标准库为这一目的提供了一个互斥锁 - sync.Mutex

要想这类加锁起作用的话,关键之处在于:所有对共享数据的访问,不管读写,仅当 goroutine 持有锁时才能操作。一个goroutine出错就足以破坏掉整个程序,引起数据竞争

如何杀死一个Go程

One goroutine can’t forcibly(强制) stop another. To make a goroutine stoppable, let it listen for a stop signal on a channel.

一个 goroutine 不能强制停止另一个。为了能够让一个 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/*
 * 说明:how to kill a goroutine
 * 作者:zhe
 * 时间:2018-07-01 15:02
 * 更新:update demo
 */

package main

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

var wg sync.WaitGroup

func main() {
	wg.Add(1)

	quit := make(chan struct{})
	goWithQuitChan(quit)

	time.AfterFunc(time.Second*2, func() {
		quit <- struct{}{}
	})

	wg.Wait()
	fmt.Println("main end...")
}

// goWithQuitChan 带有 stop signal 的 Goroutine
func goWithQuitChan(quit chan struct{}) {
	defer func() {
		fmt.Println("g1 end...")
		close(quit)
		wg.Done()
	}()

	go func() {
		for {
			select {
			case <-quit:
				return
			default:
				// ...
			}
		}
	}()
}

有时为数据和信号使用一个单独的通道也是很方便的(Note: 利用了 colse(chan) 返回 zero value 的特性)

 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() {
	num := Generator()
	fmt.Println(<-num)
	fmt.Println(<-num)

	// close(num) 操作可能会引起 panic: send on a closed channel
	// Generator 中的 go 程在 select 选择上可能会出现: case ch <- n: 发送了,紧接着 case <- ch: 收到信号 return
	// 而 ch <- n 之前发送的值没有被接收就关闭了 channel, 从而就会是在一个关闭了的通道上执行发送操作 panic.
	//
	// Note: 有数据竞争
	close(num)
}

// Generator returns a channel that produces the numbers 1, 2 ...
// To stop underlying goroutine, close the channel
func Generator() chan int {
	ch := make(chan int)

	go func() {
		n := 1
		for {
			select {
			case ch <- n: // produces 1, 2, 3 ...
				n++
			case <-ch: // receive close signal
				return
			}
		}
	}()

	return ch
}

事件机制

Timers and Tickers let you execute code in the future, once or repeatedly.

Timeout(Timer)

Repeat(Ticker)

Wait、act and cancel(time.AfterFunc)

综合示例

 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
/*
 * 说明:Go 并发编程基础综合示例
 * 作者:zhe
 * 时间:2018-07-01 18:33
 * 更新:Updating demo
 */

package main

import (
	"fmt"
	"sync"
)

func main() {
	people := []string{"KeBe", "Wade", "Paul", "HaDen", "James"}

	match := make(chan string, 1) // 为一个未匹配的发送操作提供空间

	wg := new(sync.WaitGroup)
	wg.Add(len(people))
	for _, name := range people {
		go Seek(name, match, wg)
		// Step1: 第一个被调度的 goroutine 必定执行分支 `case match <- name:` 给通道写入数据,且不阻塞
		// Step2: 第二个被调度的 goroutine 必定执行分支 `case peer :<- match:` 读取通道的数据
		// Step3: 同 Step1
		// Step4: 同 Step2
		// Step5: 同 Step1
	}
	wg.Wait() // 等待所有 goroutine 结束

	select {
	case name := <-match:
		// 接收来自最后一个被调度的 goroutine 发送的数据
		fmt.Printf("No one received %s’s message.\n", name)
	default:
		// 没有待处理的发送操作
	}
}

// Seek 发送一个name到match管道或从match管道接收一个peer,结束时通知wait group
func Seek(name string, match chan string, wg *sync.WaitGroup) {
	fmt.Printf("[g:%6s] ", name)
	select {
	case peer := <-match:
		fmt.Printf("%s sent a message to %s.\n", peer, name)
	case match <- name:
		// 等待某个goroutine接收我的消息
		fmt.Println("Send")
	}
	wg.Done()
}

Click here to checkout the Repo

See Also

Thanks to the authors 🙂

返回目录