channel通道特性和应用

一、channel特性

无缓冲channel特性

1. 通信和同步(单方面的读或写会阻塞)
2. 一发送,多接受,只有一个goroutine能接收到
3. 一接收,多发送,发送端goroutine只能阻塞,一个一个goroutine抢占式发送
4. 关闭一个channel,所有接收端都会收到停止阻塞的信号,并不会收到实际消息
5. len(c)总是返回0
6. 往关闭的channel中发送数据会panic
7. 从关闭的channel中读取数据会读到零值

带缓冲channel特性

1. 缓冲区未满时,发送操作不会阻塞
2. 缓冲区未空时,接收操作不会阻塞
3. 带缓冲channel性能优于无缓冲channel
4. len(s)返回当前channel s中尚未被读取的元素个数
5. 往关闭的channel中发送数据会panic

nil channel特性

1. nil channel 读写会阻塞
2. 避免向nil channel中读写,这是编程错误.

二、channel-应用

无缓冲channel

//无缓冲channel兼具通信和同步特性
c := make(chan T) // T为channel中元素的类型
1、用作信号传递
//1、一对一通知信号
type signal struct{}
func worker() {
    println("worker is working...")
    time.Sleep(1 * time.Second)
}
func spawn(f func()) <-chan signal {
    c := make(chan signal)
    go func() {
        println("worker start to work...")
        f()
        c <- signal(truct{}{})
    }()
    return c
}
func main() {
    println("start a worker...")
    c := spawn(worker)
    <-c
    fmt.Println("worker work done!")
}
//2、一对多通知信号,利用close(chan)会通知所有接收chan的goroutine的机制
type signal struct{}
func worker(i int) {
    fmt.Printf("worker %d: is working...\n", i)
    time.Sleep(1 * time.Second)
    fmt.Printf("worker %d: works done\n", i)
}
func spawnGroup(f func(i int), num int, groupSignal <-chan signal) <-chan signal {
    c := make(chan signal)
    var wg sync.WaitGroup
    for i := 0; i < num; i++ {
        wg.Add(1)
        go func(i int) {
            <-groupSignal
            fmt.Printf("worker %d: start to work...\n", i)
            f(i)
            wg.Done()
        }(i + 1)
    }
    go func() {
        wg.Wait()
        c <- signal(struct{}{})
    }()
    return c
}
func main() {
    fmt.Println("start a group of workers...")
    groupSignal := make(chan signal)
    c := spawnGroup(worker, 5, groupSignal)
    time.Sleep(5 * time.Second)
    fmt.Println("the group of workers start to work...")
    close(groupSignal)
    <-c
    fmt.Println("the group of workers work done!")
}
//
type signal struct{}
func worker(i int, quit <-chan signal) {
    fmt.Printf("worker %d: is working...\n", i)
LOOP:
    for {
        select {
        default:
            // 模拟worker工作
            time.Sleep(1 * time.Second)
        case <-quit:
            break LOOP
        }
    }
    fmt.Printf("worker %d: works done\n", i)
}
func spawnGroup(f func(int, <-chan signal), num int, groupSignal <-chan signal) <-chan signal {
    c := make(chan signal)
    var wg sync.WaitGroup
    for i := 0; i < num; i++ {
        wg.Add(1)
        go func(i int) {
            fmt.Printf("worker %d: start to work...\n", i)
            f(i, groupSignal)
            wg.Done()
        }(i + 1)
    }
    go func() {
        wg.Wait()
        c <- signal(struct{}{})
    }()
    return c
}
func main() {
    fmt.Println("start a group of workers...")
    groupSignal := make(chan signal)
    c := spawnGroup(worker, 5, groupSignal)
    fmt.Println("the group of workers start to work...")
    time.Sleep(5 * time.Second)
    // 通知workers退出
    fmt.Println("notify the group of workers to exit...")
    close(groupSignal)
    <-c
    fmt.Println("the group of workers work done!")
}
2、用于替代锁机制
//无缓冲channel具有同步特性,这让它在某些场合可以替代锁,从而使得程序更加清晰,可读性更好。
//共享内存+锁模式
// chapter6/sources/go-channel-case-5.go
type counter struct {
    sync.Mutex
    i int
}
var cter counter
func Increase() int {
    cter.Lock()
    defer cter.Unlock()
    cter.i++
    return cter.i
}
func main() {
    for i := 0; i < 10; i++ {
        go func(i int) {
            v := Increase()
            fmt.Printf("goroutine-%d: current counter value is %d\n", i, v)
        }(i)
    }
    time.Sleep(5 * time.Second)
}
//无缓冲channel替代锁后的实现。这种并发设计逻辑更符合Go语言所倡导的“不要通过共享内存来通信,而应该通过通信来共享内存”的原则。
// chapter6/sources/go-channel-case-6.go
type counter struct {
    c chan int
    i int
}
var cter counter
func InitCounter() {
    cter = counter{
        c: make(chan int),
    } 
    go func() {
        for {
                cter.i++
                cter.c <- cter.i
        }
    }()
    fmt.Println("counter init ok")
}
func Increase() int {
    return <-cter.c
}
func init() {
    InitCounter()
}
func main() {
    for i := 0; i < 10; i++ {
        go func(i int) {
            v := Increase()
            fmt.Printf("goroutine-%d: current counter value is %d\n", i, v)
        }(i)
    }
    time.Sleep(5 * time.Second)
}

