学习《GO语言学习笔记》的学习笔记之8.2 通道 (详解)

本文适合初学者阅读

通道
  • 从底层实现上来说, 通道只是一个队列, 分为同步和异步两种模式
  • 同步模式,发送和接收双方配对, 然后直接复制数据给对方, 若配对失败, 则置入等待队列, 直到另一方出现后才被唤醒.
  • 异步模式, 抢夺的则是数据缓冲槽, 发送方要求有空槽可供写入, 接收方则要求有缓冲数据可读. 需求不符时, 同样加入等待队列, 直到有另一方写入数据或腾出空槽后被唤醒.
  • 通道除传递数据外, 还常被用作事件通知
package main


func main() {
	done := make(chan struct{})        // 结束事件
	c := make(chan string)      		// 数据传输通道

	go func() {
		s := <-c						// s接收来自c的数据
		println(s)						
		close(done)						// 关闭通道, 作为结束通知
	}()

	c <- "hi"							// 发送消息
	<-done								// 阻塞, 直到有数据或管道关闭

}
  • 同步模式必须有配对操作的goruntine出现, 否则会一直阻塞, 而异步模式在缓冲区未满或数据未读完前, 不会阻塞.
package main

func main() {
	c := make(chan string, 3)       //创建带3个缓冲槽的异步通道

	c <- "hi"						// 缓冲区未满, 不会阻塞
	c <- "word"

	println(<-c)					// 缓冲区尚有数据, 不会阻塞
	println(<-c)
	println(<-c)  					// 缓冲区没有数据了, 产生死锁.
}
//hi
//word
//fatal error: all goroutines are asleep - deadlock!
//goroutine 1 [chan receive]:

  • 多数时候, 异步通道有助于提升性能, 减少排队阻塞.
  • 内置函数 cap和len返回缓冲区大小少当前已缓冲数量; 而对于同步通道则都返回0, 据此可判断通道是同步还是异步.
  • 除使用简单的发送接收操作符外, 还可用ok-idom或range模式处理数据
package main
// 使用ok-idom模式处理
func main() {
	done := make(chan struct{})
	c := make(chan int)

	go func() {
		defer close(done)     // 确保发出结束通知

		for {
			x, ok := <-c
			if !ok {			 // 据此判断通道是否关闭
				return
			}
			println(x)
		}
	}()

	c <- 1
	c <- 2
	c <- 3
	close(c)
	<-done
}
package main

func main() {
	done := make(chan struct{})
	c := make(chan int)

	go func() {
		defer close(done)

		for x := range c {     // 循环获取消息, 直到通道被关闭
			println(x)
		}

	}()

	c <- 1
	c <- 2
	c <- 3
	close(c)
	<-done
}

及时用close函数关闭通道引发结束通告, 否则可能会引起死锁
通告可以是群体性的,也未必就是通告结束, 可以是任何需要表达的事件

package main

import (
	"sync"
	"time"
)

func main() {
	var wg sync.WaitGroup
	ready := make(chan struct{})

	for i := 0; i < 3; i++ {
		wg.Add(1)

		go func(id int) {
			defer wg.Done()

			println(id, ":ready.")        // 运动员准备就绪
			<-ready							// 等待发令枪响
			println(id, ": running...")
		}(i)

	}

	time.Sleep(time.Second)
	println("ready?   GO!")

	close(ready)   					// 发令枪: 砰!
	wg.Wait()
}

一次性事件用close效率更好, 没有多余开销, 连续或多样性事件, 可传递不同数据标志实现. 还可使用sync.Cond实现单播或广播事件.
对于closed或nil通道, 发送和接收操作都有相应规则:
- 向已关闭的通道发送数据,会引发panic
- 从已关闭的通道接收数据, 返回已缓冲数据或零值
- 无论收发, nil通道都会阻塞

package main

