go-channel使用(3)

通过反射的方式执行 select 语句,在处理很多的 case clause,尤其是不定长的 case clause 的时候,非常有用。而且,在后面介绍任务编排的实现时,我也会采用这种方法,所以,我先带你具体学习下 Channel 的反射用法。

使用反射操作 Channel

select可以处理chan的send和recv,它们都可以作为case clause。
当用select处理不定长case clause时,可以使用reflect.SelectCase方法

func main() {
	ch1 := make(chan int, 10)
	ch2 := make(chan int, 20)

	cases := createCases(ch1, ch2)

	for i := 0; i < 10; i++ {
		chosen, recv, ok := reflect.Select(cases)
			if recv.IsValid() { // recv case
				if ok {
					fmt.Printf("recv data case from index[%v], Value: %v, ok: %v\n", chosen, recv, ok)
				}else{
					fmt.Printf("recv close case from index[%v], Value: %v, ok: %v\n", chosen, recv, ok)
					// 已close的chan的receive和send都移除
				}
			}else {				// send case
				//fmt.Println("send:", chosen, recv, ok)
				fmt.Printf("send case from index[%v], Value: %v, ok: %v\n", chosen, recv, ok)
			}
	}
	fmt.Printf("main end\n")
}

典型应用场景

消息交流

