概述

并发编程是利用多核心能力,提升程序性能,而多线程之间需要相互协作、共享资源、线程安全等。任何并发模型都要解决线程间通讯问题,毫不夸张的说线程通讯是并发编程的主要问题。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. django.core.exceptions.ImproperlyConfigured: Specifying a namespace in include() without providing an app_name is not supported.

    django.core.exceptions.ImproperlyConfigured: Specifying a namespace in include() without providing a ...

  2. 一个简单利用WebGL绘制频谱瀑布图示例

    先看效果 还是比较节省性能的,这个还是包含了生成测试数据的性能,实际应用如果是直接通信获得数据应该还能少几毫秒吧! 准备工作 用了React,但是关系不大 WebGL的基础用法(推荐看一看掘金里的一个 ...

  3. KVM下windows由IDE模式改为virtio模式蓝屏 开不开机

    KVM安装Windows默认使用的是qemu虚拟化IDE硬盘模式,在这种情况下,IO性能比较低,如果使用virtio的方式可以提高虚拟机IO性能. 于是我想将这台虚拟机迁移到openstack中管理 ...

  4. CodeForces 1367E Necklace Assembly

    题意 给定一个字符串\(s\),长度为\(n\),一根项链为一个环,定义一根项链为\(k-beautiful\),则该项链顺时针转\(k\)下后与原项链相等,给出\(k\),请构造一根最长的\(k-b ...

  5. SQL Server实例间同步登录用户

    SQL Server实例间同步登录用户 问题痛点:由于AlwaysOn和数据库镜像无法同步数据库外实例对象,例如 登录用户.作业.链接服务器等,导致主库切换之后,应用连接不上数据库或者作业不存在导致每 ...

  6. 杰哥教你面试之一百问系列:java集合

    目录 1. 什么是Java集合?请简要介绍一下集合框架. 2. Java集合框架主要分为哪几种类型? 3. 什么是迭代器(Iterator)?它的作用是什么? 4. ArrayList和LinkedL ...

  7. Seata AT和XA模式

    一.分布式事务产生得原因: 1.1.数据库分库分表 当数据库单表一年产生的数据超过1000W,那么就要考虑分库分表,具体分库分表的原理在此不做解释,以后有空详细说,简单的说就是原来的一个数据库变成了多 ...

  8. 西门子Teamcenter 许可分析

    西门子Teamcenter 许可 绑定了主机名称,mac地址 另外,Teamcenter可以支持多个许可服务 所以.......................找个正式许可复制就可以 end succ ...

  9. Biwen.QuickApi代码生成器功能上线

    [QuickApi("hello/world")] public class MyApi : BaseQuickApi<Req,Rsp>{} 使用方式 : dotnet ...

  10. Python开发之Django框架

    一. Django框架 01.网络软件开发架构演变过程 02.HTTP协议讲解 03.web应用与框架介绍及手撸web框架 04.Django入门项目创建与必会三板斧 05.Django静态文件配置与 ...