概述

并发编程是利用多核心能力,提升程序性能,而多线程之间需要相互协作、共享资源、线程安全等。任何并发模型都要解决线程间通讯问题,毫不夸张的说线程通讯是并发编程的主要问题。go使用著名的CSP(Communicating Sequential Process,通讯顺序进程)并发模型,从设计之初 Go 语言就注重如何在编程语言层级上设计一个简洁安全高效的抽象模型,让程序员专注于分解问题和组合方案,而且不用被线程管理和信号互斥这些繁琐的操作分散精力。channel是线程简通讯的具体实现之一,本质就是一个线程安全的 FIFO 阻塞队列(先进先出),向队列中写入数据,在另一个线程从队列读取数据。很多语言都有类似实现,比如 Java 的线程池任务队列。

基本使用

通道是引用类型,需要使用 make 创建,格式如下

通道实例 := make(chan 数据类型, 通道长度)
  • 数据类型:通道内传输的元素类型,可以基本数据类型,也可以使自定义数据类型。
  • 通道实例:通过make创建的通道句柄,与函数名称一样,指向通道的内存首地址。
  • 通道长度:通道本质是队列,创建时候可指定长度,默认为0

创建通道

ch1 := make(chan int)                 // 创建一个整型类型的通道
ch2 := make(chan interface{}) // 创建一个空接口类型的通道, 可以存放任意格式
ch3 := make(chan *Equip) // 创建Equip指针类型的通道, 可以存放*Equip
ch4 := make(chan *Equip, 10) // 创建Equip指针类型的通道, 并指定队列长度

通道本质就是线程安全的队列,创建时候可以指定队列长度,默认为0。

向通道写入数据,使用语法非常形象,写入channel <-,读取<-channel

ch2 := make(chan interface{}, 10)
ch2<- 10 // 向队列写入
n := <-ch2 // 从队列读取
fmt.Println(n) // 10

箭头语法虽然很形象,但是有些奇怪,也不利于扩展。使用函数方式感觉更好,也更主流,如func (p *chan) get() any func (p *chan) put(any) err,扩展性也更强,通过参数可增加超时、同步、异步等技能。

箭头符号并没有规定位置,与C指针一样,如下两个语句等效

ch1 := make(chan int)
i := <-ch1
i := <- ch1

箭头语法的读写有相对性,可读性一般,有时候无法分辨是读或写,看起来很奇怪,如下伪代码

func main() {
input := make(chan int, 2)
output := make(chan int, 2) go func() {
input <- 10
}()
output<- <-input
fmt.Println(<-output)
}

管道是用于协程之间通讯,主流使用方式如下

ch2 := make(chan interface{}, 10)

go func() {
data := <-ch2 // 用户协程读取
fmt.Println(data)
}() ch2 <- "hello" // 主协程写入
time.Sleep(time.Second)

管道也支持遍历,与箭头符号一样,无数据时候循环将被阻塞,循环永远不会结束,除非关闭管道

chanInt := make(chan int, 10)

for chanInt, ok := range chanInts {
fmt.Println(chanInt)
}

管道也支持关闭,关闭后的管道不允许写入,panic 异常

chanInts := make(chan int, 10)
close(chanInts)
chanInts <- 1 // panic: send on closed channel

读取则不同,已有数据可继续读取,无数据时返回false,不阻塞

if value, ok := <-chanInts; ok {			// 从管道读取数据不在阻塞
fmt.Println("从管读取=", value)
} else {
fmt.Println("从管道读取失败", ok)
return
}

单向管道

管道也支持单向模式,仅允许读取、或者写入

var queue <-chan string = make(chan string)

函数形参也可以定义定向管道

func customer(channel <-chan string) {		// 形参为只读管道
for {
message := <-channel // 只允许读取数据
fmt.Println(message)
}
}
channel := make(chan string)
go customer(channel)

管道阻塞

Go管道的读写都是同步模式,当管道容量还有空间,则写入成功,否则将阻塞直到写入成功。从管道读取也一样,有数据直接读取,否则将阻塞直到读取成功。

var done = make(chan bool)

func aGoroutine() {
fmt.Println("hello")
done <- true // 写管道
} func main() {
go aGoroutine()
<-done // 读阻塞
}

