Go并发编程-Channel

一、前言

传统的线程模型,比如经常使用Java、C++、Python编程的时候,需要多个线程之间通过共享内存(比如在堆上创建的共享变量)来通信。这时候为保证线程安全,多线程共享的数据结构需要使用锁来保护,多线程访问共享数据结构时候需要竞争获取锁,只有获取到锁的线程才可以存取共享数据。这种情况下使开发人员更加容易的使用线程安全的数据结构,比如Java中JUC包中的并发安全的队列、列表等。

前面章节我们讲解了Go中提供的低级同步原语-锁,其实Go的并发原语 - goroutines和channels 提供了一种优雅而独特的结构化开发并发软件的方式。Go鼓励使用通道在goroutine之间传递对共享数据的引用,而不是明确地使用锁来保护对共享数据的访问。这种方法确保在给定时间只有一个goroutine可以访问共享数据。这个理念被总结为:不要通过共享内存来通信,而要通过通信来共享内存。

本章我们就来讲解有关Channel的知识,我们可以把通道理解为一个并发安全的队列,生产者goroutine可以向通道里面放入元素,消费者goroutine可以从通道里面获取元素。

从队列大小来看通道可以分为有缓冲通道和无缓冲通道,无缓冲通道里面最多有一个元素,有缓冲通道里面可以有很多元素。

另外通道还是有方向的,如果一个通道只允许向通道里面放元素,但是不允许从通道里面取元素,则我们称之为单向的发送通道(向通道里面写元素),例如var ch chan <-int声明了一个发送通道;如果一个通道只允许从通道里面获取元素,而不允许向其中写入元素,则称之为接受通道(从通道里面读取元素),例如var ch <-chan int声明了一个接受通道;如果一个通道既可以从中读取元素,又可以向其中写入元素,则称之为双向通道,例如var ch chan int声明了一个双向通道。

另外通道是可以被关闭的,当调用close(ch)关闭了通道ch后,不能再向通道ch写入元素,但是可以从通道读取元素。

二、无缓冲通道

在go中创建一个无缓冲的通道使用下面两种方式:

  var c chan int
   c = make(chan int)

或者

   c := make(chan int)

如上创建了一个int类型的无缓冲通道c,其中第一种方式是先声明,然后在初始化;第二种是简短式声明和初始化一步完成,也就推荐的方式;

向通道c内写入获取读取元素可以使用<-符号,比如向通道c写入元素12,c<-12;从通道c中读取元素可以使用<-c,比如从通道读取一个元素到变量w,w := <-c;当没有向通道内写入元素时候,试图从通道内读取元素的goroutine会被阻塞;对应无缓冲通道,当试图向没有goroutine正在从通道读取元素的通道写入元素时候,写入的goroutine会被阻塞。

需要注意的是这里的make和chan都是内置的语言层面的关键字,当我们创建具体类型的通道时候只需要替换int就可以了。

下面我们看个例子:

package main
import (
    "fmt"
    "sort"
)
var c chan int
var nums []int
func main() {
    //1.初始化通道
    c = make(chan int)
    nums = []int{5, 3, 7, 2, 9, 1, 6}
    //2.开启goroutine排序
    go func() {
        //2.1
        sort.Ints(nums)
        //2.2
        c <- 1
    }()
    //3.阻塞,直到通道内有元素
    <-c
    fmt.Println(nums)
}
  • 如上首先创建了一个无缓冲通道c,和一个切片nums

  • 代码2开启了一个goroutine1

  • 代码3企图从通道内读取一个元素,当通道内没有元素时候,代码3所在的 goroutine就会被阻塞,goroutine1执行完毕2.1对元素排序后,会执行代码2.2 向通道写入一元素,这时候代码3就会返回,然后打印排序后的切片内容

  • 这里我们使用无缓冲通道实现了之前使用WaitGroup来实现主goroutine等待子goroutine执行完毕的方式。

三、有缓冲通道

在go中创建一个有缓冲的通道使用下面两种方式:

var c chan int
c = make(chan int, 1)

或者

   c := make(chan int, 1)

如上创建了一个int类型的缓冲队列为1通道c,其中第一种方式是先声明,然后在初始化;第二种是简短式声明和初始化一步完成,也就推荐的方式;

有缓冲通道当缓冲有空间时候,向里面放入元素会马上返回,当缓冲满了的时候在放入元素调用goroutine会被阻塞;当通道内没有元素时候尝试从通道获取元素会被阻塞。

下面看一个使用有缓冲通道实现生产消费模型:

package main
import (
    "fmt"
)
func printer(ch <-chan int, wg chan<- int) {
    //3.1
    for i := range ch {
        fmt.Println(i)
    }
    //3.2
    wg <- 1
    close(wg)
}
func main() {
    //1.创建缓冲通道
    ch := make(chan int, 10)
    //2.创建同步用的无缓冲通道
    wg := make(chan int)
    //3.开启go协程
    go printer(ch, wg)
    //4.写入到通道
    for i := 1; i < 100; i++ {
        ch <- i
    }
    //5.关闭协程
    close(ch)
    fmt.Println("wait sub goroutine over")
    //6.等待子goroutine结束
    <-wg
    fmt.Println("main goroutine over")
}
  • 如上代码1创建另一个含有10个int类型的元素的有缓冲通道ch,代码2创建了一个无缓冲通道wg用来做线程间同步

  • 代码3开启新goroutine执行函数printer,其内部从通道ch读取元素,一开始ch内没有元素,则当前goroutine会被阻塞

  • 代码4则向通道ch写入100个元素,当ch里面有元素时候,新goroutine就会被激活,然后从通道里面跌打出元素进行打印

  • 代码5则当向ch写入100个元素后关闭通道,关闭后不能再向通道写入元素,但是通道内的元素还是会被读取的,代码6则试图从通道wg读取元素,一开始通道内无元素 所以main groutine阻塞到这里。

  • 等新goroutine代码3.1把通道ch里面元素全部迭代完毕后,执行代码3.2向通道wg写入一个元素然后关闭通道,这时候main goroutine则会从代码6中返回。

Go中以消息进行通信的方式允许程序员安全地协调多个并发任务,并且容易理解语义和控制流,这通常比其他语言比如Java中的回调函数(callbacks)或共享内存方式更优雅简单。

假期在家无聊?那就免费学习下Go语言吧!!!

Go并发编程-并发与并行

Go并发编程-并发编程难在哪里

Go并发编程-线程模型

Go并发编程-内存模型

Go并发编程-goroutine轻量级的线程

Go并发编程-runtime包

Go并发编程-互斥锁

Go并发编程-读写锁

Go并发编程-条件变量

Go并发编程-WaitGroup

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值