程序员社区

channel使用

【译文】原文地址
channel是Go语言的一个标志性特性,为go协程之间的数据交互提供一种非常强大的方式,而不需要使用锁机制。
本文将讨论channel的两个重要属性,一个是控制协程间数据发送和接收,以及对channel本身控制。

关闭的channel是非阻塞的

首先讨论下关闭的channel特性。一旦channel被关闭之后,就不能再继续发送数据给该channel,但是还是可以继续接收channel中的数据。如下所示:

package main

import "fmt"

func main()  {
    ch := make(chan bool, 2)
    ch <- true
    ch <- true
    close(ch)
    for i :=0; i < cap(ch) + 1; i++{
        v, ok := <-ch
        fmt.Println(v, ok)
    }
}

output:

true true
true true
false false

上述例子显示即使ch在for循环之前已经关闭,但还是可以正常的读取缓存中的true值,读完之后ok就会被赋值为false表示channel已经关闭,而且value值为对应channel类型bool的默认零值false。只要不停地从关闭的channel接收,就会无限的返回默认值和false。可以将for循环次数改大点试试即可验证。

通过以上例子可以发现,关闭的channel可以继续接收读取操作,这种特征是有用的。在使用range读取带缓存的channel时就会用到,一旦channel关闭,读取完缓存中数据就会停止接收数据退出。
将前面的例子改为如下:

package main

import "fmt"

func main()  {
    ch := make(chan bool, 2)
    ch <- true
    ch <- true
    close(ch)
    for v := range ch{
        fmt.Println(v)
    }
}

output:

true
true

上面的例子就没有false打出来了。正好是写入channel里面的两个值。

channel与select结合更能发挥出其作用,让我们看一个例子:

package main

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

func main()  {
    finish := make(chan bool)
    var done sync.WaitGroup
    done.Add(1)
    go func() {
        select {
        case <-time.After(1 * time.Hour):
        case <-finish:
        }
        done.Done()
    }()
    t0 := time.Now()
    finish <-true
    done.Wait()
    fmt.Printf("waited %v for goroutine to stop\n", time.Since(t0))
}

上面的例子,因为finish在主协程中发送之后,马上就会在select中接收,并执行done.Done()。主协程wait马上会退出整个程序就结束。但是这里面存在一个问题,如果在select中没有添加finish case的话,主协程就永远发送不了数据到finish这个channel,因为其不带缓存。这里就可以通过将finish改成带缓存的channel,或者可以让select中的finish不会阻塞。

但是出现多个协程都在接收finish通道中的数据的话,就需要发送对应协程数量的值到channel中才能解决上面的问题。但是具体有多少个协程这往往是不好确定的,因为有些协程可能是程序其他部分创建的。一个比较好的选择就是通过使用关闭通道的方法来实现各协程能正常接收并结束。
如下所示:

package main

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

func main() {
    const n = 100
    finish := make(chan bool)
    var done sync.WaitGroup
    for i := 0; i < n; i++ {
        done.Add(1)
        go func() {
            select {
            case <-time.After(1 * time.Hour):
            case <-finish:
            }
            done.Done()
        }()
    }
    t0 := time.Now()
    close(finish)
    done.Wait()
    fmt.Printf("waited %v for %d goroutine to stop\n", time.Since(t0), n)
}

output:

waited 0s for 100 goroutine to stop

上面的例子就是使用了关闭的channel可以无限地接收到反馈数据。这样每个协程都能从finish通道中读到关闭信息并执行done.Done()使得主协程wait能退出。并且不需要关注多少个协程数,就能正确的让所有协程读到finish通道信息。
channel的这个特性,可以让程序员无需关注后台具体执行协程个数,确保每个协程都能接收到通道关闭信息,而无需担心死锁问题。

通过上面的例子我们也发现每个协程并不需要从通道中读取对应类型的数据,只需让接收操作能执行就行,让select不被阻塞。所以可以使用空结构体类型,我们可以改成如下:

package main

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

func main() {
    const n = 100
    finish := make(chan struct{})
    var done sync.WaitGroup
    for i := 0; i < n; i++ {
        done.Add(1)
        go func() {
            select {
            case <-time.After(1 * time.Hour):
            case <-finish:
            }
            done.Done()
        }()
    }
    t0 := time.Now()
    close(finish)
    done.Wait()
    fmt.Printf("waited %v for %d goroutine to stop\n", time.Since(t0), n)
}

这里我们只关注通道是否关闭这个信号,而不需要关注通道里面的数据,所以可使用空结构体类型通道。

nil通道总是阻塞的

第二个要讨论的是nil通道:如果定义了一个channel变量没有被初始化,或者被赋值为nil,那么该chennel总是处于阻塞状态。如下所示:

package main

func main() {
    var ch chan bool
    ch <- true
}

执行结果为:

fatal error: all goroutines are asleep - deadlock!

因为channel为nil无法发送数据,当然也不能接收数据:

package main

func main() {
    var ch chan bool
    <-ch  //all goroutines are asleep - deadlock!
}

这个似乎看起来不是很重要,但是如果你想使用关闭channel来等待多个channel关闭的话,这个特性就有用处了。先看下面的例子:

// WaitMany waits for a and b to close.
func WaitMany(a, b chan bool) {
        var aclosed, bclosed bool
        for !aclosed || !bclosed {
                select {
                case <-a:
                        aclosed = true
                case <-b:
                        bclosed = true
                }
        }
}

WaitMany()函数看起来好像是一个等待通道a和b关闭的好方法,但是存在一个问题。假设a通道先关闭,case <-a就会变成非阻塞。因为bclosed还是false,程序就会进入到一个死循环当中,导致b通道永远无法确认关闭。
一个安全的方法就是使用nil通道总是阻塞的特点,如下所示:

package main

import (
    "fmt"
    "time"
)

func WaitMany(a, b chan bool)  {
    for a != nil || b != nil{
        select {
        case <-a:
            a = nil
        case <-b:
            b = nil
        }
    }
}

func main() {
    a, b := make(chan bool), make(chan bool)
    t0 := time.Now()
    go func() {
        close(a)
        close(b)
    }()
    WaitMany(a, b)
    fmt.Printf("waited %v for WaitMany\n", time.Since(t0))
}

上面的例子我们在WaitMany函数当中,当a或者b关闭时,case可执行了将对应的通道赋值为nil,让其阻塞这样就可以等待另一个通道关闭。当nil通道是select语句的一部分时,它会被有效地忽略,因此nil通道a会从select中删除它,只留下b,直到它被关闭,退出循环。

总之,closed和nil通道的简单属性对写出优质的go程序是很有用的,可以用来创建高并发程序。

赞(0) 打赏
未经允许不得转载:IDEA激活码 » channel使用

一个分享Java & Python知识的社区