主协程从管道读取数据时将被阻塞,直到用户协程写入数据。管道非常适合用于生产者消费者模式,需要平滑两者的性能差异,可通过管道容量实现缓冲,所以除非特定场景,都建议管道容量大于零。

有些场景可以使用管道控制线程并发数

// 待补充

阻塞特性也带来了些问题,程序无法控制超时(箭头函数语法的后遗症),go 也提供了解决方案, 使用select关键,与网络编程的select函数类似,监测多个通道是否可读状态,都可读随机选择一个,都不可读进入Default分支,否则阻塞

select {
case n := <-input:
fmt.Println(n)
case m := <-output:
fmt.Println(m)
default:
fmt.Println("default")
}

当然也可以使用select向管道写入数据,只要不关闭管道总是可写入,此时加入default分支永远不会被执行到,如下随机石头剪刀布

ch := make(chan string)
go func() {
for {
select {
case ch <- "石头":
case ch <- "剪刀":
case ch <- "布":
}
}
}() for value := range ch {
log.Println(value)
time.Sleep(time.Second)
}

模拟线程池

由于go的管道非常轻量且简洁,大部分直接使用,封装线程池模式并不常见。案例仅作为功能演示,非常简单几十行代码即可实现线程池的基本功能,体现了go并发模型的简洁、高效。

type Runnable interface {
Start()
} // 线程池对象
type ThreadPool struct {
queueSize int
workSize int
channel chan Runnable
wait sync.WaitGroup
} // 工作线程, 执行异步任务
func (pool *ThreadPool) doWorker(name string) {
log.Printf("%s 启动工作协程", name)
for true {
if runnable, ok := <-pool.channel; ok {
log.Printf("%s 获取任务, %v\n", name, runnable)
runnable.Start()
log.Printf("%s 任务执行成功, %v\n", name, runnable)
} else {
log.Printf("%s 线程池关闭, 退出工作协程\n", name)
pool.wait.Done()
return
}
}
} // 启动工作线程
func (pool *ThreadPool) worker() {
pool.wait.Add(pool.workSize)
for i := 0; i < pool.workSize; i++ {
go pool.doWorker(fmt.Sprintf("work-%d", i))
}
} // Submit 提交任务
func (pool *ThreadPool) Submit(task Runnable) bool {
defer func() { recover() }()
pool.channel <- task
return true
} // Close 关闭线程池
func (pool *ThreadPool) Close() {
defer func() { recover() }()
close(pool.channel)
} // Wait 等待线程池任务完成
func (pool *ThreadPool) Wait() {
pool.Close()
pool.wait.Wait()
} // NewThreadPool 工厂函数,创建线程池
func NewThreadPool(queueSize int, workSize int) *ThreadPool {
pool := &ThreadPool{queueSize: queueSize, workSize: workSize, channel: make(chan Runnable, queueSize)}
pool.worker()
return pool
}

使用线程池

type person struct {
name string
} func (p *person) Start() {
fmt.Println(p.name)
} func main() {
threadPool := executor.NewThreadPool(10, 2) // 创建线程池, 队列长度10, 工作线程2 for i := 0; i < 5; i++ {
threadPool.Submit(&person{name: "xx"}) // 提交十个任务
} threadPool.Wait() // 阻塞等待所有任务完成
}

模拟管道

任何线程之间的通讯都依赖底层锁机制,channel是对锁机制封装后的实现对象,与Java中线程池任务队列机制几乎一样,但要简洁很多。使用切片简单模拟

接口声明

type Queue interface {
// Put 向队列添加任务, 添加成功返回true, 添加失败返回false, 队列满了则阻塞直到添加成功
Put(task interface{}) bool // Get 从队列获取任务, 一直阻塞直到获取任务, 队列关闭且没有任务则返回false
Get() (interface{}, bool) // Size 查看队列中的任务数
Size() int // Close 关闭队列, 关闭后将无法添加任务, 已有的任务可以继续获取
Close()
}

基于切片实现

