概述

并发编程是利用多核心能力,提升程序性能,而多线程之间需要相互协作、共享资源、线程安全等。任何并发模型都要解决线程间通讯问题,毫不夸张的说线程通讯是并发编程的主要问题。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. vue2 如何实现将dom元素转移到指定节点

    背景:在写商城页面时,PC端给的设计图纸是按照宽度1920给的,内部内容区域(main)1191px,写死的指定宽度.然后新出了一个页面,类似于12306的这个页面,图片部分,直接占满了屏幕的100v ...

  2. Inno SetUp安装包:如何在程序安装时卸载驱动程序

    pnputil命令行方式卸载 如果您想通过命令行卸载.INF文件的驱动程序,您需要使用PnPUtil命令.以下是一个示例: pnputil /delete-driver oem0.inf /unins ...

  3. 只要学会这些AI工具,一个人就是一家营销咨询公司

    随着AI工具的不断涌现,您只需掌握市面上热门的AI工具,便可独自开展营销咨询公司.通过一系列AI工具,您可以为企业提供全案服务,收获丰厚回报. 例如,在协助一家美妆初创公司出海时,我们运用一系列AI工 ...

  4. VulnStack - ATT&CK红队评估实战(一) Writeup

    VulnStack - ATT&CK红队评估实战(一) Writeup VulnStack(一)环境搭建 项目地址 http://vulnstack.qiyuanxuetang.net/vul ...

  5. Redis系列22:Redis 的Pub/Sub能力

    Redis系列1:深刻理解高性能Redis的本质 Redis系列2:数据持久化提高可用性 Redis系列3:高可用之主从架构 Redis系列4:高可用之Sentinel(哨兵模式) Redis系列5: ...

  6. OpenCASCADE 显示对象设置不可选中

    原有的选中模式代码: static Standard_Integer SelectionMode (const TopAbs_ShapeEnum theShapeType) { switch (the ...

  7. DevSecOps之应用安全测试工具及选型

    上篇文章,有同学私信想了解有哪些DevSecOps工具,这里整理出来,供大家参考(PS: 非专业安全人士,仅从DevOps建设角度,给出自己见解) 软件中的漏洞和弱点很常见:84%的软件漏洞都是利用应 ...

  8. Java爬虫实战系列2——动手写爬虫初体验

    在上面的章节中,我们介绍了几个目前比较活跃的Java爬虫框架.在今天的章节中,我们会参考开源爬虫框架,开发我们自己的Java爬虫软件. 首先,我们下载本章节要使用到的源代码,本章节主要提供了基于HTT ...

  9. SQL语句简单入门

    SQL语句速查 创建部门表 deptno dname location 1 技术部 23楼 create table dept --dept部门 ( deptno int primary key, - ...

  10. Avalonia开发(一)环境搭建

    一.介绍 开源 GitHub:https://github.com/AvaloniaUI/Avalonia/ 多平台支持,包括Windows.mac OS.Linux.iOS.Android.Sams ...