Go语言学习之路-12-并发(2)-channel

channel

什么是channel

channel就是一个先进先出的队列,就是一种排队的机制通道,分解它的属性:

  • 排队
  • 阻塞(通道满了就等等着,好比去火车站排队一样,没有人检票了就的在检票口等着)
  • 先进先出

CSP(Communicating Sequential Processes)通信实现数据共享

Go语言CSP理念是:通过通信共享数据,而不是通过共享内存共享数据

channel的作用就是实现CSP理念的基石

goroutine和channel使用

goroutine和channel分别解决的什么问题

  • goroutine解决了并发的问题
  • channel解决了并发通信的问题,并通过CSP理念解决了并发goroutine的数据共享

channel的基本使用

创建通道

channel是一种类型并且是一种引用类型。所以在创建channel的时候需要make

make(chan 元素类型, [缓冲队列长度])

// 如果不设定缓队列的长度,就是无缓冲队列
ch1 := make(chan int)
// 如果设定了缓冲队列的长度,就是有缓冲队列,他们有什么不同我们下面继续看

给channel发送和接收消息

channel有三种操作:

  • 发送
  • 接收
  • 关闭

发送和接收都是通过: <-

发送(给通道发消息)

// 给通道发送一个消息
ch1 <- 1

接收(从通道接受消息)

// 接收一个消息并赋值
v1 := <-ch1
// 接收一个消息并忽略这个值
<-ch1

关闭channel

close关闭channel

// 关闭channel
close(ch1)

判断channel是否关闭

package main

import (
	"fmt"
	"time"
)

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

	ch1 <- 1
	ch1 <- 2
	ch1 <- 3

	close(ch1)

	for {
		i,ok := <- ch1

		if ok {
			fmt.Printf("channel是正常的,获取的值是:%v\n",i)
		}else {
			fmt.Printf("channel是关闭的, 需要退出\n")
			break
		}
		time.Sleep(time.Second)
	}
}

for range从通道循环取值

虽然一个通道关闭后在往里面写入元素会产生panic,但是channel关闭后可以继续读里面的元素,通过for range读完就可以正常推出

注意:如果这个通道没有关闭使用for range会产生panic,因为如果通道没有关闭的话会一直卡着死锁~

package main

import "fmt"

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

	// 1 循环往ch1里写数据
	go func(){
		for i := 1; i < 101; i++ {
			ch1 <- i
		}
		// 这里注意用完ch1, 这里要关闭ch1
		close(ch1)
	}()

	// 2 循环从ch1里取数据,并求平方然后写入到ch2去
	go func() {
		for j := range ch1{
			ch2 <- j * j
		}
		// 当数据写完后,注意这里要关闭ch2
		close(ch2)
	}()

	// 3 循环取结果
	for r := range ch2 {
		fmt.Println(r)
	}
}

通道(channel)的例子

无缓冲通道

看实际的例子:

package main

import "fmt"

func main() {
       // 如果不设定缓队列的长度,就是无缓冲队列
	ch1 := make(chan int)
	// 接收一个消息并赋值
	v1 := <-ch1
	// 接收一个消息并忽略这个值
	fmt.Println(v1)
	// 关闭channel
	close(ch1)
}

但是会出现异常(死锁): fatal error: all goroutines are asleep - deadlock!

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
	/Users/apple/code/2del/g1/main.go:9 +0x73

无缓冲通道死锁原因

**无缓冲的通道只有在有人接收值的时候才能发送值**

举个栗子: 你去银行办理业务,你去办理业务了但是没有业务员没人帮你干活只能干着急,一样的道理,要给无缓冲通道发消息必须有接收方?--> 启动一个goroutine接收消息

package main

import (
	"fmt"
)