// SliceQueue 使用切片实现, 自动扩容属性队列永远都不会满, 扩容时候会触发数据复制, 性能一般
type SliceQueue struct {
sync.Mutex
cond *sync.Cond
queue []interface{}
close atomic.Bool
} func (q *SliceQueue) Get() (data interface{}, ok bool) {
q.Lock()
defer q.Unlock() for true {
if len(q.queue) == 0 {
if q.close.Load() == true {
return nil, false
}
q.cond.Wait()
}
if data := q.doGet(); data != nil {
return data, true
}
}
return
} func (q *SliceQueue) doGet() interface{} {
if len(q.queue) >= 1 {
data := q.queue[0]
q.queue = q.queue[1:]
return data
}
return nil
} func (q *SliceQueue) Put(task interface{}) bool {
q.Lock()
defer func() {
q.cond.Signal()
q.Unlock()
}() if q.close.Load() == true {
return false
}
q.queue = append(q.queue, task)
return true
} func (q *SliceQueue) Size() int {
return len(q.queue)
} func (q *SliceQueue) Close() {
if q.close.Load() == true {
return
} q.Lock()
defer q.Unlock() q.close.Store(true)
q.cond.Broadcast()
} func NewSliceQueue() Queue {
sliceQueue := &SliceQueue{queue: make([]interface{}, 0, 2)}
sliceQueue.cond = sync.NewCond(sliceQueue)
return sliceQueue
}

基于环行数组实现

type ArrayQueue struct {
sync.Mutex
readCond *sync.Cond
writeCond *sync.Cond
readIndex int
writeIndex int
queueMaxSize int
close atomic.Bool
queue []interface{}
} func (q *ArrayQueue) Put(task interface{}) bool {
q.Lock()
defer q.Unlock() for true {
if q.close.Load() == true {
return false
}
if q.IsFull() {
q.writeCond.Wait()
if q.IsFull() {
continue
}
}
q.queue[q.writeIndex] = task
q.writeIndex = (q.writeIndex + 1) % q.queueMaxSize
q.readCond.Signal()
return true
}
return true
} func (q *ArrayQueue) Get() (interface{}, bool) {
q.Lock()
defer q.Unlock() for true {
if q.IsEmpty() {
if q.close.Load() == true {
return nil, false
}
q.readCond.Wait()
if q.IsEmpty() {
continue
}
}
task := q.queue[q.readIndex]
q.readIndex = (q.readIndex + 1) % q.queueMaxSize
q.writeCond.Signal()
return task, true
}
return nil, true
} func (q *ArrayQueue) Size() int {
return q.queueMaxSize
} func (q *ArrayQueue) Close() {
if q.close.Load() == true {
return
}
q.Lock()
q.Unlock()
q.close.Store(true)
q.readCond.Broadcast()
} func (q *ArrayQueue) IsFull() bool {
return (q.writeIndex+1)%q.queueMaxSize == q.readIndex
} func (q *ArrayQueue) IsEmpty() bool {
return q.readIndex == q.writeIndex
} func NewArrayQueue(size int) Queue {
queue := &ArrayQueue{queue: make([]interface{}, size), readIndex: 0, writeIndex: 0, queueMaxSize: size}
queue.readCond = sync.NewCond(queue)
queue.writeCond = sync.NewCond(queue)
return queue
}

测试用例

func TestWith(t *testing.T) {
q := NewSliceQueue()
go func() {
time.Sleep(time.Second * 2)
q.Put(true) // 向队列写入数据, 与 chan<- 功能相同
}() q.Get() // 阻塞直到读取数据, 与 <-chan 功能相同
}

