20.通道
在Go语言中,通道(Channel)是一种强大的并发编程工具,主要用于在不同的Goroutine之间传递数据。通过通道,可以轻松实现在Goroutine之间的通信和同步,避免手动加锁的复杂性。Channel底层是一个先进选出的环形队列(固定大小环形数组实现),示意图如下所示:

通道具备以下特点:
- full或empty就会阻塞
- send发送,recv接收并移除
- sendx表示最后一次插入元素的index
- recvx表示最后一次接收元素的index
- 发送和接收对应的操作符为 <-
20.1 通道声明
通道使用 chan 关键字和数据类型来声明,通道的类型定义了它可以传递的数据类型,示例如下所示:
var ch chan int
上面示例中,chan 代表声明的是一个通道变量,其数据类型为int,表示该通道可以发送和接收int类型的数据通道。通道的默认值为 nil,在使用前需要通过make()函数对其进行初始化。
20.2 发送和接收数据
package main
import "fmt"
func add(x, y int, out chan<- int) {
	result := x + y
	// 将结果发送到通道
	out <- result
}
func main() {
	// 声明通道,数据类型为int
	out := make(chan int)
	// 创建协程,传入相应的参数
	go add(4, 5, out)
	// 从协程中接收数据并打印
	fmt.Printf("从通道中获取到的数据为:%+v\n", <-out)
}
20.3 通道创建
通道的创建通常使用make来创建,在创建的时候,还可以指定其容量。
20.3.1 非缓冲通道
非缓冲通道是指容量为0的通道,也叫同步通道。这种通道发送第一个元素时,如果没有接收操作就立即阻塞,直到被接收。同样接收时,如果没有数据被发送立即阻塞,直到有数据发送。
package main
import "fmt"
func main() {
	ch := make(chan int, 0)
	fmt.Printf("通道:%+v 类型:%[1]T 长度:%d 容量:%d\n", ch, len(ch), cap(ch))
	fmt.Println("准备发送数据...")
	// 向ch里面发送数据,会阻塞在这一句,导致死锁,因为无人接收
	ch <- 8
	fmt.Println("发送数据结束...")
}
在使用make创建通道时,如果未指定容量,则容量默认为0
20.3.2 缓冲通道
缓冲通道是指容量不为0的通道。通道已满时,发送操作会被阻塞;通道为空,接收操作会被阻塞。
package main
import "fmt"
func main() {
	ch := make(chan int, 8)
	fmt.Printf("通道:%+v 类型:%[1]T 长度:%d 容量:%d\n", ch, len(ch), cap(ch))
	fmt.Println("准备发送数据...")
	// 向ch里面发送数据,会阻塞在这一句,导致死锁,因为无人接收
	ch <- 8
	ch <- 9
	ch <- 10
	fmt.Println("发送数据结束...")
	fmt.Printf("接收数据:%T 详情:%[1]v\n", <-ch)
	fmt.Printf("接收数据:%T 详情:%[1]v\n", <-ch)
	fmt.Printf("接收数据:%T 详情:%[1]v\n", <-ch)
}
输出结果如下所示:
通道:0xc000128000 类型:chan int 长度:0 容量:8
准备发送数据...
发送数据结束...
接收数据:int 详情:8
接收数据:int 详情:9
接收数据:int 详情:10
20.4 通道类型
在Go语言中,通道可以被限定为只发送或只接收数据,这种通道声明中的方向符号 <- 来指定。因此可以分为三种只发送通道、只接收通道、双向通道。
20.4.1 只发送通道
声明 chan<- 表示该通道仅用于发送数据,不能接收数据。示例如下所示:
ch chan<- int
20.4.2 只接收通道
声明 <-chan 表示该通道仅用于接收数据,不能发送数据。示例如下所示:
ch <-chan int
20.4.3 双向通道
如果不指定通道方向,则通道既可以发送数据,也可以接收数据。示例如下所示:
ch chan int
双向通道可以在不同的场景下转换为只接收通道或只发送通道,具体取决于函数的需求。
package main
import "fmt"
func sendOnly(ch chan<- int, num int) {
	ch <- num
}
func recvOnly(ch <-chan int) {
	fmt.Printf("接收到的数据为:%v\n", <-ch)
}
func main() {
	ch := make(chan int, 8)
	sendOnly(ch, 100)
	recvOnly(ch)
}
通过以上方式,开发者可以在代码中明确限制通道的作用方向,避免潜在的误用。
根据只接收和只发送数据的通道特点,我们可以实现生产者消费者模式,示例如下所示:
package main
import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)
func product(ch chan<- int) {
	for {
		randNum := rand.Intn(100)
		fmt.Printf("向通道发送数据:%d\n", randNum)
		ch <- randNum
		time.Sleep(2 * time.Second)
	}
}
func consume(ch <-chan int) {
	for {
		fmt.Printf("从通道中接收到的数据:%d\n", <-ch)
	}
}
func main() {
	var wg sync.WaitGroup
	wg.Add(1)
	ch := make(chan int)
	go product(ch)
	go consume(ch)
	wg.Wait()
}
运行结果如下所示:
向通道发送数据:80
从通道中接收到的数据:80
向通道发送数据:56
从通道中接收到的数据:56
向通道发送数据:27
从通道中接收到的数据:27
向通道发送数据:24
从通道中接收到的数据:24
向通道发送数据:38
从通道中接收到的数据:38
向通道发送数据:12
从通道中接收到的数据:12
20.4.4 struct{}通道
如果一个结构体类型是struct{},则说明该结构体的实例没有数据成员,也就是实例内存占用为0。这种类型数据构成的通道,非常节省内存,仅仅是为了传递一个信号标志。
package main
import (
	"fmt"
	"time"
)
func main() {
	fmt.Println("main start")
	flag := make(chan struct{})
	go func() {
		time.Sleep(5 * time.Second)
		flag <- struct{}{} //无数据成员的结构体实例
	}()
	fmt.Printf("类型:%T 内容:%#[1]v\n", <-flag)
	fmt.Println("main end")
}
运行结果如下所示:
main start
类型:struct {} 内容:struct {}{}
main end
20.5 通道关闭
一般在正常情况下,通道是不用关闭的,但当通道不再使用时需要及时关闭通道。关闭通道使用关键字close()。注意事项如下所示:
- 只有发送方才能关闭通道,一旦通道关闭,发送者不能再往通道里面发送数据,否则会出现panic
- 一个通道仅能关闭一次,若再次关闭则会出现panic,因此通道不能重复关闭
- 在通道已经关闭后,里面的数据还可以继续读取
通道关闭的作用主要是告诉接收者后续没有新数据可以到达了。
package main
import (
	"fmt"
)
func main() {
	ch := make(chan int, 8)
	ch <- 8
	ch <- 9
	ch <- 10
	fmt.Printf("从通道中获取数据:%+v\n", <-ch)
	fmt.Println("关闭通道")
	close(ch)
	fmt.Printf("从通道中获取数据:%+v\n", <-ch)
	fmt.Printf("从通道中获取数据:%+v\n", <-ch)
}
运行结果如下所示:
从通道中获取数据:8
关闭通道
从通道中获取数据:9
从通道中获取数据:10
    在从通道中获取数据,通常使用t:=<-ch,但为了区分是否从通道里面拿到了有效的数据,通常使用t,ok:=<-ch来从通道中获取数据,ok的取值又可以分为以下两种情况:
