chan通道使用

106 阅读3分钟

chan通道使用

不要通过共享内存来通信,而是要通过通信来共享内存

  • 解决问题

0、解决共享资源的竞争状态,可以原子锁或者互斥锁,还可以使用使用通道更优雅处理。

0、通过使用通道,可以实现多个goroutine直接共享资源。

基本使用

chan是golang语言的关键字,chan T,声明通道类型。

ch := make(chan int)

初始化有2种方式

// 无缓冲通道(阻塞通道)
ch := make(chan int)

// 有缓冲通道(非阻塞通道)
ch2 := make(chan int,1)

无缓冲通道(阻塞通道)

是指在接受前没有任何能力保存值的通道,要求发送和接收的goroutine同时准备好,才能完成发送和接收,否则就会产生死锁。

  • 错误使用
func main() {
	ch := make(chan int)
	ch <- 1
	fmt.Println("hello")
}
// fatal error: all goroutines are asleep - deadlock!
  • 正常使用
func main() {
	ch := make(chan int)
	go func() { ch <- 1 }()

	fmt.Println("hello", <-ch)
}
// hello 1

有缓冲通道(非阻塞)

是指在被接收前能够存储一个或者多个值的通道,并不会要求发送和接收同时准备好。

func main() {
	ch := make(chan int, 10)
	ch <- 1
	ch <- 2
	fmt.Println(<-ch)
}
// 1

两者的最大区别就是,在被接收前一个不能存值,一个能存值,无缓冲通道能够保证在同一时间内进行数据交换。有缓冲通道不能保证,因为两者的阻塞时机都不一样。

for和select结合使用

  • 可以使用 for 循环读出chan中的数据
func main() {
	ch := make(chan int, 10)
	go func() {
		for i := 1; i < 10; i++ {
			ch <- i
		}
		close(ch) // 避免下面for,一致阻塞读取导致死锁,
	}()

	for val := range ch {
		fmt.Print(val)
	}
	fmt.Println(" end。。。")
}
// 123456789 end。。。
  • select可以随机读取多个通道,
func main() {
	ch := make(chan string, 10)
	ch2 := make(chan string, 10)
	for {
		select {
		case val1 := <-ch:
			fmt.Println("ch-val1", val1)
		case val2 := <-ch2:
			fmt.Println("ch2-val2", val2)
		case <-time.NewTimer(time.Second).C:
			ch <- time.Now().String()
			ch2 <- time.Now().String()
		}
	}
}
// ch 和 ch2随机读,time.NewTimer(time.Second)创建一个超时器,超过1s则往ch中写新内容

单向通道

避免通道的的写入和读取误用在错误的场景,所以提供了单方向的通道类型,单向通道有利于代码接口的严谨性

func main() {
	// 双向通道
	ch := make(chan int)
	// 写通道
	writeCh := make(chan<- int)
	// 读通道
	readCh := make(<-chan int)
}

双向通道可以转换为 单项通道,反之则不能。

单向通道的使用

type Timer struct {
	C <-chan Time
	r runtimeTimer
}

<-time.NewTimer(time.Second).C

close通道

通道关闭会通知所有正在读取通道的协成,

在实践中,并不需要关心是否所有通道已经关闭,当通道没有被引用是,将会被golang的垃圾自动回收器回收。

close(chan)

生产者和消费者

package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)

var ch = make(chan string, 10)
var wg sync.WaitGroup
var isClose bool // 模拟程序关闭-设置 isClose 并退出生产者

func main() {

	go producer("p1")
	go producer("p2")

	go consumer("c1")

	time.Sleep(10 * time.Second)
	isClose = true
	close(ch)
	wg.Wait() // 等待所有的消费者消费完毕,消息则退出应用程序
}

func producer(name string) {
	for i := 1; i < 5; i++ {
		fmt.Printf("producer[%s]--%d \n", name, i)
		if isClose { // 通道chan已经关闭了
			return
		}
		time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond) // 模拟消费者消费速度
		ch <- fmt.Sprintf("producer[%s]-%d", name, i)
	}
}

func consumer(name string) {
	wg.Add(1)
	for val := range ch {
		time.Sleep(500 * time.Millisecond) // 模拟消费者消费速度
		fmt.Printf("consumer[%s]--%s \n", name, val)
	}
	wg.Done()
}

Q&A

通道那些操作导致painc

  • 关闭一个已经关闭的通道,将会触发panic
func main() {
	ch := make(chan int, 2)
	close(ch)
	close(ch)
}
  • 关闭一个没有初始化的通道,将会触发painc
func main() {
	var ch chan int
	close(ch)
}
  • 向已经关闭的通道写数据,将会触发painc
func main() {
	ch := make(chan int, 2)
	close(ch)
	ch <- 1
}

make chan

  • make(chan int) 和 make(chan int,0) 的性质都是一样的,都是无缓冲通道
  • len 和cap ,len-chan中的现有元素个数是多少,cap-chan中的容量是多大