带缓冲channel

c := make(chan T, capacity) // T为channel中元素的类型, capacity为带缓冲channel的缓冲区容量
1、用作消息队列

​ 无论是单收单发还是多收多发,带缓冲channel的收发性能都要好于无缓冲channel的;对于带缓冲channel而言,选择适当容量会在一定程度上提升收发性能。

2、用作计数信号量

​ Go并发设计的一个惯用法是将带缓冲channel用作计数信号量(counting semaphore)。带缓冲channel中的当前数据个数代表的是当前同时处于活动状态(处理业务)的goroutine的数量,而带缓冲channel的容量(capacity)代表允许同时处于活动状态的goroutine的最大数量。一个发往带缓冲channel的发送操作表示获取一个信号量槽位,而一个来自带缓冲channel的接收操作则表示释放一个信号量槽位。

// chapter6/sources/go-channel-case-7.go
var active = make(chan struct{}, 3)
var jobs = make(chan int, 10)

func main() {
    go func() {
        for i := 0; i < 8; i++ {
            jobs <- (i + 1)
        }
        close(jobs)
    }()
    
    var wg sync.WaitGroup
    
    for j := range jobs {
        wg.Add(1)
        go func(j int) {
            active <- struct{}{}
            log.Printf("handle job: %d\n", j)
            time.Sleep(2 * time.Second)
            <-active
            wg.Done()
        }(j)
    }
    wg.Wait()
}
3、len(channel)的应用

如果s是字符串(string)类型,len(s)返回字符串中的字节数;

当s为无缓冲channel时,len(s)总是返回0;

当s为带缓冲channel时,len(s)返回当前channel s中尚未被读取的元素个数。

//一旦多个goroutine共同对channel进行收发操作,那么len(channel)就会在多个goroutine间形成竞态,单纯依靠len(channel)来判断channel中元素的状态,不能保证在后续对channel进行收发时channel的状态不变。
//goroutine1在使用len(channel)判空后,便尝试从channel中接收数据。但在其真正从channel中读数据前,goroutine2已经将数据读了出去,goroutine1后面的读取将阻塞在channel上,导致后面逻辑失效。因此,为了不阻塞在channel上,常见的方法是将判空与读取放在一个事务中,将判满与写入放在一个事务中,而这类事务我们可以通过select实现。
// chapter6/sources/go-channel-case-8.go   
func producer(c chan<- int) {
    var i int = 1
    for {
        time.Sleep(2 * time.Second)
        ok := trySend(c, i)
        if ok {
            fmt.Printf("[producer]: send [%d] to channel\n", i)
            i++
            continue
        }
        fmt.Printf("[producer]: try send [%d], but channel is full\n", i)
    }
}

func tryRecv(c <-chan int) (int, bool) {
    select {
    case i := <-c:
        return i, true
    default:
        return 0, false
    }
}

func trySend(c chan<- int, i int) bool {
    select {
    case c <- i:
        return true
    default:
        eturn false
    }
}

func consumer(c <-chan int) {
    for {
        i, ok := tryRecv(c)
        if !ok {
            fmt.Println("[consumer]: try to recv from channel, but the channel is empty")
            time.Sleep(1 * time.Second)
            continue
        }
        fmt.Printf("[consumer]: recv [%d] from channel\n", i)
        if i >= 3 {
            fmt.Println("[consumer]: exit")
            return
        }
    }
}

func main() {
    c := make(chan int, 3)
    go producer(c)
    go consumer(c)
    
    select {} // 仅用于演示,临时用来阻塞主goroutine
}

nil channel的妙用

//该例预期打印5,7后退出,但是打印5后还会从关闭的通道接收打印0,再打印7退出.
func main() {
    c1, c2 := make(chan int), make(chan int)
    go func() {
        time.Sleep(time.Second * 5)
        c1 <- 5
        close(c1)
    }()
    
    go func() {
        time.Sleep(time.Second * 7)
        c2 <- 7
        close(c2)
    }()
    
    var ok1, ok2 bool
    for {
        select {
        case x := <-c1:
            ok1 = true
            fmt.Println(x)
        case x := <-c2:
            ok2 = true
            fmt.Println(x)
        }
        
        if ok1 && ok2 {
            break
        }
    }
    fmt.Println("program end")
}
//关闭通道后,将通道置为空,通过通道是否都为空判断程序是否退出,省去定义bool变量的空间.
func main() {
    c1, c2 := make(chan int), make(chan int)
    go func() {
        time.Sleep(time.Second * 5)
        c1 <- 5
        close(c1)
    }()
    
    go func() {
        time.Sleep(time.Second * 7)
        c2 <- 7
        close(c2)
    }()
    
    for {
        select {
        case x, ok := <-c1:
            if !ok {
                c1 = nil
            } else {
                fmt.Println(x)
            }
        case x, ok := <-c2:
                if !ok {
                    c2 = nil
                } else {
                    fmt.Println(x)
                }
        }
        if c1 == nil && c2 == nil {
            break
        }
    }
    fmt.Println("program end")
}
posted @ 2023-11-10 20:20  longan55  阅读(471)  评论(0)    收藏  举报