Golang—channel

1,450 阅读1分钟

通道

1、简介

通道类型的值本身就是并发安全的,这也是Golang语言自带的,唯一一个可以满足并发安全性的类型; channel称为管道,本质是一个先进先出的队列; 使用goroutine+channel进行数据通信简单高效,同时线程安全,多个goroutine可同时修改一个channel,不需要加锁。

2、类型

2.1、收发角度分类

chanel分为三种类型

 //只读 channel, 只能读channel里面的数据,不可写入
var readOnlyChan <- chan int
//只写channel,只能写入数据,不可读
var writeOnlyChan chan<- int 
 //可读可写,一般channel,可以读可以写
var mychan chan int
2.2、是否有缓冲角度分类
// 缓存通道
data := make(chan int, 3)

// 非缓存通道:
data := make(chan int)

3、对通道的发送和接收操作有哪些特性特性

  • 对于同一个通道,发送操作之间是互斥的,接收操作也是互斥的
  • 发送操作和接收操作中对元素值的处理都是不可分割的
  • 发送操作在完全完成之前会被阻塞,接收操作也是如此

4、什么情况会出现阻塞?

4.1、对于缓冲通道

对于缓冲通道,如果通道已满,那么对它的所有发送操作都会被阻塞,直到通道中有元素值被接收;

func main() {
    data := make(chan int1)
    data <- 1
    log.Println("向缓冲通道写入")
    data <- 2
}
// output
2020/08/04 21:19:14 向缓冲通道写入
fatal error: all goroutines are asleep - deadlock!

如果通道已经为空,那么对它的所有接收操作都被会阻塞,直到通道中有新的元素出现;

func main() {
    data := make(chan int1)
    log.Println("从缓冲通道读取")
    <-data
}
// output
2020/08/04 21:24:58 从缓冲通道读取
fatal error: all goroutines are asleep - deadlock!
4.2、对于非缓冲通道

通道中无数据,执行读通道

func main() {
    data := make(chan int)
    <-data
    log.Println("从非缓冲通道读取")
}

// output
fatal error: all goroutines are asleep - deadlock!

通道中无数据,向通道写数据,无协程读取

func main() {
    data := make(chan int)
    data <- 1
    log.Println("向非缓冲通道写入")
}

// output
fatal error: all goroutines are asleep - deadlock!

4.3、对于值为nil的通道

无论他的具体类型是什么,对它的发送操作,和接收操作都会永久的处于阻塞状态;所属的goroutine中的任何代码,都不再会执行。

func main() {
    var data chan int
    data <- 1
    log.Println("空的通道")
}
// output
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send (nil chan)]:

5、select

5.1、简介

select是执行选择操作的一个结构,包含一组case语句,会执行其中无阻塞的某一个case,如果全部阻塞了,那就等待其中一个不阻塞进而继续执行;还有一个defalut语句,default语句永远不会阻塞,可以借助它实现无阻塞操作。

5.2、使用select解决非缓冲通道读取的阻塞问题
func main() {
    data := make(chan int)
    if v, err := readWithSelect(data); err != nil {
        log.Println(err)
    } else {
        log.Println("read", v)
    }
}
func readWithSelect(ch chan int) (interror) {
    select {
    case x := <-ch:
        return x, nil
    default:
        return 0, errors.New("channel has no data")
    }
}

// output
2020/08/05 09:58:39 channel has no data
5.3、使用select解决缓冲通道读取的阻塞问题
func main() {
    data := make(chan int, 1)
    if v, err := readWithSelect(data); err != nil {
        log.Println(err)
    } else {
        log.Println("read", v)
    }
}
func readWithSelect(ch chan int) (interror) {
    select {
    case x := <-ch:
        return x, nil
    default:
        return 0, errors.New("channel has no data")
    }
}
// output
2020/08/05 10:04:06 channel has no data
5.4、使用select解决非缓冲通道写入的阻塞问题
func main() {
    data := make(chan int)
    if err := writeWithSelect(data); err != nil {
        log.Println(err)
    } else {
        log.Println("write success")
    }
}
// writeWithSelect
// 通过select进行写入操作
func writeWithSelect(ch chan int) error {
    select {
    case ch <- 1:
        return nil
    default:
        return errors.New("The pipe is full")
    }
}
// outputt
2020/08/05 10:10:58 The pipe is full
5.5、使用select解决缓冲通道写入的阻塞问题
func main() {
    data := make(chan int1)
    data <- 1
    if err := writeWithSelect(data); err != nil {
        log.Println(err)
    } else {
        log.Println("write success")
    }
}
// writeWithSelect
// 通过select进行写入操作
func writeWithSelect(ch chan int) error {
    select {
    case ch <- 1:
        return nil
    default:
        return errors.New("The pipe is full")
    }
}

// output
2020/08/05 10:12:02 The pipe is full
5.6、使用select+time实现超时写入或读取
// 读取超时
func readWithSelectTime(ch chan int) (interror) {
    timeout := time.NewTimer(time.Microsecond * 500)
    select {
    case x := <-ch:
        return x, nil
    case <-timeout.C:
        return 0, errors.New("read timeout")
    }
}
// 写入超时
func writeWithSelectTime(ch chan int) error {
    timeout := time.NewTimer(time.Microsecond * 500)
    select {
    case ch <- 1:
        return nil
    case <-timeout.C:
        return errors.New("write time out")
    }
}

6、什么情况会引发panic?

