12.1 通道-使用通道、带缓冲的通道

2024-05-28 12:28
文章标签 使用 缓冲 通道 12.1

本文主要是介绍12.1 通道-使用通道、带缓冲的通道,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

使用通道

如果说Goroutine是一种支持并发的编程方式,那么通道就是一种在Goroutine之间建立通信的机制。(详细内容参见《Goroutine与MPG多线程文档》)

  • 传统意义上讲,线程之间的数据是天然共享的,并不需要为通信建立专门的机制。
  • 线程之间真正需要考虑的是如何避免针对共享数据的访问冲突,例如通过加锁,在一个线程读取共享数据时禁止其它线程修改该数据,或者相反。
  • 然而通过锁来维护数据完整性的做法并非那么简单。如果不能确知程序中哪些部分需要读取数据,哪些部分又需要修改数据,以及它们的先后顺序,那么将很难预测可能发生的情况。即便是久经沙场的编程老手,也很难避免线程间因竞争共享资源而导致的BUG。
  • 鉴于历史的教训,Go语言主张线程之间与其通过共享数据交换信息,不如建立通信机制。

除去在线程间交换数据以外,通道还有另外一个作用,即在宏观的异步过程中,借助某种形式的"停——等"逻辑,以满足微观的同步需求。

  • 某个线程执行到某个特定位置,可能需要""下来,""另一个线程完成特定的操作后再继续执行,例如消费者线程必须等待生产者线程为其创造可供消费的资源才能继续消费。
  • 传统意义上的线程同步,往往需要借助于互斥锁信号量条件变量等内核机制来实现。
  • Go语言利用通道的阻塞I/O,在语言层面,以极简单的方式为异步线程提供了同步支持。

创建通道可使用内置函数make,收发消息可使用<-符号

  • c := make(chan string) // 创建一个用于传输字符串数据的通道
  • c <- "Task A of step 1 finished" // 向通道发送(写入)消息(数据)
  • message := <-c // 从通道接收(读取)消息(数据)
// 并发中的异步问题
// 完全并发的"线程"以异步模式运行,其先后顺序完全不受控制
//
// step1和step2是一个任务的两步,需顺序执行,且step1是step2的前置条件;
// Task A 和Task B是step1的两个可并发执行的子任务。
//       Task A of step 1
// +--------------------------+
// |                          |
// +--------+--------+--------+--------+
// 0        1        2        3        4
//
//  Task B of step 1
// +-----------------+
// |                 |
// +--------+--------+--------+--------+
// 0        1        2        3        4
//
//                         Step 2
//                   +-----------------+
//                   |                 |
// +--------+--------+--------+--------+
// 0        1        2        3        4
package mainimport ("fmt""time"
)func step1a() {fmt.Println("Task A of step 1 start...")time.Sleep(time.Second * 3)fmt.Println("Task A of step 1 finished")
}func step1b() {fmt.Println("Task B of step 1 start...")time.Sleep(time.Second * 2)fmt.Println("Task B of step 1 finished")
}func step2() {fmt.Println("Step 2 start...")time.Sleep(time.Second * 2)fmt.Println("Step 2 finished")
}func main() {go step1a()step1b()step2()
}
// 打印输出:
// Task B of step 1 start...
// Task A of step 1 start...
// Task B of step 1 finished
// Step 2 start...
// Task A of step 1 finished
// Step 2 finished 
// 从上述输出可知,程序执行流程存在问题,step2在setep1完成之前就执行了。
// 并发中的同步协调
// 通过收发消息,以推送方式协调并发事件,使执行过程更加有序
//
//       Task A of step 1
// +--------------------------+
// |                          |
// +--------+--------+--------+--------+--------+
// 0        1        2        3        4        5
//
//  Task B of step 1
// +-----------------+
// |                 |
// +--------+--------+--------+--------+--------+
// 0        1        2        3        4        5
//
//                                  Step 2
//                            +-----------------+
//                            |                 |
// +--------+--------+--------+--------+--------+
// 0        1        2        3        4        5 // 通道让数据能够进入或离开"线程",以在"线程"之间建立通信
// 
// +---------------+           +----------------+ 
// |   GOROUTINE   |<-CHANNEL->|   GOROUTINE    |
// +---------------+           +----------------+
//
// 创建通道:通道 := make(chan 消息类型) 
// 发送消息:通道 <- 消息
// 接收消息:消息 = <-通道
package main
import ("fmt""time"
)
// 通道可以作为函数的参数被传递
// 读写通道:通道 chan 消息类型
// 只读通道:通道 <-chan 消息类型
// 只写通道:通道 chan<- 消息类型func step1a(c chan string) {fmt.Println("Task A of step 1 start...")time.Sleep(time.Second * 3)c <- "Task A of step 1 finished"//channel c的写入操作,会解阻塞c的可读
}
func step1b() {fmt.Println("Task B of step 1 start...")time.Sleep(time.Second * 2)fmt.Println("Task B of step 1 finished")
}
func step2() {fmt.Println("Step 2 start...")time.Sleep(time.Second * 2)fmt.Println("Step 2 finished")
} func main() {c := make(chan string)go step1a(c) // 将通道c作为参数传给step1astep1b()message := <-c // 当channel c为空时,读取数据是阻塞的,需要等待。因此,// 借助channel解决了同步问题fmt.Println(message)step2()
}
// 打印输出:
// Task B of step 1 start...
// Task A of step 1 start...
// Task B of step 1 finished
// Task A of step 1 finished
// Step 2 start...
// Step 2 finished 