func recv(c chan int) {
	ret := <-c
	fmt.Println("接收成功", ret)
}
func main() {
	ch := make(chan int)
	go recv(ch) // 启用goroutine从通道接收值
	ch <- 100
	fmt.Println("发送成功")
}
  1. 无缓冲通道,发送会阻塞,直到另一个goroutine开始接收channel里的数据(你去办理银行办理业务,他们银行只有一个业务员,但是当前没有上班,所以只能阻塞(等着),直到业务员开始办理业务了才可以)
  2. 使用无缓冲通道导致发送接收同步化(一个发一个收),就成为了串行的同步的情况了,所以无缓冲通道又称为:同步通道

有缓冲通道

解决:无缓冲通道带来的同步问题,可以使用有缓冲通道,让消息可以进行异步处理~

只要channel的容量大于0,就是无缓冲通道(现在你这个银行[channel]有多个业务员了可以同时的处理问题了,就1个业务员的时候只能串行来,多个就可以并行了)

package main

import (
	"fmt"
	"time"
)

func test(ch chan int)  {
    for i := 0; i < 100; i++ {
        ch <- i
    }
    close(ch)
}

func main()  {
    start := time.Now()
    ch := make(chan int, 100)
	// 有缓冲通道: 程序执行耗时(s): 0.000247284
	// 无缓冲通道: 程序执行耗时(s): 0.000391499

    go test(ch)
    for i := range ch {
        fmt.Println("接收到的数据:", i)
    }
    end := time.Now()
    consume := end.Sub(start).Seconds()
    fmt.Println("程序执行耗时(s):", consume)
}

无缓channel和有缓冲channel使用场景

首先这里没必要想,我什么情况下必须用什么channel,需要看场景:用于同步的时候,一般用无缓冲 , 一般大量交换数据、或者并发处理(生产-消费)情况下用缓存的多

无缓冲的例子:

** 一个函数开启一个goroutine处理一个事情,然后函数继续执行,但执行到某一步后,需要等前面的那个goroutine的结果**

package main

import (
	"fmt"
)

func main() {

	// 现在有一个任务
	// 它需要并行准备两份数据
	Worker()

}

// 斐波那契数列计算
func Fib(n int) int {
	if n == 1 || n == 2 {
		return 1
	}
	return (Fib(n-2) + Fib(n-1))
}



// 比如我有一个函数 Worker
// 这个函数的功能是
// 1 并发一个goroutine处理一个事情,不阻塞后面的逻辑
// 2 但是执行到某一步需要等待上面的goroutine结束

func Worker(){
	done := make(chan int)

	// 1 并发一个goroutine处理一个事情(我去拿一份数据,但是这份数据不立刻就用但是比较耗时间)
	// 这个时候就可以启动一个goroutine先去取数据
	go func(){
		done <- Fib(10)
		// 用完的关闭否则只有接收方了就会出现死锁的问题
		close(done)
	}()

	// 调用一个标准的业务逻辑
	n2 := func()int{return 1 + 1}()
	fmt.Printf("n2的结果是:%v, 这里很快就执行完了\n",n2)

	// 2 但是执行到某一步需要等待上面的goroutine结束
	// 等待异步任务结束然后在计算结果
	fmt.Printf("这里需要等待异步的流程执行完\n")
	n1 := <-done
	fmt.Printf("n1的结果是:%v\n",n2)

	sum := n1 + n2

	fmt.Printf("n1 和 n2的结果是:%v",sum)
}

有缓冲例子

无缓冲需要同步,有缓冲就是需要异步操作,它的场景是并发,且可以限制并发数量

有缓冲通道不需要收发同时都准备好

正常情况下我们是可以无限开启gorotuine的但是有时候我们既要并发也要限制这个时候缓冲通道就比较合适了~

package main

import (
	"fmt"
	"time"
)

func main() {
	// 假如我有1000个任务
	// 没开启一个goroutine就往channel里写入一个元素,直到写满了就阻塞了
	// 当任务执行完从channel里哪出一个元素,for循环继续

    control := make(chan interface{},2)

    for i := 1; i <= 10; i++ {
		// 这里应该放上面,如果放下面就会每次都执行三个了
		// 往有缓冲channel里写入一个元素
        control <- i
        go func(num int) {
			// 伪代码逻辑
			fmt.Printf("当前时间是:%v\n",time.Now().Format("2006-01-02 15:04:05"))
            time.Sleep(time.Second * 2)
			// 从channel里接收数据
            <-control
        }(i)
    }
}