func main() {
	c := make(chan int, 3)

	c <- 10
	c <- 20

	close(c)

	for i := 0; i < cap(c)+1; i++ {
		x, ok := <-c
		println(i, ":", ok, x)
	}
}


// 0 : true 10
// 1 : true 20
// 2 : false 0
// 3 : false 0

重复关闭,或关闭nil通道都会引发panic发错误 ,

单向通道

通道是默认双向的,并不区分发送和接收端,但某些时候,我们可以限制收发操作的方向, 来获得更严谨的操作逻辑

package main

import (
	"sync"
)

func main() {
	var wg sync.WaitGroup
	wg.Add(2)

	c := make(chan int)
	var send chan<- int = c
	var recv <-chan int = c

	go func() {
		defer wg.Done()

		for x := range recv {
			println(x)
		}
	}()

	go func() {
		defer wg.Done()
		defer close(c)

		for i := 0; i < 3; i++ {
			send <- i
		}
	}()

	wg.Wait()
}
  • 不能在单向通道上做逆向操作.
  • close不能用于接收端
  • 无法将单向通道重新转换回去.

通道选择

如果要同时处理多个通道 , 可选用select语句.它会随机选择一个可用通道做收发操作.

import (
	"sync"
)

func main() {
	var wg sync.WaitGroup
	wg.Add(2)

	a, b := make(chan int), make(chan int)
	// 接收端
	go func() {
		defer wg.Done()
		for {
			var (
				name string
				x    int
				ok   bool
			)

			select { // 随机选择可用channel接收数据
			case x, ok = <-a:
				name = "a"
			case x, ok = <-b:
				name = "b"
			}

			if !ok { // 如果任一通道关闭, 则终止接收
				return
			}

			println(name, x) // 输出接收到数据
		}
	}()

	// 发送端
	go func() {
		defer wg.Done()
		defer close(a)
		defer close(b)

		for i := 0; i < 10; i++ {
			select { // 随机选择发送端
			case a <- i:
			case b <- i * 10:

			}
		}
	}()

	wg.Wait()

}
  • 当所有通道都不可用时, select会执行default语句. 如此可避开select阻塞, 但须注意处理外层循环, 以免陷入空耗.
  • 当前通道已满, 可以使用default生成新的缓存通道

模式

通常使用工厂方法将 goroutine和通道绑定.

package main

import "sync"

type receiver struct {
	sync.WaitGroup
	data chan int
}

func newReceiver() *receiver {
	r := &receiver{
		data: make(chan int),
	}

	r.Add(1)

	go func() {
		defer r.Done()
		for x := range r.data { //接收消息,直到通道关闭
			println("recv:", x)
		}
	}()

	return r
}

func main() {
	r := newReceiver()
	r.data <- 1
	r.data <- 2

	close(r.data) // 关闭通道, 发出结束通知
	r.Wait()      // 等待接收者处理结束
}

// recv: 1
// recv: 2

性能

  • 将发往通道的数据打包, 减少传输次数, 可有效提升性能.单次获取更多数据, 可改善因频繁加锁造成的性能问题,
  • 虽然单次消耗内存, 但性能提升非常明显.

8.3 同步

  • 通道并非用来取代锁的, 它们有各自不同的使用场景, 通道倾向于解决逻辑层次的并发处理架构, 而锁则用来保护局部范围内的数据安全.
  • 标准库sync 提供互斥和读写锁, 另有原子操作等, 可基本满足日常开发需要. Mutex, RWmutex的使用并不复杂, 只有几个地方需要注意.
  • 将Mutex作为匿名字段时, 相关方法必须实现为pointer-receiver, 否则会因复制导致锁机制失效.
  • 对性能要求较高时, 应避免使用defer Unlock
  • 读写并发时, 用RWMutex性能会更好一些.
  • 对单个数据读写保护, 可尝试用原子操作
  • 执行严格测试, 尽可能打开数据竞争检查.

你可能感兴趣的:(GO学习)