带缓冲的通道(缓冲通道的遍历-2种方法)

 

  • 默认情况下的通道是不带缓冲区的,发送者向这样的通道发送消息,如果没有接收者前来接收,发送过程会发生阻塞。
  • 如果不希望发送过程因所发消息无人接收而阻塞,可以使用带缓冲的通道
  • 缓冲意味着可将消息缓存在通道内部,发送者不必等待,什么时候接收者准备就绪了,再从通道中将消息取走。
  • 创建带缓冲的通道,可在make函数的第二个参数中指定缓冲区的长度。
    • c := make(chan string, 2) // 该通道最多缓存两条消息,向其发送更多消息将导致错误。
  • 访问缓冲通道中的消息(2中方法)可以通过循环迭代缓冲通道中的所有消息。
    • for i := 0; i < cap(c); i++ { message := <-c }cap(c)可以返回channel c的缓冲数据个数
    • for message := range c { fmt.Println(message) } 
// 有缓冲通道
// 无缓冲通道:如果没有接收者从通道中接收消息,发送者将会阻塞等待
// 有缓冲通道:如果没有接收者从通道中接收消息,发送者不会阻塞等待
// 无论哪种通道,如果没有发送者向通道发送消息,接收者都会阻塞等待
// 创建有缓冲通道:通道 := make(chan 消息类型, 消息数)
package main
import ("fmt""time"
)func step1a(c chan string) {fmt.Println("Task A of step 1 start...")time.Sleep(time.Second * 3)c <- "Task A of step 1 finished" // task a写入完成信息
} func step1b(c chan string) {fmt.Println("Task B of step 1 start...")time.Sleep(time.Second * 2)c <- "Task B of step 1 finished" // task b写入完成信息
}
func step2() {fmt.Println("Step 2 start...")time.Sleep(time.Second * 2)fmt.Println("Step 2 finished")
} func main() {c := make(chan string, 2)go step1a(c)  // 执行task1ago step1b(c)  // 执行task1bfor i := 0; i < cap(c); i++ {message := <-c			// 遍历channle c,模拟确认所有前置已完成fmt.Println(message)}step2()
}
// 打印输出:
// Task B of step 1 start...
// Task A of step 1 start...
// Task B of step 1 finished
// Task A of step 1 finished
// Step 2 start...
// Step 2 finished

这篇关于12.1 通道-使用通道、带缓冲的通道的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1010560

相关文章