go并发 - channel的更多相关文章

  1. Netty实战六之ChannelHandler和ChannelPipeline

    1.Channel的生命周期 Interface Channel定义了一组和ChannelInboundHandler API密切相关的简单但功能强大的状态模型,以下列出Channel的4个状态. C ...

  2. Netty实战

    一.Netty异步和事件驱动1.Java网络编程回顾socket.accept 阻塞socket.setsockopt /非阻塞2.NIO异步非阻塞a).nio 非阻塞的关键时使用选择器(java.n ...

  3. [go]包管理

    vendor方式 //包管理发展 go get(无版本概念) -> vendor(godep)(无版本概念) -> go modules go get github.com/tools/g ...

  4. go/wiki/MutexOrChannel Golang并发:选channel还是选锁?

    https://mp.weixin.qq.com/s/JcED2qgJEj8LaBckVZBhDA https://github.com/golang/go/wiki/MutexOrChannel M ...

  5. Golang的channel使用以及并发同步技巧

    在学习<The Go Programming Language>第八章并发单元的时候还是遭遇了不少问题,和值得总结思考和记录的地方. 做一个类似于unix du命令的工具.但是阉割了一些功 ...

  6. golang语言并发与并行——goroutine和channel的详细理解(一)

    如果不是我对真正并行的线程的追求,就不会认识到Go有多么的迷人. Go语言从语言层面上就支持了并发,这与其他语言大不一样,不像以前我们要用Thread库 来新建线程,还要用线程安全的队列库来共享数据. ...

  7. golang语言并发与并行——goroutine和channel的详细理解

    如果不是我对真正并行的线程的追求,就不会认识到Go有多么的迷人. Go语言从语言层面上就支持了并发,这与其他语言大不一样,不像以前我们要用Thread库 来新建线程,还要用线程安全的队列库来共享数据. ...

  8. Golang并发编程进程通信channel了解及简单使用

    概念及作用 channel是一个数据类型,用于实现同步,用于两个协程之间交换数据.goroutine奉行通过通信来共享内存,而不是共享内存来通信.引用类型channel是CSP模式的具体实现,用于多个 ...

  9. golang语言并发与并行——goroutine和channel的详细理解(一) 转发自https://blog.csdn.net/skh2015java/article/details/60330785

    如果不是我对真正并行的线程的追求,就不会认识到Go有多么的迷人. Go语言从语言层面上就支持了并发,这与其他语言大不一样,不像以前我们要用Thread库 来新建线程,还要用线程安全的队列库来共享数据. ...

  10. golang中并发sync和channel

    golang中实现并发非常简单,只需在需要并发的函数前面添加关键字"go",但是如何处理go并发机制中不同goroutine之间的同步与通信,golang 中提供了sync包和channel ...

随机推荐

  1. Node: 使用nvm切换node版本

    软件下载 https://github.com/coreybutler/nvm-windows/releases/tag/1.1.7 解压并安装 双击程序一路安装即可.安装完成后,在控制台输入nvm出 ...

  2. go 判断文件是否存在,并创建

    1 package main 2 3 import ( 4 "fmt" 5 "os" 6 ) 7 8 //判断文件夹是否存在 9 func PathExists ...

  3. Java中的线程池使用及原理

    开篇-为什么要使用线程池? ​ Java 中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池.在开发过程中,合理地使用线程池能够带来 3 个好处. ​ 第一:降低 ...

  4. ASP.NET WebForm中在TextBox输入框回车时会触发其他事件,如何处理?

    一.TextBox在输入框回车时会触发其他事件,如何解决? 前台代码: <ul> <li><span>名称:</span><asp:TextBox ...

  5. 如何正确使用 ThreadLocal,你真的用对了吗?

    引言: 当多线程访问共享且可变的数据时,涉及到线程间同步的问题,并不是所有时候,都要用到共享数据,所以就需要ThreadLocal出场了. ThreadLocal又称线程本地变量,使用其能够将数据封闭 ...

  6. webpack dev server 与 hot module replace 提高开发效率

    通过 webpack 命令编译源代码时,如果我们对源代码进行了修改,需要重新执行命令才能看到编译后的效果. 这样在开发中非常的影响效率,如果存在一种方式,当文件被修改时,webpack 自动监听重新编 ...

  7. 苹果MacOS系统傻瓜式本地部署AI绘画Stable Diffusion教程

    Stable Diffusion的部署对小白来说非常麻烦,特别是又不懂技术的人.今天分享两个一键傻瓜式安装包,对小白来说非常有用.下面两个任选一个安装就可以. 一.DiffusionBee 简单介绍 ...

  8. Vue【原创】时间轴 【time-axis】&【date-axis】

    封装了关于时间轴的组件,有时候统计页面会用到. 效果图: 时间轴分为2种,一种是time-axis:范围选择模式,一种是date-axis:步长选择模式. 代码中涉及到的工具类和图片资源,请移步页面底 ...

  9. Go语言-Slice详解

    Go语言中的slice表示一个具有相同类型元素的可变长序列,语言本身提供了两个操作方法: 创建:make([]T,len,cap) 追加: append(slice, T ...) 同时slice支持 ...

  10. 4.go语言复合类型简述

    目录 1. 本章前瞻 2.来自leetcode的例题 描述 分析 题解 3. 复合类型新版本的变化 3.1 string和[]byte的高效转化 3.2 内置函数clear 4. 复合类型概述 4.1 ...