チャネルの使用状況
チャンネル宣言方法
- chInt := make(chan int) // バッファなしチャネル バッファなしチャネル
- chInt := make(chan int, 0) // バッファなしチャネル バッファなしチャネル
- chInt := make(chan int, 2) // バッファリングされたチャネル バッファリングされたチャネル
チャンネルの基本的な使い方
- ch <- x // チャネルはデータ x を受信します
- x <- ch // チャネルはデータを送信し、x に割り当てます
- <- ch // チャネルはデータを送信し、受信者を無視します
バッファなしチャネルを使用する場合、この時点でデータをバッファに詰め込むときにすぐにデータを受信できる場所が必要です。そうでないと、常にブロックされてしまいます。原則として、この時点ではバッファーにデータは存在せず (バッファリングなし)、チャネルへのデータ送信は直接送信、つまりスリープを待っているコルーチンに直接送信すると見なされます。
func main() {
ch := make(chan string)
// 阻塞
ch <- "ping"
<-ch
}
コルーチンを開始してデータを取得します。
func main() {
ch := make(chan string)
// 程序能通过
go func() {
<-ch
}()
ch<-"ping"
}
記憶とコミュニケーション
記憶を共有することでコミュニケーションするのではなく、コミュニケーションすることで記憶を共有するのです。主に:
- コルーチンの競合とデータの衝突を避けてください。
- より高いレベルの抽象化により、開発の困難さが軽減され、プログラムの可読性が向上します。
- モジュールの分離が容易になり、拡張性と保守性が向上します。
共有メモリのケースを通じて:
func watch(p *int) {
for {
if *p == 1 {
fmt.Println("go")
break
}
}
}
func main() {
i := 0
go watch(&i)
time.Sleep(time.Second)
i = 1
time.Sleep(time.Second)
}
通信方法は以下の通りです。
func watch(c chan int) {
if <-c == 1 {
fmt.Println("go")
}
}
func main() {
c := make(chan int)
go watch(c)
time.Sleep(time.Second)
c <- 1
time.Sleep(time.Second)
}
チャネル設計
チャネルは Go の下部にある hchan 構造として表されます。
type hchan struct {
/* 缓存区结构开始 */
qcount uint // total data in the queue
dataqsiz uint // size of the circular queue
buf unsafe.Pointer // points to an array of dataqsiz elements
elemsize uint16
elemtype *_type // element type
/* 缓存区结构结束*/
// 发送队列
sendx uint // send index
sendq waitq // list of send waiters
// 接收队列
recvx uint // receive index
recvq waitq // list of recv waiters
lock mutex
// 0:关闭状态;1:开启状态
closed uint32
}
type waitq struct {
first *sudog
last *sudog
}
データはリング バッファリング バッファーに保存され、メモリ/GC オーバーヘッドを削減できます。
c <- “x” の糖衣構文に関しては、Go のチャネル データ送信の原則により、
c <- が chansend1 メソッドにコンパイルされます。
// %GOROOT%src/runtime/chan.go
//go:nosplit
func chansend1(c *hchan, elem unsafe.Pointer) {
chansend(c, elem, true, getcallerpc())
}
データを送信する 3 つの状況:
- バッファにデータが存在せず、チャネルへのデータ送信は直接送信とみなされ、受信待ちのコルーチンのアクセプタにデータが直接コピーされ、コルーチンが起動されます。受信待ちのコルーチンが存在しない場合は、データをバッファに入れます。
- バッファーにデータがあってもバッファーがいっぱいではない場合、データはバッファーに保管されます。
- 受信キューに休止中の待機コルーチンがなく、バッファーがいっぱいの場合、データは sudog にパッケージ化され、休止中の待機のために sendq キューに入れられ、その後チャネルによってロックが解除されます。
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
// ...
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
// 1. 取出接收等待队列 recvq 的协程,将数据发送给它
if sg := c.recvq.dequeue(); sg != nil {
send(c, sg, ep, func() {
unlock(&c.lock) }, 3)
return true
}
// 接收等待队列中没有协程时
if c.qcount < c.dataqsiz {
// 2. 缓存空间还有余量,将数据放入缓冲区
qp := chanbuf(c, c.sendx)
if raceenabled {
racenotify(c, c.sendx, nil)
}
typedmemmove(c.elemtype, qp, ep)
c.sendx++
if c.sendx == c.dataqsiz {
c.sendx = 0
}
c.qcount++
unlock(&c.lock)
return true
}
// ...
// 3. 休眠等待
gp := getg()
// 包装为 sudog
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
}
// 将数据,协程指针等记录到 mysq 中
mysg.elem = ep
mysg.waitlink = nil
mysg.g = gp
mysg.isSelect = false
mysg.c = c
gp.waiting = mysg
gp.param = nil
// 将 mysg 自己入队
c.sendq.enqueue(mysg)
// 休眠
gp.parkingOnChan.Store(true)
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
KeepAlive(ep)
// 被唤醒后再维护一些数据,注意,此时的记录的数据已经被拿走
if mysg != gp.waiting {
throw("G waiting list is corrupted")
}
gp.waiting = nil
gp.activeStackChans = false
closed := !mysg.success
gp.param = nil
// ...
}
rec <-c の文法糖に関しては、チャネル データ受信原理
Go は、次のように <-c を func chanrecv メソッドにコンパイルします。
- コンパイル段階では、rec <- c は次のように変換されます。
runtime.chanrecv1()
- コンパイル段階では、rec, ok <- c は次のように変換されます。
runtime.chanrecv2()
chanrecv()
最終的にメソッドを呼び出します
//go:nosplit
func chanrecv1(c *hchan, elem unsafe.Pointer) {
chanrecv(c, elem, true)
}
//go:nosplit
func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) {
_, received = chanrecv(c, elem, true)
return
}
データを受信する4つのケース:
- 待機中の送信コルーチン (sendq) がありますが、バッファは空で、コルーチンから受信しています。
- 待機中の送信コルーチン (sendq) がありますが、バッファは空ではなく、キャッシュから受信されています。
- 受信バッファ
- 受信をブロックする
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
// ...
if c.closed != 0 {
// ...
} else {
// 1.,2. 接收数据前,已经有协程在休眠等待发送数据
if sg := c.sendq.dequeue(); sg != nil {
// 在 1 的情况下,缓存为空,直接从 sendq 的协程取数据
// 在 2 的情况下,从缓存(缓冲)取走数据后,将 sendq 里的等待中的协程的数据放入缓存,并唤醒该协程
// 这就是为什么 sendq 队列中的协程被唤醒后,其携带的数据已经被取走的原因
recv(c, sg, ep, func() {
unlock(&c.lock) }, 3)
return true, true
}
}
// 3. 直接从缓存接收数据
if c.qcount > 0 {
qp := chanbuf(c, c.recvx)
if raceenabled {
racenotify(c, c.recvx, nil)
}
if ep != nil {
typedmemmove(c.elemtype, ep, qp)
}
typedmemclr(c.elemtype, qp)
c.recvx++
if c.recvx == c.dataqsiz {
c.recvx = 0
}
c.qcount--
unlock(&c.lock)
return true, true
}
// ...
// 缓冲区为空,同时 sendq 里也没休眠的协程,则休眠等待
gp := getg()
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
}
mysg.elem = ep
mysg.waitlink = nil
gp.waiting = mysg
mysg.g = gp
mysg.isSelect = false
mysg.c = c
gp.param = nil
c.recvq.enqueue(mysg)
// 休眠
gp.parkingOnChan.Store(true)
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)
// ...
}
チャネルからデータを受信するプロセス中にウェイクアップされ、以前にデータがなかったため休止状態で待機していたことを示します。送信者がデータを送信すると、そのデータを受信者のローカルにアクティブにコピーします。