单向通道

有时候我们会将通道作为参数传递,通道是双向的:可以读也可以写,如果不做限制很容易用乱产生死锁,所以要限制它:只读或者只写,所以go提供了单向通道来解决这个case

goroutine 池例子

package main

import (
	"fmt"
	"time"
)

func main() {
	jobs := make(chan int, 100)
	rets := make(chan int, 100)

	// 启动2个goroutine
	for i := 1; i < 3; i++ {
		go worker(i, jobs, rets)
	}

	// 总共10个任务
	for j := 1; j < 11; j++ {
		jobs <- j
	}

	// 关闭jobs的channel
	// 让goroutine的for循环正常退出
	close(jobs)

	// 循环读取结果
	for r := 1; r < 11; r++ {
		fmt.Println("结果是:",<-rets)
	}
}

// chanJobs 只读
// chanRets 只写
func worker(i int, chanJobs <-chan int, chanRets chan<- int) {
	for job := range chanJobs {
		fmt.Printf("开始:当前goroutine的ID是:%v, 当前job的ID是:%v\n",i,job)
		time.Sleep(time.Second)
		fmt.Printf("开始:当前goroutine的ID是:%v, 当前job的ID是:%v\n",i,job)
		chanRets <- job * 2
	}
}

channel总结

正常的channel

接收:
	空的:
		阻塞
	没空:
		接收
	通道已关闭:
		1 如果通道里还有元素会继续读取
		2 如果通道里已经没有元素了,也会取出值只是他是这个通道元素的零值

发送:
	满了:
		阻塞
	没满:
		发送
	通道已关闭:
		panic异常
关闭:
	通道未关闭:
		正常关闭
	通道已关闭:
		panic异常

对nil类型的channel操作

只申明了但是没有初始化的channel

发送:
      阻塞
接收:
      阻塞
关闭:
      panic
package main

import (
	"fmt"
	"time"
)

func main() {
	// 因为这个ch变量只是声明了但是并没有初始化
	var ch chan int

	go send(ch)
	<-ch
	time.Sleep(time.Second * 1)
}

func send(ch chan int) {
	fmt.Println("Sending value to channnel start")
	ch <- 1
	fmt.Println("Sending value to channnel finish")
}

select多路复用

在某些场景下有时候回需要同时从多个通道接收数据。通道在接收数据时,如果没有数据可以接收将会发生阻塞。为了解决这个问题go为我们提供了select关键字

golang中的select语句格式如下

	select {
	case v1 := <-ch1:
		// 如果从 ch1 信道成功接收数据,则执行该分支代码
		fmt.Println("接收成功", v1)
	case ch2 <- 1:
		// 如果成功向 ch2 信道成功发送数据,则执行该分支代码
		fmt.Println("发送成功")
	default:
		// 如果上面都没有成功,则进入 default 分支处理流程
		fmt.Println("走的default分支")
	}

select的语法结构有点类似于switch,但select里的case后面并不带判断条件,而是一个信道的操作,所有case中的表达式都必须是channel的发送或接收操作

另外:golang 的 select 就是监听 IO 操作,当 IO 操作发生时,触发相应的动作每个case语句里必须是一个IO操作,确切的说,应该是一个面向channel的IO操作。

Go 语言的 select 语句借鉴自 Unix 的 select() 函数,在 Unix 中,可以通过调用 select() 函数来监控一系列的文件句柄,一旦其中一个文件句柄发生了 IO 动作,该 select() 调用就会被返回(C 语言中就是这么做的),后来该机制也被用于实现高并发的 Socket 服务器程序。Go 语言直接在语言级别支持 select关键字,用于处理并发编程中通道之间异步 IO 通信问题。

posted @ 2021-10-20 15:15  天帅  阅读(313)  评论(0编辑  收藏  举报