Spring Boot整合Redis注解实现增删改查功能(Redis注解使用)

《SpringBoot整合Redis注解实现增删改查功能(Redis注解使用)》文章介绍了如何使用SpringBoot整合Redis注解实现增删改查功能,包括配置、实体类、Repository、Se... 目录配置Redis连接定义实体类创建Repository接口增删改查操作示例插入数据查询数据删除数据更

使用python生成固定格式序号的方法详解

《使用python生成固定格式序号的方法详解》这篇文章主要为大家详细介绍了如何使用python生成固定格式序号,文中的示例代码讲解详细,具有一定的借鉴价值,有需要的小伙伴可以参考一下... 目录生成结果验证完整生成代码扩展说明1. 保存到文本文件2. 转换为jsON格式3. 处理特殊序号格式(如带圈数字)4

Java使用Swing生成一个最大公约数计算器

《Java使用Swing生成一个最大公约数计算器》这篇文章主要为大家详细介绍了Java使用Swing生成一个最大公约数计算器的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可以了解一下... 目录第一步:利用欧几里得算法计算最大公约数欧几里得算法的证明情形 1:b=0情形 2:b>0完成相关代码第二步:加

Java中流式并行操作parallelStream的原理和使用方法

《Java中流式并行操作parallelStream的原理和使用方法》本文详细介绍了Java中的并行流(parallelStream)的原理、正确使用方法以及在实际业务中的应用案例,并指出在使用并行流... 目录Java中流式并行操作parallelStream0. 问题的产生1. 什么是parallelS

Linux join命令的使用及说明

《Linuxjoin命令的使用及说明》`join`命令用于在Linux中按字段将两个文件进行连接,类似于SQL的JOIN,它需要两个文件按用于匹配的字段排序,并且第一个文件的换行符必须是LF,`jo... 目录一. 基本语法二. 数据准备三. 指定文件的连接key四.-a输出指定文件的所有行五.-o指定输出

Linux jq命令的使用解读

《Linuxjq命令的使用解读》jq是一个强大的命令行工具,用于处理JSON数据,它可以用来查看、过滤、修改、格式化JSON数据,通过使用各种选项和过滤器,可以实现复杂的JSON处理任务... 目录一. 简介二. 选项2.1.2.2-c2.3-r2.4-R三. 字段提取3.1 普通字段3.2 数组字段四.

Linux kill正在执行的后台任务 kill进程组使用详解

《Linuxkill正在执行的后台任务kill进程组使用详解》文章介绍了两个脚本的功能和区别,以及执行这些脚本时遇到的进程管理问题,通过查看进程树、使用`kill`命令和`lsof`命令,分析了子... 目录零. 用到的命令一. 待执行的脚本二. 执行含子进程的脚本,并kill2.1 进程查看2.2 遇到的

详解SpringBoot+Ehcache使用示例

《详解SpringBoot+Ehcache使用示例》本文介绍了SpringBoot中配置Ehcache、自定义get/set方式,并实际使用缓存的过程,文中通过示例代码介绍的非常详细,对大家的学习或者... 目录摘要概念内存与磁盘持久化存储:配置灵活性:编码示例引入依赖:配置ehcache.XML文件:配置

Java 虚拟线程的创建与使用深度解析

《Java虚拟线程的创建与使用深度解析》虚拟线程是Java19中以预览特性形式引入,Java21起正式发布的轻量级线程,本文给大家介绍Java虚拟线程的创建与使用,感兴趣的朋友一起看看吧... 目录一、虚拟线程简介1.1 什么是虚拟线程?1.2 为什么需要虚拟线程?二、虚拟线程与平台线程对比代码对比示例:三

k8s按需创建PV和使用PVC详解

《k8s按需创建PV和使用PVC详解》Kubernetes中,PV和PVC用于管理持久存储,StorageClass实现动态PV分配,PVC声明存储需求并绑定PV,通过kubectl验证状态,注意回收... 目录1.按需创建 PV(使用 StorageClass)创建 StorageClass2.创建 PV