- 正在阻塞等待通道中的数据接收者,由于通道被关闭,接收者将不再阻塞,获取数据失败时,ok为false,返回零值
- 接收者仍然可以访问关闭的通道而不阻塞,如果通道中还有剩余数据,ok为true,正常接收到数据,如果通道中无剩余数据,继续接收而不阻塞,ok为false,返回零值
package main
import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)
func product(ch chan<- int) {
	defer func() {
		fmt.Println("生产者关闭通道")
		close(ch)
	}()
	for i := 0; i < 5; i++ {
		randNum := rand.Intn(100)
		fmt.Printf("向通道发送数据:%d\n", randNum)
		ch <- randNum
		time.Sleep(2 * time.Second)
	}
}
func consume(ch <-chan int) {
	for {
		t, ok := <-ch
		fmt.Printf("从通道中接收到的数据:%d,ok的值为%+v\n", t, ok)
	}
}
func main() {
	var wg sync.WaitGroup
	wg.Add(1)
	ch := make(chan int, 8)
	go product(ch)
	go consume(ch)
	wg.Wait()
}
运行结果如下所示:
向通道发送数据:77
从通道中接收到的数据:77,ok的值为true
向通道发送数据:17
从通道中接收到的数据:17,ok的值为true
向通道发送数据:59
从通道中接收到的数据:59,ok的值为true
向通道发送数据:45
从通道中接收到的数据:45,ok的值为true
向通道发送数据:65
从通道中接收到的数据:65,ok的值为true
生产者关闭通道
从通道中接收到的数据:0,ok的值为false
从通道中接收到的数据:0,ok的值为false
从通道中接收到的数据:0,ok的值为false
从通道中接收到的数据:0,ok的值为false
从通道中接收到的数据:0,ok的值为false
20.6 通道遍历
20.6.1 nil 通道
发送和接收、遍历均阻塞
nil 通道一般是通过声明创建例如var ch chan int
20.6.2 缓冲未关闭的通道
相当于一个无限元素的通道,迭代不完,阻塞在等下一个元素到达。
package main
import "fmt"
func main() {
	ch := make(chan int, 9)
	fmt.Printf("通道长度:%d 容量:%d 详情:%+v\n", len(ch), cap(ch), ch)
	ch <- 8
	ch <- 9
	ch <- 10
	fmt.Println("提前拿走一个数据:", <-ch)
	fmt.Println("开始遍历通道")
	for v := range ch {
		fmt.Printf("通道遍历值:%+v\n", v)
	}
	// 下面这行因通道遍历而阻塞
	fmt.Println("结束遍历通道")
}
输出结果:
通道长度:0 容量:9 详情:0xc000126000
提前拿走一个数据: 8
开始遍历通道
通道遍历值:9
通道遍历值:10
fatal error: all goroutines are asleep - deadlock!
20.6.3 缓冲关闭的通道
关闭后,通道不能再进入新的元素,那么相当于遍历有限个元素容器,遍历完就结束了。
package main
import "fmt"
func main() {
	ch := make(chan int, 9)
	fmt.Printf("通道长度:%d 容量:%d 详情:%+v\n", len(ch), cap(ch), ch)
	ch <- 8
	ch <- 9
	ch <- 10
	fmt.Println("提前拿走一个数据:", <-ch)
	fmt.Println("关闭通道")
	close(ch)
	fmt.Println("开始遍历通道")
	for v := range ch {
		fmt.Printf("通道遍历值:%+v\n", v)
	}
	fmt.Println("结束遍历通道")
}
输出结果:
通道长度:0 容量:9 详情:0xc00010e000
提前拿走一个数据: 8
关闭通道
开始遍历通道
通道遍历值:9
通道遍历值:10
结束遍历通道
20.6.4 非缓冲未关闭的通道
相当于一个无限元素的通道,迭代不完,阻塞在等下一个元素到达。
package main
import (
	"fmt"
	"time"
)
func main() {
	fmt.Println("main start")
	ch := make(chan int)
	fmt.Printf("通道长度:%d 容量:%d 详情:%+v\n", len(ch), cap(ch), ch)
	go func() {
		count := 1
		time.Sleep(1 * time.Second)
		fmt.Printf("向通道发送数据:%d\n", count)
		ch <- count
		count++
	}()
	for v := range ch {
		fmt.Printf("向通道中遍历数据:%d\n", v)
	}
	//  下面这一句代码无法运行
	fmt.Println("main end")
}
运行结果如下所示:
main start
通道长度:0 容量:0 详情:0xc000100070
向通道发送数据:1
向通道中遍历数据:1
fatal error: all goroutines are asleep - deadlock!
20.6.5 非缓冲关闭的通道
关闭后,通道不能再进入新的元素,相当于遍历有限个元素容器,遍历完即结束。
package main
import (
	"fmt"
	"time"
)
func main() {
	fmt.Println("main start")
	ch := make(chan int)
	fmt.Printf("通道长度:%d 容量:%d 详情:%+v\n", len(ch), cap(ch), ch)
	go func() {
		defer close(ch)
		count := 1
		time.Sleep(1 * time.Second)
		fmt.Printf("向通道发送数据:%d\n", count)
		ch <- count
		count++
	}()
	for v := range ch {
		fmt.Printf("向通道中遍历数据:%d\n", v)
	}
	fmt.Println("main end")
}
main start
通道长度:0 容量:0 详情:0xc000098070
向通道发送数据:1
向通道中遍历数据:1
main end
除nil通道以外,根据以上运行可以总结出如下结论:
- 未关闭通道,像是一个无限元素的容器,将一直迭代通道中的元素,如果没有元素,则阻塞
- 已关闭通道,将不能再添加新的元素,相当于一个有限元素的容器,迭代完当前通道中的元素,即结束
20.7 定时器
定时器也是可以通过通道来实现,一般分为两种延迟执行和间隔执行。
20.7.1 延迟执行
    延迟执行是在指定的时间执行一次,使用time.NewTimer(),返回Timer,C里面有数据到达,仅读取一次。其中Timer的定义如下所示:
type Timer struct {
	C         <-chan Time
	initTimer bool
}
其中C为通道,读取它,一般不关心里面的数据,但要求该通道中有数据来,如果没有则阻塞。
package main
import (
	"fmt"
	"time"
)
func main() {
	t := time.NewTimer(2 * time.Second)
	for {
		fmt.Println(<-t.C)
	}
}
如果使用Timer时,尽量不要放在循环中,否则会导致死锁
20.7.2 间隔执行
    间隔执行,可以理解为心跳,代表每隔指定的时间执行一下。使用time.NewTicker(),返回一个Ticker。其中Ticker的定义如下所示:
type Ticker struct {
	C          <-chan Time // The channel on which the ticks are delivered.
	initTicker bool
}
示例代码如下所示:
package main
import (
	"fmt"
	"time"
)
func main() {
	t := time.NewTicker(2 * time.Second)
	for {
		fmt.Println(<-t.C)
	}
}
运行结果如下所示:
2025-05-04 14:59:13.7300158 +0800 CST m=+2.000000001
2025-05-04 14:59:15.7300158 +0800 CST m=+4.000000001
2025-05-04 14:59:17.7300158 +0800 CST m=+6.000000001
2025-05-04 14:59:19.7300158 +0800 CST m=+8.000000001
2025-05-04 14:59:21.7300158 +0800 CST m=+10.000000001
2025-05-04 14:59:23.7300158 +0800 CST m=+12.000000001
2025-05-04 14:59:25.7300158 +0800 CST m=+14.000000001
20.8 通道死锁
    Channel在满了时候,就会阻塞写;Channel在空的时候,就会阻塞读。阻塞了当前协程之后会交出CPU,去执行其他协程,希望其他协程帮助自己解除阻塞。而在Go语言中main函数结束执行时,整个进程都结束了。例如在main函数中,执行语句阻塞时,环顾四周,如果已经没有其他子协程可以执行,只剩下主协程自己,则会造成协程解锁无望,就自己把自己打掉,然后抛出一个异常fatal error: all goroutines are asleep - deadlock!。示例代码如下所示:
package main
import (
	"fmt"
)
func main() {
	ch := make(chan int)
	fmt.Printf("详情:%+v 长度:%d 容量: %d\n", ch, len(ch), cap(ch))
	// 当前协程阻塞,无人能解琐,造成死琐
	ch <- 8
}
死琐一名话来概述就是,只有主协程活着且主协程阻塞后,没有其他协程帮忙解琐,在出现这种情况后,Go程序会采取自杀来结束程序运行。
如果通道阻塞不是在main协程中发生,而是发生在子协程中,子协程也会阻塞,也可能会发生死琐。但是由于至少主协程是一个值得等待的希望,编译器不能自动识别出死琐。如果真的无任何协程可以帮助解除阻塞状态,那么事实上该子协程也会解琐无望,造成死琐。
package main
import (
	"fmt"
	"time"
)
func main() {
	t := time.NewTicker(2 * time.Second)
	ch := make(chan int)
	for i := 0; i < 5; i++ {
		go func(id int) {
			fmt.Printf("协程 i:%+v 启动\n", id)
			fmt.Println(<-ch)
			fmt.Printf("协程 i:%+v 结束\n", id)
		}(i)
	}
	for {
		fmt.Printf("%+v\n", <-t.C)
	}
}
运行结果如下所示:
协程 i:3 启动
协程 i:1 启动
协程 i:0 启动
协程 i:2 启动
协程 i:4 启动
2025-05-04 15:27:28.4042193 +0800 CST m=+2.000000001
2025-05-04 15:27:30.4042193 +0800 CST m=+4.000000001
2025-05-04 15:27:32.4042193 +0800 CST m=+6.000000001
2025-05-04 15:27:34.4042193 +0800 CST m=+8.000000001
2025-05-04 15:27:36.4042193 +0800 CST m=+10.000000001
2025-05-04 15:27:38.4042193 +0800 CST m=+12.000000001
死琐的危害可能会导致进程活着,但实际上某些协程却是未能真正工作而处于阻塞状态。因此在编写代码需要有良好的习惯,减少死琐的产生。
20.9 通道多路复用
在通道数据到达上限时,再向里面发送或写入数据时,会阻塞,而在通道为空时,再从中接收或读取数据时,也会阻塞。在实际程序运行过程中,我们通常是无法判断通道当前是否有数据或数据已经到达上限的。针对这种情况,Go语言提供了select...case来解决这个问题。它就像一个多路复用器,监控多个通道的读或写。其具备以下特点:
- select 是一次执行
- 如果没有default,则一直阻塞至某个通道就绪
- 如果case都不成立,有default,则直接执行default,相当于兜底
- 如果case成立,则走相应的case通道
package main
import (
	"fmt"
	"time"
)
func main() {
	fmt.Println("main start")
	ch := make(chan int, 8)
	flag := make(chan struct{})
	go func() {
		defer func() {
			flag <- struct{}{}
		}()
		for i := 0; i < 10; i++ {
			time.Sleep(1 * time.Second)
			ch <- i
		}
	}()
	// 永久阻塞
	// select {}
	time.Sleep(5 * time.Second)
LOOP:
	for {
		select {
		case <-ch:
			fmt.Println("开始读取数据:", <-ch)
		case f := <-flag:
			fmt.Printf("结束了:%v\n", f)
			break LOOP
			// goto END
			// default:
			// 	fmt.Println("default")
		}
	}
	// END:
	fmt.Println("main end")
}
运行结果如下所示:
main start
开始读取数据: 1
开始读取数据: 3
开始读取数据: 5
开始读取数据: 7
开始读取数据: 9
结束了:{}
main end
示例代码
package main
import (
	"fmt"
	"time"
)
func main() {
	fmt.Println("main start")
	ch := make(chan int, 8)
	flag := make(chan struct{})
	count := 50
	go func() {
		defer func() {
			flag <- struct{}{}
		}()
		for i := 0; i < 5; i++ {
			ch <- i
		}
	}()
	t1Interval, t2Interval := 1, 5
	t1 := time.NewTicker(time.Duration(t1Interval) * time.Second)
	t2 := time.NewTicker(time.Duration(t2Interval) * time.Second)
	for {
		// select 负责通道的读写
		select {
		case n := <-t1.C:
			fmt.Printf("日志:%v 间隔%d s,长度:%d\n", n, t1Interval, len(ch))
		case n := <-t2.C:
			fmt.Printf("日志:%v 间隔%d s,读取到数据:%d\n", n, t2Interval, <-ch)
		case ch <- count:
			fmt.Printf("向通道%v写入数据%d\n", ch, count)
			count++
		}
	}
	fmt.Println("main end")
}
运行结果如下所示:
main start
向通道0xc0000be000写入数据50
向通道0xc0000be000写入数据51
向通道0xc0000be000写入数据52
日志:2025-05-04 17:07:58.3883083 +0800 CST m=+1.000528401 间隔1 s,长度:8
日志:2025-05-04 17:07:59.3883083 +0800 CST m=+2.000528401 间隔1 s,长度:8
日志:2025-05-04 17:08:00.3883083 +0800 CST m=+3.000528401 间隔1 s,长度:8
日志:2025-05-04 17:08:01.3883083 +0800 CST m=+4.000528401 间隔1 s,长度:8
日志:2025-05-04 17:08:02.3883083 +0800 CST m=+5.000528401 间隔1 s,长度:8
日志:2025-05-04 17:08:02.3883083 +0800 CST m=+5.000528401 间隔5 s,读取到数据:0
向通道0xc0000be000写入数据53
日志:2025-05-04 17:08:03.3883083 +0800 CST m=+6.000528401 间隔1 s,长度:8
日志:2025-05-04 17:08:04.3883083 +0800 CST m=+7.000528401 间隔1 s,长度:8
日志:2025-05-04 17:08:05.3883083 +0800 CST m=+8.000528401 间隔1 s,长度:8
日志:2025-05-04 17:08:06.3883083 +0800 CST m=+9.000528401 间隔1 s,长度:8
日志:2025-05-04 17:08:07.3883083 +0800 CST m=+10.000528401 间隔1 s,长度:8
日志:2025-05-04 17:08:07.3883083 +0800 CST m=+10.000528401 间隔5 s,读取到数据:1
向通道0xc0000be000写入数据54
日志:2025-05-04 17:08:08.3883083 +0800 CST m=+11.000528401 间隔1 s,长度:8
日志:2025-05-04 17:08:09.3883083 +0800 CST m=+12.000528401 间隔1 s,长度:8
日志:2025-05-04 17:08:10.3883083 +0800 CST m=+13.000528401 间隔1 s,长度:8
日志:2025-05-04 17:08:11.3883083 +0800 CST m=+14.000528401 间隔1 s,长度:8
日志:2025-05-04 17:08:12.3883083 +0800 CST m=+15.000528401 间隔1 s,长度:8
日志:2025-05-04 17:08:12.3883083 +0800 CST m=+15.000528401 间隔5 s,读取到数据:2
向通道0xc0000be000写入数据55
日志:2025-05-04 17:08:13.3883083 +0800 CST m=+16.000528401 间隔1 s,长度:8
日志:2025-05-04 17:08:14.3883083 +0800 CST m=+17.000528401 间隔1 s,长度:8
日志:2025-05-04 17:08:15.3883083 +0800 CST m=+18.000528401 间隔1 s,长度:8
日志:2025-05-04 17:08:16.3883083 +0800 CST m=+19.000528401 间隔1 s,长度:8
日志:2025-05-04 17:08:17.3883083 +0800 CST m=+20.000528401 间隔1 s,长度:8
日志:2025-05-04 17:08:17.3883083 +0800 CST m=+20.000528401 间隔5 s,读取到数据:3
向通道0xc0000be000写入数据56
20.10 通道并发
Go语言采用的并发同步模型叫CSP(Communication Sequential Process)的通信顺序进程,这是一种消息传递模型,在Goroutine间传递消息,而不是对数据进行加锁来实现同步访问。在Goroutine之间使用Channel来同步和传递数据。其具备以下特点:
- Channel是多个协程之间通信的管道
- 一端推入数据,一端取走数据
- 同一时间,只有一个协程可以访问Channel的数据
- 可以协调协程的执行顺序
CSP是Communication Sequential Process的简称,中文叫通信顺序进程,是一种并发编程模型,用于描述两个独立的并发实体通过共享的通信Channel进行通信的并发的模型。在CSP中Channel是第一类对象,不关注发送消息的实体,而关注发送消息时使用的Channel,即不关心发送的人和接收的人是谁。
如果多个线程都使用了同一个数据,就会出现竞争问题。因为线程的切换不会服从程序员的意志,时间片用完就需要进行切换。解决办法通常是加锁处理,让其他线程不能共享数据进行修改,从而保证逻辑正确,但锁的引入又会出现并行的效率。
- 串行示例如下所示:
package main
import (
	"fmt"
	"runtime"
	"time"
)
var count int64 = 0
func increase() {
	for i := 0; i < 1000000; i++ {
		count++
	}
}
func main() {
	start := time.Now()
	increase()
	increase()
	increase()
	increase()
	increase()
	fmt.Printf("协程数量:%d\n", runtime.NumGoroutine())
	fmt.Printf("运行时长:%d 微秒\n", time.Since(start).Microseconds())
	fmt.Printf("最终count=%d\n", count)
}
运行结果如下所示:
协程数量:1
运行时长:8982 微秒
最终count=5000000
- 并发示例如下所示:
package main
import (
	"fmt"
	"runtime"
	"sync"
	"time"
)
var count int64 = 0
var wg sync.WaitGroup
func increase() {
	defer wg.Done()
	for i := 0; i < 1000000; i++ {
		count++
	}
}
func main() {
	start := time.Now()
	wg.Add(5)
	for i := 0; i < 5; i++ {
		go increase()
	}
	fmt.Printf("协程数量:%d\n", runtime.NumGoroutine())
	wg.Wait()
	fmt.Printf("运行时长:%d 微秒\n", time.Since(start).Microseconds())
	fmt.Printf("最终count=%d\n", count)
}
运行结果如下所示:
协程数量:6
运行时长:8357 微秒
最终count=1998382
从上面结果可以看出,第二次运行结果存在错误。原因在于count++不属于原子操作,可以被其他线程打断。所以,即使使用Goroutine也会有竞争,一样会有并发完全问题。可以换成以下的代码来运行
package main
import (
	"fmt"
	"runtime"
	"sync"
	"sync/atomic"
	"time"
)
var count int64 = 0
var wg sync.WaitGroup
func increase() {
	defer wg.Done()
	for i := 0; i < 1000000; i++ {
		atomic.AddInt64(&count, 1)
	}
}
func main() {
	start := time.Now()
	wg.Add(5)
	for i := 0; i < 5; i++ {
		go increase()
	}
	fmt.Printf("协程数量:%d\n", runtime.NumGoroutine())
	wg.Wait()
	fmt.Printf("运行时长:%d 微秒\n", time.Since(start).Microseconds())
	fmt.Printf("最终count=%d\n", count)
}
运行结果如下所示:
协程数量:6
运行时长:67800 微秒
最终count=5000000
通过以上运行结果来看,虽然最终结果正确,但运行时长却增加非常明显。那我们再来试试互斥琐来实现,看看结果
package main
import (
	"fmt"
	"runtime"
	"sync"
	"time"
)
var count int64 = 0
var wg sync.WaitGroup
var mx sync.Mutex
func increase() {
	defer wg.Done()
	for i := 0; i < 1000000; i++ {
		mx.Lock()
		count++
		mx.Unlock()
	}
}
func main() {
	start := time.Now()
	wg.Add(5)
	for i := 0; i < 5; i++ {
		go increase()
	}
	fmt.Printf("协程数量:%d\n", runtime.NumGoroutine())
	wg.Wait()
	fmt.Printf("运行时长:%d 微秒\n", time.Since(start).Microseconds())
	fmt.Printf("最终count=%d\n", count)
}
运行结果如下所示:
协程数量:6
运行时长:194754 微秒
最终count=5000000
再来使用Channel来同步各个协程的数据
package main
import (
	"fmt"
	"runtime"
	"sync"
	"time"
)
var wg sync.WaitGroup
var ch = make(chan int64, 1)
func increase() {
	defer wg.Done()
	for i := 0; i < 1000000; i++ {
		t := <-ch
		t++
		ch <- t
	}
}
func main() {
	start := time.Now()
	ch <- 0
	wg.Add(5)
	for i := 0; i < 5; i++ {
		go increase()
	}
	fmt.Printf("协程数量:%d\n", runtime.NumGoroutine())
	wg.Wait()
	fmt.Printf("运行时长:%d 微秒\n", time.Since(start).Microseconds())
	fmt.Printf("最终count=%d\n", <-ch)
}
运行结果如下所示:
协程数量:6
运行时长:1335388 微秒
最终count=5000000
以上的示例是一种计算密集型,需要对同一个数据进行争抢,并不能发挥并行计算的优势,也不适合使用通道。针对于计算密集型,使用琐更有效率和优势。因此通道适合的数据场景:
- 像管道一样,一级一级处理,一个协程处理完成后,发送给其他协程
- 生产者消费者模型
20.11 协程泄漏
协程泄漏是指只创建协程,不回收协程。造成协程泄漏主要原因是协程阻塞,未能如期结束,之后便大量累积,而协程的阻塞大部分跟通道相关,由于每个协程都要占用内存,如果出现协程泄漏,也会导致内存泄漏。
本文同步在微信订阅号上发布,如各位小伙伴们喜欢我的文章,也可以关注我的微信订阅号:woaitest,或扫描下面的二维码添加关注:

作者: Surpassme
来源: http://www.jianshu.com/u/28161b7c9995/
http://www.cnblogs.com/surpassme/
声明:本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出 原文链接 ,否则保留追究法律责任的权利。如有问题,可发送邮件 联系。让我们尊重原创者版权,共同营造良好的IT朋友圈。

 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号