从 chan 的内部实现看,它是以一个循环队列的方式存放数据,所以,它有时候也会被当成线程安全的队列和 buffer 使用。一个 goroutine 可以安全地往 Channel 中塞数据,另外一个 goroutine 可以安全地从 Channel 中读取数据,goroutine 就可以安全地实现信息交流了。
(即把chan当作一个消息池,消息或数据都存放在chan中?
那么容量是否有要求呢?

数据传递

比如之前讲的按顺序打印值,这类场景都有一个特点,就是当前持有数据的 goroutine 都有一个信箱,信箱使用 chan 实现,goroutine 只需要关注自己的信箱中的数据,处理完毕后,就把结果发送到下一家的信箱中。

信号通知

chan类型有这个特点:如果为空,那么receive接收数据时就会阻塞等待,直到chan被关闭或有新数据到来。利用此机制,可以实现wait/notify设计模式。

传统的cond并发原语也能实现此功能,但使用比较复杂,容易出错,而使用 chan 实现 wait/notify 模式,就方便多了。

除了正常的业务处理时的 wait/notify,我们经常碰到的一个场景,就是程序关闭的时候,我们需要在退出之前做一些清理(doCleanup 方法)的动作。这个时候,我们经常要使用 chan。


func main() {
    var closing = make(chan struct{})
    var closed = make(chan struct{})

    go func() {
        // 模拟业务处理
        for {
            select {
            case <-closing:
                return
            default:
                // ....... 业务计算
                time.Sleep(100 * time.Millisecond)
            }
        }
    }()

    // 处理CTRL+C等中断信号
    termChan := make(chan os.Signal)
    signal.Notify(termChan, syscall.SIGINT, syscall.SIGTERM)
    <-termChan

    close(closing)
    // 执行退出之前的清理动作
    go doCleanup(closed)

    select {
    case <-closed:
    case <-time.After(time.Second):
        fmt.Println("清理超时,不等了")
    }
    fmt.Println("优雅退出")
}

func doCleanup(closed chan struct{}) {
    time.Sleep((time.Minute))
    close(closed)
}

closing,代表程序退出,但是清理工作还没做;
closed,代表清理工作已经做完。

使用 chan 也可以实现互斥锁。

在 chan 的内部实现中,就有一把互斥锁保护着它的所有字段。从外在表现上,chan 的发送和接收之间也存在着 happens-before 的关系,保证元素放进去之后,receiver 才能读取到(关于 happends-before 的关系,是指事件发生的先后顺序关系,我会在下一讲详细介绍,这里你只需要知道它是一种描述事件先后顺序的方法)。

要想使用 chan 实现互斥锁,至少有两种方式。一种方式是先初始化一个 capacity 等于 1 的 Channel,然后再放入一个元素。这个元素就代表锁,谁取得了这个元素,就相当于获取了这把锁。另一种方式是,先初始化一个 capacity 等于 1 的 Channel,它的“空槽”代表锁,谁能成功地把元素发送到这个 Channel,谁就获取了这把锁。

package mychanmutex

import "time"

type Mutex struct {
	ch chan struct{}
}

// NewMutex 初始化时放入一个值
func NewMutex() *Mutex {
	mu := &Mutex{ch: make(chan struct{}, 1)}
	mu.ch <- struct{}{}
	return mu
}


func (m *Mutex) Lock() {
	<- m.ch
}

func (m *Mutex) Unlock() {
	select {
		case m.ch <- struct{}{}:
	default:
		panic("unlock of unlocked mutex")
	}
}

func (m *Mutex) TryLock() bool {
	select {
		case <- m.ch:
			return true
		default:
	}
	return false
}

func (m *Mutex) LockTimeout(timeout time.Duration) bool {
	select {
		case <- m.ch:
			return true
		case <- time.After(timeout):
			return false
	}
}

func (m *Mutex) IsLocked() bool {
	return len(m.ch) == 0
}

任务编排

前面所说的消息交流的场景是一个特殊的任务编排的场景,这个“击鼓传花”的模式也被称为流水线模式。

在第 6 讲,我们学习了 WaitGroup,我们可以利用它实现等待模式:启动一组 goroutine 执行任务,然后等待这些任务都完成。其实,我们也可以使用 chan 实现 WaitGroup 的功能。这个比较简单,我就不举例子了,接下来我介绍几种更复杂的编排模式。

这里的编排既指安排 goroutine 按照指定的顺序执行,也指多个 chan 按照指定的方式组合处理的方式。goroutine 的编排类似“击鼓传花”的例子,我们通过编排数据在 chan 之间的流转,就可以控制 goroutine 的执行。接下来,我来重点介绍下多个 chan 的编排方式,总共 5 种,分别是 Or-Done 模式、扇入模式、扇出模式、Stream 和 map-reduce。

Or-Done 模式

首先来看 Or-Done 模式。Or-Done 模式是信号通知模式中更宽泛的一种模式。这里提到了“信号通知模式”,我先来解释一下。

我们会使用“信号通知”实现某个任务执行完成后的通知机制,在实现时,我们为这个任务定义一个类型为 chan struct{}类型的 done 变量,等任务结束后,我们就可以 close 这个变量,然后,其它 receiver 就会收到这个通知。

这是有一个任务的情况,如果有多个任务,只要有任意一个任务执行完,我们就想获得这个信号,这就是 Or-Done 模式。

比如,你发送同一个请求到多个微服务节点,只要任意一个微服务节点返回结果,就算成功,这个时候,就可以参考下面的实现:


func or(channels ...<-chan interface{}) <-chan interface{} {
    //特殊情况,只有0个或者1个
    switch len(channels) {
    case 0:
        return nil
    case 1:
        return channels[0]
    }

    orDone := make(chan interface{})
    go func() {
        defer close(orDone)
        // 利用反射构建SelectCase
        var cases []reflect.SelectCase
        for _, c := range channels {
            cases = append(cases, reflect.SelectCase{
                Dir:  reflect.SelectRecv,
                Chan: reflect.ValueOf(c),
            })
        }

        // 随机选择一个可用的case
        reflect.Select(cases)
    }()


    return orDone
}

这里reflect.Select只要其中有chan可读,则会返回,然后close(orDone).

扇入模式

扇入借鉴了数字电路的概念,它定义了单个逻辑门能够接受的数字信号输入最大量的术语。一个逻辑门可以有多个输入,一个输出。

在软件工程中,模块的扇入是指有多少个上级模块调用它。而对于我们这里的 Channel 扇入模式来说,就是指有多个源 Channel 输入、一个目的 Channel 输出的情况。扇入比就是源 Channel 数量比 1。

每个源 Channel 的元素都会发送给目标 Channel,相当于目标 Channel 的 receiver 只需要监听目标 Channel,就可以接收所有发送给源 Channel 的数据。

扇入模式也可以使用反射、递归,或者是用最笨的每个 goroutine 处理一个 Channel 的方式来实现。

把每个input channel里数据读出,写如到output channel中:


func fanInReflect(chans ...<-chan interface{}) <-chan interface{} {
    out := make(chan interface{})
    go func() {
        defer close(out)
        // 构造SelectCase slice
        var cases []reflect.SelectCase
        for _, c := range chans {
            cases = append(cases, reflect.SelectCase{
                Dir:  reflect.SelectRecv,
                Chan: reflect.ValueOf(c),
            })
        }
        
        // 循环,从cases中选择一个可用的
        for len(cases) > 0 {
            i, v, ok := reflect.Select(cases)
            if !ok { // 此channel已经close
                cases = append(cases[:i], cases[i+1:]...)
                continue
            }
            out <- v.Interface()
        }
    }()
    return out
}

扇出模式

有扇入模式,就有扇出模式,扇出模式是和扇入模式相反的。

扇出模式只有一个输入源 Channel,有多个目标 Channel,扇出比就是 1 比目标 Channel 数的值,经常用在设计模式中的观察者模式中(观察者设计模式定义了对象间的一种一对多的组合关系。这样一来,一个对象的状态发生变化时,所有依赖于它的对象都会得到通知并自动刷新)。在观察者模式中,数据变动后,多个观察者都会收到这个变更信号。


func fanOut(ch <-chan interface{}, out []chan interface{}, async bool) {
    go func() {
        defer func() { //退出时关闭所有的输出chan
            for i := 0; i < len(out); i++ {
                close(out[i])
            }
        }()

        for v := range ch { // 从输入chan中读取数据
            v := v
            for i := 0; i < len(out); i++ {
                i := i
                if async { //异步
                    go func() {
                        out[i] <- v // 放入到输出chan中,异步方式
                    }()
                } else {
                    out[i] <- v // 放入到输出chan中,同步方式
                }
            }
        }
    }()
}

Stream

这里我来介绍一种把 Channel 当作流式管道使用的方式,也就是把 Channel 看作流(Stream),提供跳过几个元素,或者是只取其中的几个元素等方法。

func asStream(done <-chan struct{}, values ...interface{}) <-chan interface{} {
    s := make(chan interface{}) //创建一个unbuffered的channel
    go func() { // 启动一个goroutine,往s中塞数据
        defer close(s) // 退出时关闭chan
        for _, v := range values { // 遍历数组
            select {
            case <-done:
                return
            case s <- v: // 将数组元素塞入到chan中
            }
        }
    }()
    return s
}

流创建好(创建完需要close)以后,该咋处理呢?下面我再给你介绍下实现流的方法。
takeN:只取流中的前 n 个数据;
takeFn:筛选流中的数据,只保留满足条件的数据;
takeWhile:只取前面满足条件的数据,一旦不满足条件,就不再取;
skipN:跳过流中前几个数据;
skipFn:跳过满足条件的数据;
skipWhile:跳过前面满足条件的数据,一旦不满足条件,当前这个元素和以后的元素都会输出给 Channel 的 receiver。
这些方法的实现很类似,我们以 takeN 为例来具体解释一下。

func takeN(done <-chan struct{}, valueStream <-chan interface{}, num int) <-chan interface{} {
    takeStream := make(chan interface{}) // 创建输出流
    go func() {
        defer close(takeStream)
        for i := 0; i < num; i++ { // 只读取前num个元素
            select {
            case <-done:
                return
            case takeStream <- <-valueStream: //从输入流中读取元素
            }
        }
    }()
    return takeStream
}

map-reduce

map-reduce 分为两个步骤,第一步是映射(map),处理队列中的数据,第二步是规约(reduce),把列表中的每一个元素按照一定的处理方式处理成结果,放入到结果队列中。

func mapChan(in <-chan interface{}, fn func(interface{}) interface{}) <-chan interface{} {
    out := make(chan interface{}) //创建一个输出chan
    if in == nil { // 异常检查
        close(out)
        return out
    }

    go func() { // 启动一个goroutine,实现map的主要逻辑
        defer close(out)
        for v := range in { // 从输入chan读取数据,执行业务操作,也就是map操作
            out <- fn(v)
        }
    }()

    return out
}

func reduce(in <-chan interface{}, fn func(r, v interface{}) interface{}) interface{} {
    if in == nil { // 异常检查
        return nil
    }

    out := <-in // 先读取第一个元素
    for v := range in { // 实现reduce的主要逻辑
        out = fn(out, v)
    }

    return out
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
channel 是 Go 语言中的一个核心特性,它允许不同的 goroutine 之间进行通信和同步。以下是 channel 的一些使用案例: 1. 并发任务的协调 在一个并发任务中,可能需要多个 goroutine 协同工作,彼此之间需要进行数据传递和同步。这时可以使用 channel 来协调不同的 goroutine,确保它们按照正确的顺序执行。例如,可以使用一个 channel 来传递任务和结果,让不同的 goroutine 同时处理任务,最后把结果传回主 goroutine 进行处理。 2. 生产者-消费者模型 在生产者-消费者模型中,一个或多个生产者会生成数据,而一个或多个消费者会消费这些数据。这时可以使用 channel 作为一个缓冲区来存储数据,让生产者将数据写入 channel,让消费者从 channel 中读取数据。这种方式可以避免生产者和消费者之间的竞争条件,确保每个数据都能被正确地处理。 3. 信号量 在某些场景下,需要限制并发的数量,以避免资源的过度消耗。这时可以使用 channel 作为一个信号量,限制同时执行的 goroutine 数量。例如,可以创建一个有缓冲的 channel,容量为 N,然后在每个 goroutine 开始执行前从 channel 中读取一个元素,在执行完成后再将元素写回 channel。这样可以保证同时执行的 goroutine 数量不超过 N。 4. 事件驱动编程 在事件驱动编程中,需要监听一些事件,当事件发生时执行相应的操作。这时可以使用 channel 来实现事件的监听和触发。例如,可以创建一个 channel,然后将它传递给一个事件监听器。当事件发生时,监听器就可以向 channel 中写入一个值,通知主 goroutine 执行相应的操作。 总之,channel 是 Go 语言中非常强大的一种并发编程工具,可以在各种场景下使用,提高程序的并发性能和可靠性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值