对于一个已初始化,但未关闭的通道来说,收发操作一定不会引发panic;但是通道一旦关闭,再对它进行“发送”操作,就会引发panic。 试图关闭一个已经关闭了通道,也会引发panic。

关闭一个nil的通道

func main() {
    var data chan int
    close(data)
}
// output
panic: close of nil channel

关闭通道再进行“写入”操作

func main() {
    data := make(chan int1)
    close(data)
    log.Println("关闭通道")
    data <- 1
}
// output
2020/08/06 20:28:33 关闭通道
panic: send on closed channel

7、应用场景

7.1、超时

time.After会在另一线程经过时间段d后向返回值发送当时的时间

// withChannelTime
// 通过channel进行超时控制
func withChannelTime() {
    done := make(chan int1)
    // 协程写入
    go func() {
        time.Sleep(2 * time.Second)
        done <- 1
    }()
    for {
        select {
        case work := <-done:
            log.Println("sucess", work)
        case <-time.After(1 * time.Second):
            log.Println("timeout")
        }
    }
}
// output
$ go run main.go
2020/08/06 09:47:35 timeout
2020/08/06 09:47:36 sucess 1
2020/08/06 09:47:37 timeout
2020/08/06 09:47:38 timeout
7.2、取最快的结果
func main() {
    data := make(chan string5)
    for i := 0; i < 5; i++ {
        go func(num int) {
            data <- task(strconv.Itoa(num))
        }(i)
    }
    log.Println("最快的结果", <-data)
}
func task(link string) string {
    randNumber := rand.Intn(1000)
    time.Sleep(time.Duration(randNumber) * time.Millisecond)
    log.Println(link, randNumber)
    return link
}
// output
2020/08/06 20:55:12 2 59
2020/08/06 20:55:12 最快的结果 2
7.3、协程之间的通知

启动两个线程, 一个输出 1,3,5,7…99, 另一个输出 2,4,6,8…100 最后 STDOUT 中按序输出 1,2,3,4,5…100?


// withChannel
// 使用管道方式进行通知
func withChannel() {
    var wg sync.WaitGroup
    var signal = make(chan struct{}, 1)
    var signa2 = make(chan struct{}, 1)
    wg.Add(2)
    // 输出基数
    go func() {
        defer func() {
            wg.Done()
        }()
        for i := 1; i <= 100; i += 2 {
            if i != 1 {
                // 读取数据,没有数据时阻塞
                <-signal
            }
            // <-signal
            log.Println("goroutine1->", i)
            signa2 <- struct{}{}
        }
    }()
    // 输出偶数
    go func() {
        defer func() {
            wg.Done()
        }()
        for i := 2; i <= 100; i += 2 {
            <-signa2
            log.Println("goroutine2->", i)
            signal <- struct{}{}
        }
    }()
    wg.Wait()
}

7.4、多个协程同步响应
func main() {
    data := make(chan struct{})
    for i := 0; i < 5; i++ {
        go task(data)
    }
    close(data)
    time.Sleep(1 * time.Second)
}
func task(ch <-chan struct{}) {
    <-ch
    log.Println("接收")
}
// output
2020/08/06 22:07:28 接收
2020/08/06 22:07:28 接收
2020/08/06 22:07:28 接收
2020/08/06 22:07:28 接收
2020/08/06 22:07:28 接收
限制最大并发数

通过管道数量限制最大的并发数

func main() {
    // 限制最大并发数
    data := make(chan struct{}, 2)
    for i := 0; i < 5; i++ {
        data <- struct{}{}
        go task()
        <-data
    }
    time.Sleep(2 * time.Second)
}
func task() {
    log.Println("任务")
}
2020/08/07 09:51:07 任务
2020/08/07 09:51:07 任务
2020/08/07 09:51:07 任务
2020/08/07 09:51:07 任务
2020/08/07 09:51:07 任务

注意事项

  • 管道如果未关闭,在读取超时则会引发deadlock异常
  • 管道如果关闭进行写入数据会发panic
  • 当管道没有数据时候在行读取活读取到默认值
  • 元素值从外界进入通道会被“复制”(具体描述,进入通道的并不是接收操作符右边的元素值,而是它的副本)
  • 接收操作是可以感知通道的关闭的并能够安全退出

问题

如果在select语句中发现某个通道已经关闭,那么应该如何屏蔽调他所在的分支?【设置nil】

func main() {
    work := asChan(12)
    for {
        select {
        case rs, ok := <-work:
            if !ok {
                work = nil
                //break
            }
            log.Println(rs)
            time.Sleep(2 * time.Second)
        default:
            log.Println("无选择")
            time.Sleep(2 * time.Second)
        }
    }
}
func asChan(vs ...int) chan int {
    ch := make(chan int)
    go func() {
        for _, v := range vs {
            ch <- v
            time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
        }
        close(ch)
    }()
    return ch
}
// output
2020/08/06 11:23:04 无选择
2020/08/06 11:23:06 1
2020/08/06 11:23:08 2
2020/08/06 11:23:10 0
2020/08/06 11:23:12 无选择

在select语句与for语句联用是,怎样直接退出外层for语句?
答:将条件写在for循环中

通道的长度代表着什么?它在什么时候与通道的容量相同?
答:长度代表通道当前包含的元素个数,容量就是初始化设置的值

参考

Golang并发:一招掌握无阻塞通道读写
Go Channel 高级实践
为什么 Go 会有 nil channels