禅与计算机 禅与计算机
首页
  • Java基础

    • 聊一聊java一些核心知识点
    • 聊聊java面向对象核心知识点
    • 聊聊Java中的异常
    • 聊聊Java中的常用类String
    • 万字长文带你细聊Java注解本质
    • 来聊聊Java的反射机制
    • 深入解析Java泛型的魅力与机制
    • Java集合框架深度解析与面试指南
    • Java常用集合类HashMap深度解析
    • LinkedHashMap源码到面试题的全解析
    • 深入解析CopyOnWriteArrayList的工作机制
    • Java基础IO总结
    • Java三大IO模型小结
    • Java BIO NIO AIO详解
    • Java进阶NIO之IO多路复用详解
    • Java8流式编程入门
    • 一文速通lambda与函数式编程
    • Java8函数式方法引用最佳实践
  • Java并发编程

    • Java并发编程基础小结
    • 深入理解Java中的final关键字
    • 浅谈Java并发安全发布技术
    • 浅谈Java并发编程中断的哲学
    • Java线程池知识点小结
    • 浅谈Java线程池中拒绝策略与流控的艺术
    • synchronized关键字使用指南
    • 深入源码解析synchronized关键字
    • 详解JUC包下的锁
    • 详解并发编程中的CAS原子类
    • LongAdder源码分析
    • AQS源码解析
    • 深入剖析Java并发编程中的死锁问题
    • Java并发容器总结
    • 详解Java并发编程volatile关键字
    • 并发编程ThreadLocal必知必会
    • CompletableFuture基础实践小结
    • CompletableFuture异步多任务最佳实践
    • 硬核详解FutureTask设计与实现
    • 线程池大小设置的底层逻辑与场景化方案
    • 来聊一个有趣的限流器RateLimiter
  • JVM相关

    • 从零开始掌握 JVM
    • JVM核心知识点小结
    • JVM指令集概览:基础与应用
    • JVM类加载器深度解析
    • JVM方法区深度解析
    • Java内存模型JMM详解
    • Java对象大小的精确计算方法
    • 逃逸分析在Java中的应用与优化
    • 从零开始理解JVM的JIT编译机制
    • G1垃圾回收器:原理详解与调优指南
    • JVM故障排查实战指南
    • JVM内存问题排错最佳实践
    • JVM内存溢出排查指南
    • 简明的Arthas使用教程
    • 简明的Arthas配置及基础运维教程
    • 基于Arthas Idea的JVM故障排查与指令生成
    • 基于arthas量化监控诊断java应用方法论与实践
    • 深入剖析arthas技术原理
  • 深入理解Spring框架

    • Spring 核心知识点全面解析
    • Spring核心功能IOC详解
    • Spring AOP 深度剖析与实践
    • Spring 三级缓存机制深度解析
    • 深入 Spring 源码,剖析设计模式的落地实践
    • 探索 Spring 事务的奥秘
    • 深入解析Spring Bean的生命周期管理
    • 解读 Spring Boot 核心知识点
    • Spring Boot 启动优化实战:1分钟到13秒的排查与优化之路
    • Spring Boot自动装配原理及实践
    • 一文快速上手Sharding-JDBC
    • sharding-jdbc如何实现分页查询
    • 基于DynamicDataSource整合分库分表框架Shardingsphere
  • 计算机组成原理

    • 计算机硬件知识小结
    • CPU核心知识点小结
    • 浅谈CPU流水线的艺术
    • 从Java程序员视角聊聊CPU缓存
    • CPU任务调度和伪共享问题小结
    • CPU MESI缓存一致性协议
    • CPU内存管理机制
    • 内存深度解析
    • 磁盘存储原理
    • 详解计算机启动步骤
    • CPU南北桥架构与发展史
    • CPU中断机制与硬件交互详解
  • 操作系统

    • 如何实现一个高性能服务器
    • Linux文件结构与文件权限
    • Linux常见压缩指令小结
    • Linux核心系统调用详解
    • Linux进程管理
    • Linux线程管理
    • 进程与线程深度解析
    • Linux进程间通信机制
    • 零拷贝技术原理与实践
    • CPU缓存一致性问题深度解析
    • IO任务与CPU调度艺术
  • 计算机网络

    • 网卡通信原理详解
    • 网卡数据包处理指南
    • 基于抓包详解TCP协议
  • 编码最佳实践

    • 浅谈现代软件工程TDD最佳实践
    • 浅谈TDD模式下并发程序设计与实现
    • 面向AI编程新范式Trae后端开发环境搭建与实践
    • 基于提示词工程的Redis签到功能开发实践
    • 基于Vibe Coding的Redis分页查询实现
    • 告别AI无效对话:资深工程师的提示词设计最佳实践
  • 实用技巧与配置

    • Mac常用快捷键与效率插件指南
    • Keynote技术科普短视频制作全攻略
  • 写作

    • 写好技术博客的5大核心原则:从认知科学到AI工具的全流程指南
  • 开发工具

    • IDEA配置详解与高效使用指南
  • Nodejs
  • 博客搭建
  • Redis

    • Redis核心知识小结
    • 解锁Redis发布订阅模式
    • 掌握Redis事务
    • Redis主从复制技术
    • Redis的哨兵模式详解
    • 深度剖析Redisson分布式锁
    • 详解redis单线程设计思路
    • 来聊聊Redis所实现的Reactor模型
    • Redis RDB持久化源码深度解析
    • 来聊聊redis的AOF写入
    • 来聊聊Redis持久化AOF管道通信的设计
    • 来聊聊redis集群数据迁移
    • Redis SDS动态字符串深度解析
    • 高效索引的秘密:redis跳表设计与实现
    • 聊聊redis中的字典设计与实现
  • MySQL

    • MySQL基础知识点小结
    • 解读MySQL 索引基础
    • MySQL 索引进阶指南
    • 解读MySQL Explain关键字
    • 探秘 MySQL 锁:原理与实践
    • 详解MySQL重做日志redolog
    • 详解undoLog在MySQL MVCC中的运用
    • MySQL二进制日志binlog核心知识点
    • MySQL高效插入数据的最佳实践
    • MySQL分页查询优化指南
    • MySQL流式查询的奥秘与应用解析
    • 来聊聊分库分表
    • 来聊聊大厂常用的分布式ID生成方案
  • ElasticSearch

    • 从Lucene到Elasticsearch:进化之路
    • ES 基础使用指南
    • ElasticSearch如何写入一篇文档
    • 深入剖析Elasticsearch文档读取原理
    • 聊聊ElasticSearch性能调优
    • Spring借助Easy-Es操作ES
  • Netty

    • 一文快速了解高性能网络通信框架Netty
    • Netty网络传输简记
    • 来聊聊Netty的ByteBuf
    • 来聊聊Netty消息发送的那些事
    • 解密Netty高性能之谜:NioEventLoop线程池阻塞分析
    • 详解Netty中的责任链Pipeline如何管理ChannelHandler
    • Netty Reactor模型常见知识点小结
    • Netty如何驾驭TCP流式传输?粘包拆包问题全解
    • Netty解码器源码解析
  • 消息队列

    • 一文快速入门消息队列
    • 消息队列RocketMQ入门指南
    • 基于RocketMQ实现分布式事务
    • RocketMQ容器化最佳实践
    • RocketMQ常见问题与深度解析
    • Kafka快速安装与使用指南
  • Nginx

    • Linux下的nginx安装
    • Nginx基础入门总结
    • Nginx核心指令小结
    • Nginx进程结构与核心模块初探
    • Nginx应用进阶HTTP核心模块配置
    • Nginx缓存及HTTPS配置小记
    • nginx高可用实践简记
    • Nginx性能优化
  • 微服务基础

    • 微服务基础知识小结
    • 分布式事务核心概念小结
    • OpenFeign核心知识小结
    • 微服务组件Gateway核心使用小结
    • 分布式事务Seata实践
    • 用 Docker Compose 完成 Seata 的整合部署
  • Nacos

    • Nacos服务注册原理全解析
    • Nacos服务订阅流程全解析
    • Nacos服务变更推送流程全解析
    • 深入解析SpringCloud负载均衡器Loadbalancer
    • Nacos源码环境搭建与调试指南
  • Seata

    • 深度剖析Seata源码
  • Docker部署

    • 一文快速掌握docker的理念和基本使用
    • 使用docker编排容器
    • 基于docker-compose部署微服务基本环境
    • 基于docker容器化部署微服务
    • Gateway全局异常处理及请求响应监控
    • Docker图形化界面工具Portainer最佳实践
  • Go基础

    • 一文带你速通Go语言基础语法
    • 一文快速掌握Go语言切片
    • 来聊聊go语言的hashMap
    • 一文速通go语言类型系统
    • 浅谈Go语言中的面向对象
    • go语言是如何实现协程的
    • 聊聊go语言中的GMP模型
    • 极简的go语言channel入门
    • 聊聊go语言基于epoll的网络并发实现
    • 写给Java开发的Go语言协程实践
  • mini-redis实战

    • 来聊聊我用go手写redis这件事
    • mini-redis如何解析处理客户端请求
    • 实现mini-redis字符串操作
    • 硬核复刻redis底层双向链表核心实现
    • 动手复刻redis之go语言下的字典的设计与落地
    • Go 语言下的 Redis 跳表设计与实现
    • Go 语言版 Redis 有序集合指令复刻探索
  • 项目编排

    • Spring脚手架创建简记
    • Spring脚手架集成分页插件
    • Spring脚手架集成校验框架
    • maven父子模块两种搭建方式简记
    • SpringBoot+Vue3前后端快速整合入门
    • 来聊聊Java项目分层规范
  • 场景设计

    • Java实现文件分片上传
    • 基于时间缓存优化浏览器轮询阻塞问题
    • 基于EasyExcel实现高效导出
    • 10亿数据高效插入MySQL最佳方案
    • 从开源框架中学习那些实用的位运算技巧
  • CI/CD

    • 基于NETAPP实现内网穿透
    • 基于Gitee实现Jenkins自动化部署SpringBoot项目
    • Jenkins离线安装部署教程简记
    • 基于Nexus搭建Maven私服基础入门
    • 基于内网的Jenkins整合gitlab综合方案简记
  • 监控方法论

    • SpringBoot集成Prometheus与Grafana监控
    • Java监控度量Micrometer全解析
    • 从 micrometer计量器角度快速上手promQL
    • 硬核安利一个监控告警开源项目Nightingale
  • Spring AI

    • Spring AI Alibaba深度实战:一文掌握智能体开发全流程
    • Spring AI Alibaba实战:JVM监控诊断Arthas Agent的工程化构建与最佳实践
  • 大模型评测

    • M2.7 真能打!我用两个真实场景测了测,结果有点意外
    • Qoder JetBrains插件评测:祖传代码重构与接口优化实战
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

sharkchili

计算机禅修者
首页
  • Java基础

    • 聊一聊java一些核心知识点
    • 聊聊java面向对象核心知识点
    • 聊聊Java中的异常
    • 聊聊Java中的常用类String
    • 万字长文带你细聊Java注解本质
    • 来聊聊Java的反射机制
    • 深入解析Java泛型的魅力与机制
    • Java集合框架深度解析与面试指南
    • Java常用集合类HashMap深度解析
    • LinkedHashMap源码到面试题的全解析
    • 深入解析CopyOnWriteArrayList的工作机制
    • Java基础IO总结
    • Java三大IO模型小结
    • Java BIO NIO AIO详解
    • Java进阶NIO之IO多路复用详解
    • Java8流式编程入门
    • 一文速通lambda与函数式编程
    • Java8函数式方法引用最佳实践
  • Java并发编程

    • Java并发编程基础小结
    • 深入理解Java中的final关键字
    • 浅谈Java并发安全发布技术
    • 浅谈Java并发编程中断的哲学
    • Java线程池知识点小结
    • 浅谈Java线程池中拒绝策略与流控的艺术
    • synchronized关键字使用指南
    • 深入源码解析synchronized关键字
    • 详解JUC包下的锁
    • 详解并发编程中的CAS原子类
    • LongAdder源码分析
    • AQS源码解析
    • 深入剖析Java并发编程中的死锁问题
    • Java并发容器总结
    • 详解Java并发编程volatile关键字
    • 并发编程ThreadLocal必知必会
    • CompletableFuture基础实践小结
    • CompletableFuture异步多任务最佳实践
    • 硬核详解FutureTask设计与实现
    • 线程池大小设置的底层逻辑与场景化方案
    • 来聊一个有趣的限流器RateLimiter
  • JVM相关

    • 从零开始掌握 JVM
    • JVM核心知识点小结
    • JVM指令集概览:基础与应用
    • JVM类加载器深度解析
    • JVM方法区深度解析
    • Java内存模型JMM详解
    • Java对象大小的精确计算方法
    • 逃逸分析在Java中的应用与优化
    • 从零开始理解JVM的JIT编译机制
    • G1垃圾回收器:原理详解与调优指南
    • JVM故障排查实战指南
    • JVM内存问题排错最佳实践
    • JVM内存溢出排查指南
    • 简明的Arthas使用教程
    • 简明的Arthas配置及基础运维教程
    • 基于Arthas Idea的JVM故障排查与指令生成
    • 基于arthas量化监控诊断java应用方法论与实践
    • 深入剖析arthas技术原理
  • 深入理解Spring框架

    • Spring 核心知识点全面解析
    • Spring核心功能IOC详解
    • Spring AOP 深度剖析与实践
    • Spring 三级缓存机制深度解析
    • 深入 Spring 源码,剖析设计模式的落地实践
    • 探索 Spring 事务的奥秘
    • 深入解析Spring Bean的生命周期管理
    • 解读 Spring Boot 核心知识点
    • Spring Boot 启动优化实战:1分钟到13秒的排查与优化之路
    • Spring Boot自动装配原理及实践
    • 一文快速上手Sharding-JDBC
    • sharding-jdbc如何实现分页查询
    • 基于DynamicDataSource整合分库分表框架Shardingsphere
  • 计算机组成原理

    • 计算机硬件知识小结
    • CPU核心知识点小结
    • 浅谈CPU流水线的艺术
    • 从Java程序员视角聊聊CPU缓存
    • CPU任务调度和伪共享问题小结
    • CPU MESI缓存一致性协议
    • CPU内存管理机制
    • 内存深度解析
    • 磁盘存储原理
    • 详解计算机启动步骤
    • CPU南北桥架构与发展史
    • CPU中断机制与硬件交互详解
  • 操作系统

    • 如何实现一个高性能服务器
    • Linux文件结构与文件权限
    • Linux常见压缩指令小结
    • Linux核心系统调用详解
    • Linux进程管理
    • Linux线程管理
    • 进程与线程深度解析
    • Linux进程间通信机制
    • 零拷贝技术原理与实践
    • CPU缓存一致性问题深度解析
    • IO任务与CPU调度艺术
  • 计算机网络

    • 网卡通信原理详解
    • 网卡数据包处理指南
    • 基于抓包详解TCP协议
  • 编码最佳实践

    • 浅谈现代软件工程TDD最佳实践
    • 浅谈TDD模式下并发程序设计与实现
    • 面向AI编程新范式Trae后端开发环境搭建与实践
    • 基于提示词工程的Redis签到功能开发实践
    • 基于Vibe Coding的Redis分页查询实现
    • 告别AI无效对话:资深工程师的提示词设计最佳实践
  • 实用技巧与配置

    • Mac常用快捷键与效率插件指南
    • Keynote技术科普短视频制作全攻略
  • 写作

    • 写好技术博客的5大核心原则:从认知科学到AI工具的全流程指南
  • 开发工具

    • IDEA配置详解与高效使用指南
  • Nodejs
  • 博客搭建
  • Redis

    • Redis核心知识小结
    • 解锁Redis发布订阅模式
    • 掌握Redis事务
    • Redis主从复制技术
    • Redis的哨兵模式详解
    • 深度剖析Redisson分布式锁
    • 详解redis单线程设计思路
    • 来聊聊Redis所实现的Reactor模型
    • Redis RDB持久化源码深度解析
    • 来聊聊redis的AOF写入
    • 来聊聊Redis持久化AOF管道通信的设计
    • 来聊聊redis集群数据迁移
    • Redis SDS动态字符串深度解析
    • 高效索引的秘密:redis跳表设计与实现
    • 聊聊redis中的字典设计与实现
  • MySQL

    • MySQL基础知识点小结
    • 解读MySQL 索引基础
    • MySQL 索引进阶指南
    • 解读MySQL Explain关键字
    • 探秘 MySQL 锁:原理与实践
    • 详解MySQL重做日志redolog
    • 详解undoLog在MySQL MVCC中的运用
    • MySQL二进制日志binlog核心知识点
    • MySQL高效插入数据的最佳实践
    • MySQL分页查询优化指南
    • MySQL流式查询的奥秘与应用解析
    • 来聊聊分库分表
    • 来聊聊大厂常用的分布式ID生成方案
  • ElasticSearch

    • 从Lucene到Elasticsearch:进化之路
    • ES 基础使用指南
    • ElasticSearch如何写入一篇文档
    • 深入剖析Elasticsearch文档读取原理
    • 聊聊ElasticSearch性能调优
    • Spring借助Easy-Es操作ES
  • Netty

    • 一文快速了解高性能网络通信框架Netty
    • Netty网络传输简记
    • 来聊聊Netty的ByteBuf
    • 来聊聊Netty消息发送的那些事
    • 解密Netty高性能之谜:NioEventLoop线程池阻塞分析
    • 详解Netty中的责任链Pipeline如何管理ChannelHandler
    • Netty Reactor模型常见知识点小结
    • Netty如何驾驭TCP流式传输?粘包拆包问题全解
    • Netty解码器源码解析
  • 消息队列

    • 一文快速入门消息队列
    • 消息队列RocketMQ入门指南
    • 基于RocketMQ实现分布式事务
    • RocketMQ容器化最佳实践
    • RocketMQ常见问题与深度解析
    • Kafka快速安装与使用指南
  • Nginx

    • Linux下的nginx安装
    • Nginx基础入门总结
    • Nginx核心指令小结
    • Nginx进程结构与核心模块初探
    • Nginx应用进阶HTTP核心模块配置
    • Nginx缓存及HTTPS配置小记
    • nginx高可用实践简记
    • Nginx性能优化
  • 微服务基础

    • 微服务基础知识小结
    • 分布式事务核心概念小结
    • OpenFeign核心知识小结
    • 微服务组件Gateway核心使用小结
    • 分布式事务Seata实践
    • 用 Docker Compose 完成 Seata 的整合部署
  • Nacos

    • Nacos服务注册原理全解析
    • Nacos服务订阅流程全解析
    • Nacos服务变更推送流程全解析
    • 深入解析SpringCloud负载均衡器Loadbalancer
    • Nacos源码环境搭建与调试指南
  • Seata

    • 深度剖析Seata源码
  • Docker部署

    • 一文快速掌握docker的理念和基本使用
    • 使用docker编排容器
    • 基于docker-compose部署微服务基本环境
    • 基于docker容器化部署微服务
    • Gateway全局异常处理及请求响应监控
    • Docker图形化界面工具Portainer最佳实践
  • Go基础

    • 一文带你速通Go语言基础语法
    • 一文快速掌握Go语言切片
    • 来聊聊go语言的hashMap
    • 一文速通go语言类型系统
    • 浅谈Go语言中的面向对象
    • go语言是如何实现协程的
    • 聊聊go语言中的GMP模型
    • 极简的go语言channel入门
    • 聊聊go语言基于epoll的网络并发实现
    • 写给Java开发的Go语言协程实践
  • mini-redis实战

    • 来聊聊我用go手写redis这件事
    • mini-redis如何解析处理客户端请求
    • 实现mini-redis字符串操作
    • 硬核复刻redis底层双向链表核心实现
    • 动手复刻redis之go语言下的字典的设计与落地
    • Go 语言下的 Redis 跳表设计与实现
    • Go 语言版 Redis 有序集合指令复刻探索
  • 项目编排

    • Spring脚手架创建简记
    • Spring脚手架集成分页插件
    • Spring脚手架集成校验框架
    • maven父子模块两种搭建方式简记
    • SpringBoot+Vue3前后端快速整合入门
    • 来聊聊Java项目分层规范
  • 场景设计

    • Java实现文件分片上传
    • 基于时间缓存优化浏览器轮询阻塞问题
    • 基于EasyExcel实现高效导出
    • 10亿数据高效插入MySQL最佳方案
    • 从开源框架中学习那些实用的位运算技巧
  • CI/CD

    • 基于NETAPP实现内网穿透
    • 基于Gitee实现Jenkins自动化部署SpringBoot项目
    • Jenkins离线安装部署教程简记
    • 基于Nexus搭建Maven私服基础入门
    • 基于内网的Jenkins整合gitlab综合方案简记
  • 监控方法论

    • SpringBoot集成Prometheus与Grafana监控
    • Java监控度量Micrometer全解析
    • 从 micrometer计量器角度快速上手promQL
    • 硬核安利一个监控告警开源项目Nightingale
  • Spring AI

    • Spring AI Alibaba深度实战:一文掌握智能体开发全流程
    • Spring AI Alibaba实战:JVM监控诊断Arthas Agent的工程化构建与最佳实践
  • 大模型评测

    • M2.7 真能打!我用两个真实场景测了测,结果有点意外
    • Qoder JetBrains插件评测:祖传代码重构与接口优化实战
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Go基础

    • 关于我想写一个Go系列的这件事
    • Go环境以及IDE安装配置简记
    • 一文带你速通Go语言基础语法
    • 速通Go语言编译过程
    • 聊聊Go程序是如何运行的
    • 一文带你速通Go语言数组
    • 一文快速掌握Go语言切片
    • 深入理解go语言中的切片
    • 一文带你速通编程语言中的指针
    • 来聊聊go语言的hashMap
    • 聊聊go语言中map如何解决散列性能下降
    • 一文速通Go语言的map
    • 一文速通go语言类型系统
    • 浅谈Go语言中的面向对象
    • 聊聊go语言中的内存填充
    • 聊聊Go语言中的字符串
    • 聊一聊go语言的空值
    • Go语言企业级日志管理
    • 基于魔改Nightingale源码浅谈go语言包模块管理
    • go语言是如何解决map并发安全问题的
    • go语言是如何实现协程的
    • go语言如何实现协程的抢占式调度的
    • 聊聊go语言中的GMP模型
    • 聊聊go语言对于协程并发的设计
    • 协程是否越多越好
    • 基于Go语言的网络IO扫盲
    • 聊聊go语言基于epoll的网络并发实现
    • 聊聊Go语言中的networkpoller工作机制
    • 聊聊go语言对于socket的抽象
    • 用Go语言实现一个单协程消费者模型
    • 用Go实现一个无界资源池
    • 用go语言实现一个有界协程池
    • 极简的go语言channel入门
    • 聊聊go语言channel为什么可以高效
    • 聊聊go语言channel中的一些小技巧
    • 写给Java开发的Go语言协程实践
      • 写在文章开头
      • 基础概念
        • 进程与线程
        • 协程扫盲
      • 详解goroutine
      • 并发与并行
        • 并发与并行的区别
        • goroutine并发工作示例
        • 示例1-打印字母两次
        • 示例2-输出5000以内的质数
        • goroutine并行工作示例
      • goroutine的协程安全问题
        • 问题重现
        • 原因剖析
        • 解决方案
        • 原子函数
        • 互斥锁
      • go语言中的通道
        • 通道是如何解决协程安全问题的
        • 无缓冲通道
        • 无缓冲通道实现击球比赛
        • 无缓冲接力赛
        • 有缓冲通道示例
      • 小结
      • 参考
  • mini-redis实战

  • Go语言
  • Go基础
sharkchili
2023-07-23
目录

写给Java开发的Go语言协程实践

# 写在文章开头

go语言基于协程实现并发使得一个线程可以在用户态上灵活切换,不仅充分利用每个线程,通过协程上下文的切换使得降低了系统的开销,所以本文就基于几个经典案例探讨一下go语言中的协程——goroutine。

Hi,我是 sharkChili ,是个不断在硬核技术上作死的 java coder ,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili 。

因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

# 基础概念

# 进程与线程

在正是介绍goroutine协程前,我们先来复习一些计算机基础知识,如下图,我们可以看到进程的三大部分:

  1. 每一个进程都会通过磁盘读取需要的数据和代码。
  2. 每一个进程都会拥有操作文件、硬件设备和操作系统的句柄。
  3. 进程执行时,都是以线程为单位,所以每一个进程至少都有一个线程,每一个线程都会对应一个功能代码段,然后操作系统调度器根据某个调度算法执行进程中的线程代码。

尽管多线程提升了IO密集型任务的执行效率,但是频繁的线程上下文切换开销也是很大的,所以对于高并发的任务,使用多线程处理并发IO请求终会出现严重的性能瓶颈。

# 协程扫盲

我们是否有一种办法可以做到让线程切换工作呢?例如某段代码的函数要发起一个阻塞2s的HTTP请求,我们是否可以让当前线程在函数发起HTTP请求阻塞时,将这个方法执行时的CPU寄存器信息和函数运行时的状态信息(栈帧)信息都记录下来,并存到堆区。

然后这个空闲出来的线程就可以执行另一个代码段了,这种将代码执行信息暂时挂起让线程去执行另一个代码段的方式,避免了频繁的内核态线程切换,只需在用户态即可完成IO阻塞切换另一个业务功能的方式正是本文所说的多协程。

小结一下协程的工作流程:

  1. 线程1执行协程1的代码。
  2. 协程1代码阻塞,将执行的栈帧信息存到堆区中,挂起该协程。
  3. 线程1执行协程2。
  4. 协程2执行完成,从堆区中获取协程1调用的栈帧信息,继续执行。
  5. 协程1执行完成,线程1继续寻找需要执行的协程。

可以看到只用一个线程在用户态上灵活的切换避免了在内核态切换的开销,并且单线程利用率显著提高。通俗来说我们完全可以将协程理解为一种轻量级的线程。

# 详解goroutine

在go语言中,协程这个概念对应着就是goroutine,每个goroutine都有自己的堆栈和寄存器,可以在不同的系统线程中执行,go语言会为每一个线程分配一个逻辑处理器调取这些goroutine,然后基于这个逻辑处理器,我们可以创建成千上万个goroutine,通过用户态级别的协程切换高效实现并发编程。

如下图,在逻辑处理器上,每一个需要被执行的线程会构成一个队列,按照逻辑处理器的调度算法分段时间执行。

若以上图为例,假如我们的goroutine1要执行一个阻塞的系统调用(例如打开一个文件),goroutine就会在阻塞期间被分配到另一个线程的逻辑处理器上等待系统调用完成。

一旦goroutine1系统调用完成,goroutine1就会和线程2逻辑处理器分离,回到线程1的逻辑处理器上等待调度执行。然后线程2的逻辑处理器则继续处理其他的goroutine。

同样的,假如goroutine1执行的是网络IO,那么情况就有些不同的,当goroutine1因为网络IO而发起阻塞时,这个goroutine就会被移动到运行时集成的网络轮询器上,待网络轮询器读或写准备就绪,goroutine1才会再次分配到逻辑处理的队列中等待被处理。

# 并发与并行

# 并发与并行的区别

有了上文关于协程的基础之后,我们再来聊聊并发与并行,咋一看这两个概念没有什么区别,实际上并发指的是同一个时间管理多件事,注意是管理多件事,这一点我们完全可以理解为上文中网络中单线程处理网络IO阻塞时,挂起goroutine1去处理别人的goroutine。

而并行则是同时处理多件事,注意是同时处理多见件,可以理解为我们多个物理核心对应的线程同一时间用各自的逻辑处理器处理对应的协程。

# goroutine并发工作示例

# 示例1-打印字母两次

为了更好的理解并发,我们这里给出这样一个例子,我们当前功能会在一个线程上创建两个协程,这两个协程分别打印两次大写的A-Z和小写的a-z。因为仅仅使用一个线程在一个CPU核心上工作,而且打印并不会出现IO阻塞,所以最终我们的输出结果应该是先输出大写的A-Z两次,随后再输出小写的a-z两次。

随后我们给出下面这段代码,整体逻辑为:

  1. 设置只用一个逻辑处理器个调度器使用。
  2. 为两个线程设置一个WaitGroup(可以理解为Java的CountDownLatch)。
  3. 启用两个线程分别开始打印工作,每次打印完成WaitGroup减去1。
func main() {
	//启动一个逻辑处理器给调度器使用
	runtime.GOMAXPROCS(1)

	fmt.Println("启动两个goroutine打印3次英文字母")

	//开启一个计数器,设置为2,等待两个协程执行完,从而做到流程控制
	var wg sync.WaitGroup
	wg.Add(2)

	//启动一个协程打印a-z两次
	go func() {
		//协程结束之后,将计数器减1
		defer wg.Done()

		for i := 0; i < 3; i++ {
			for i := 'a'; i < 'a'+26; i++ {
				fmt.Printf("%c ", i)
			}
			//换行便于查看
			fmt.Println("\n")
		}
	}()

	//启动一个协程打印A-Z两次
	go func() {
		//协程结束之后,将计数器减1
		defer wg.Done()

		for i := 0; i < 3; i++ {
			for i := 'A'; i < 'A'+26; i++ {
				fmt.Printf("%c ", i)
			}
			//换行便于查看
			fmt.Println("\n")
		}
	}()

	fmt.Println("等待两个goroutine执行完")
	//等待两个goroutine执行完
	wg.Wait()

	fmt.Println("两个goroutine执行完成")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

输出结果如下,可以看到两个协程并发执行,一个协程结束后另一个协程才继续工作。

启动两个goroutine打印3次英文字母
等待两个goroutine执行完                             
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 
                                                    
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 
                                                    
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 
                                                    
a b c d e f g h i j k l m n o p q r s t u v w x y z 
                                                    
a b c d e f g h i j k l m n o p q r s t u v w x y z 
                                                    
a b c d e f g h i j k l m n o p q r s t u v w x y z 
                                                    
两个goroutine执行完成  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 示例2-输出5000以内的质数

从上面例子你可能认为对于计算密集型的任务,并发就是一个线程上的协程按顺序执行,实际上对于计算机密集型的任务go的调度器也是会按照时间分片算法执行的,例如我们现在有两个协程需要分别找出5000以内的质数,对此我们给出下面这样一段代码。


var wg sync.WaitGroup

// 单核情况下长时间执行的协程会被调度器轮流分配
func main() {
	//给调度器分配一个处理器
	runtime.GOMAXPROCS(1)

	//初始化计数器
	wg.Add(2)

	fmt.Println("两个协程开始工作")

	go printPrime("A")
	go printPrime("B")

	fmt.Println("等待两个协程执行完成")
	wg.Wait()

	fmt.Println("两个协程执行结束")

}

func printPrime(prefix string) {
	defer wg.Done()
next:
	for outer := 0; outer < 5000; outer++ {
		//除了2和本身以外还能被整数的就不是质数
		for inner := 2; inner < outer; inner++ {
			if outer%inner == 0 {
				continue next
			}
		}
		//打印协程号和质数
		fmt.Printf("%s:%d\n", prefix, outer)
	}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

输出结果如下,可以看到对于计算密集型的任务并非一直拿着线程对应的逻辑处理器,我们的协程A输出到2851时,逻辑调度器便切换到协程B继续执行查找质数的逻辑。

两个协程开始工作
等待两个协程执行完成
.....
A:2843
A:2851
A:2857
B:3557
B:3559
B:3571
B:3581
B:3583
B:3593
B:3607
            
......               


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# goroutine并行工作示例

还记得我们上文中打印英文字母的示例吗?为了演示并发我们将逻辑调度处理器设置为1,确保两个协程都在同一个线程的逻辑处理器上运行。由于两个线程执行的功能并不耗时,所以打印结果是按照两个线程功能顺序执行的。

所以在这个示例中,我们将逻辑调度处理器设置为2,确保两个协程分别在不同线程的逻辑调度器上运行。

改造也很简单,将逻辑处理器设置为2即可。

runtime.GOMAXPROCS(2)
1

从输出结果可以看出,两个协程并行运行,打印结果混起来了

启动两个goroutine打印3次英文字母
等待两个goroutine执行完                               
a A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 
                                                      
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z   
                                                      
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z   
                                                      
b c d e f g h i j k l m n o p q r s t u v w x y z     
                                                      
a b c d e f g h i j k l m n o p q r s t u v w x y z   
                                                      
a b c d e f g h i j k l m n o p q r s t u v w x y z   
                                                      
两个goroutine执行完成                                 


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# goroutine的协程安全问题

# 问题重现

和多线程一样,多协程修改共享数据是也存在安全问题,对此我们给出下面这样一段代码,代码的逻辑非常简单,启动两个协程分别循环两次对共享数据自增。需要注意的是,为了更好的演示协程安全问题,笔者在每个协程读取共享变量时调用了runtime.Gosched(),这个操作会使得当前协程让出线程的执行权并将自己回到协程队列中等待下次被执行。

func main() {

	//设置计数器为协程数2
	wg.Add(2)

	//启动两个协程对counter进行自增
	go incCounter(1)
	go incCounter(2)

	//等待两个协程执行完
	wg.Wait()

	fmt.Println("执行完毕,counter:", counter)

}

func incCounter(id int) {
	//函数执行完成后 计数器减1
	defer wg.Done()


	for i := 0; i < 2; i++ {
		value := counter

		//将协程对应的线程执行权归还,并回到队列中
		runtime.Gosched()

		value++

		counter = value

	}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

最终输出结果为2,很明显协程安全问题出现了。

执行完毕,counter: 2

1
2

# 原因剖析

以上代码的含义,协程1首先读取counter的值为0,随后让出线程回到队列中等待逻辑处理器下次执行。

随后协程2也读取到0,让出线程执行权。

然后协程1完成自增值为1,进行下一次循环,读取到值为2,让出线程执行权。

然后执行权回到了协程2,协程2执行同样的自增值还是1,由此协程安全问题出现,后续步骤同理不多赘述,最终结果就变为2。

对于这类问题,go语言也为了提供的不错的命令,即在编译阶段执行go build -race,对于windows用户可需要执行CGO_ENABLED=1 go build -race。 注意这些操作是需要安装gcc的,windows开发用户执行上述命令前建议参考笔者这篇文章完成编译环境的配置。

Go环境以及IDE安装配置简记 (opens new window)

因为笔者是在Goland上开发,所以编译参数可以在edit configurations上设置。

最终会输出下面这样一段调用的堆栈信息,可以看到代码25行的调用49的代码,以及代码26行调用40行的代码之间存在竞争。

WARNING: DATA RACE
Write at 0x00000063c508 by goroutine 7:
  main.incCounter()
      F:/github/code/chapter6/listing09/listing09.go:49 +0xa4
  main.main.func1()
      F:/github/code/chapter6/listing09/listing09.go:25 +0x30

Previous read at 0x00000063c508 by goroutine 8:
  main.incCounter()
      F:/github/code/chapter6/listing09/listing09.go:40 +0x84
  main.main.func2()
      F:/github/code/chapter6/listing09/listing09.go:26 +0x30

Goroutine 7 (running) created at:
  main.main()
      F:/github/code/chapter6/listing09/listing09.go:25 +0x44

Goroutine 8 (finished) created at:
  main.main()
      F:/github/code/chapter6/listing09/listing09.go:26 +0x50
==================
Final Counter: 2
Found 1 data race(s)

Process finished with the exit code 66
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

最终我们定位到出现协程安全问题的代码。

Line 49: counter = value
Line 40: value := counter
Line 25: go incCounter(1)
Line 26: go incCounter(2)
1
2
3
4

# 解决方案

# 原子函数

和Java语言一样,go语言提供了atomic包,它的AddInt64通过底层硬件原子操作确保同一时刻只有一个协程操作共享数据,从而保证协程安全,所以我们改进后的代码如下所示:

func incCounter(id int) {
	//函数执行完成后 计数器减1
	defer wg.Done()

	for i := 0; i < 2; i++ {

		//将自增操作原子化解决协程安全问题
		atomic.AddInt64(&counter, 1)

		//将协程对应的线程执行权归还,并回到队列中
		runtime.Gosched()

	}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

AddXXXX函数的实现我们可以从源码的文档中看出,其实现的代码如下所示,即同一时刻只有一个协程通过硬件原语实现自增。

The add operation, implemented by the AddT functions, is the atomic equivalent of:
addr += delta return *addr
1
2

对于原子函数的示例,我们这里再补充一个安全读和安全写的组合操作,代码如下所示,可以看到代码的操作很简单:

  1. 计数器设置为2控制两个协程的流程。
  2. main线程启动两个协程,使用LoadInt64原子函数安全读共享变量shutdown,如果发现shutdown变成1则停止工作,反之一直循环工作。
  3. 主线程休眠1s后使用StoreInt64使用原子硬件原语完成原子修改操作。
  4. 两个协程在StoreInt64完成原子操作后读取到了最新的值,退出返回,将WaitGroup减去1。
var (
	shutdown int64
	wg       sync.WaitGroup
)

// main 使用原子工具包实现安全读和安全写
func main() {
	//计时器设置为2
	wg.Add(2)

	fmt.Println("两个协程开始工作.....")
	go doWork("A")
	go doWork("B")

	//休眠1s让两个协程多工作一会
	time.Sleep(1 * time.Second)

	fmt.Println("main准备停止两个协程")

	atomic.StoreInt64(&shutdown, 1)

	wg.Wait()

}

func doWork(name string) {
	defer wg.Done()
	for {
		fmt.Println("协程", name, "开始工作")

		time.Sleep(250 * time.Millisecond)

		if atomic.LoadInt64(&shutdown) == 1 {
			fmt.Println("收到停止信号,协程", name, "停止工作")
			break
		}
	}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

引用的这段文本来自于Go语言官方文档中的atomic包。文本中提到,LoadT和StoreT函数分别用于原子读取和存储内存地址中的值,它们分别对应于C语言中的return *addr和*addr = val。

在Go内存模型的术语中,如果一个原子操作A的效果被另一个原子操作B所观察到,则A在B之前同步。此外,程序中执行的所有原子操作都表现为按顺序一致的顺序执行。这个定义提供了与C++的顺序一致性原子(即操作原子性,操作会按照执行顺序先后完成原子修改)和Java的volatile变量相同的语义(即StoreT可以保证操作可见性,确保其他后于StoreT操作的协程读取到最新的值)。

The load and store operations, implemented by the LoadT and StoreT functions, are the atomic equivalents of "return *addr" and "*addr = val". In the terminology of the Go memory model, if the effect of an atomic operation A is observed by atomic operation B, then A “synchronizes before” B. Additionally, all the atomic operations executed in a program behave as though executed in some sequentially consistent order. This definition provides the same semantics as C++'s sequentially consistent atomics and Java's volatile variables.

# 互斥锁

除了原子操作以外,go还提供了mutex互斥锁确保协程安全,其工作原理在文档也给出了说明,某个协程对mutex上锁后,其他尝试取锁的协程就会阻塞,直到锁定mutex的协程释放锁。

Lock locks m. If the lock is already in use, the calling goroutine blocks until the mutex is available.
1

使用mutex的示例代码如下:

func incCounter(id int) {
	defer wg.Done()
	fmt.Println("协程", id, "开始工作")
	for i := 0; i < 2; i++ {
		mutex.Lock()
		{
			value := counter
			value++
			counter = value
			fmt.Println("协程", id, "上锁成功并修改值成功,counter:", value)
		}
		mutex.Unlock()
	}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# go语言中的通道

# 通道是如何解决协程安全问题的

除了原子类和互斥锁以外,go语言也可以将操作共享数据的协程之间建立通道,通过go语言内部所提供的双向通道同步机制,确保同一个时刻通道只有两个协程可以安全的交换数据,从而保证协程安全。

# 无缓冲通道

go语言提供的通道有两种,我们先来说说无缓冲通道,该通道的特点和名字一样,通道间不提供缓存数据的缓冲区,这意外着两个协程必须都准备好收发后通道才能进行通信,反之只要任何一方没有准备好通道就会阻塞。

# 无缓冲通道实现击球比赛

用无缓冲区实现一个击球比赛,我们的模拟方式如下:

  1. 建立一个整型通道,模拟击球场地。
  2. 用两个协程模拟运动员。
  3. 主线程对通道传入0,模拟开球。
  4. 收到信号的第一个协程模拟第一次击球的运动员,随机生成一个数,如果该数会被13整除,则关闭通道,模拟该球员击球失败,反之就将通道的值加一,并传入通道,模拟击球给对手。
  5. 另一个协程从通道中收到信号,如果收到关闭信号,则说明对方没击中球直接获胜,反之随机生成一个数,如果该数会被13整除,则关闭通道,模拟该球员击球失败,反之就将通道的值加一,并传入通道,模拟击球给对手。
  6. 重复上述4-5步骤。

示例代码如下:

var wg sync.WaitGroup

func main() {

	//计数器设置为协程数2
	wg.Add(2)

	//创建一个无缓冲通到
	court := make(chan int)

	go player("运动员A", court)
	go player("运动员B", court)

	fmt.Println("开球......")
	//往无缓冲通到存一个值
	court <- 0

	wg.Wait()
	fmt.Printf("比赛结束")

}

func player(name string, court chan int) {

	defer wg.Done()
	for {
		//等待缓冲通道的值
		ball, ok := <-court
		//没收到则说明某个协程将通道关闭了(即对方没有击中球)
		if !ok {
			fmt.Println("对手击球失败", name, "获胜")
			break
		}

		//模拟运动员击球,如果能被13整除则说明该运动员击球失败,关闭通道
		n := rand.Intn(100)
		if n%13 == 0 {
			fmt.Println(name, "未能击中球")
			close(court)
			break
		}

		//增加击球数并写入通道中,模拟击球给对方
		ball++

		fmt.Println(name, "击中球:", ball, "次")
		court <- ball
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

输出结果如下,可以看到无缓冲通道对于这种交互式通信的场景时效性还是很不错的。

开球......
运动员A 击中球: 1 次     
运动员B 击中球: 2 次     
运动员A 未能击中球       
对手击球失败 运动员B 获胜
比赛结束   
1
2
3
4
5
6

# 无缓冲接力赛

上文提到无缓冲通道因为必须收发双方都得准备好才能进行数据交换,所以也很适合模拟接力赛,整体实现步骤和上述差不多:

  1. 主线程创建通道。
  2. 开启一个协程1监听通道。
  3. 主线程向通道发送1,代表起跑。
  4. 协程1收到信号,发现是数据为1代表是第一棒,创建一个协程2模拟第2棒等待接力。
  5. 协程1休眠一会,模拟冲刺中,此时协程2孩子阻塞等待协程1发送信号模拟接力。
  6. 协程将通道收到的值自增并写入通道模拟接力给第2棒。
  7. 协程2重复4-6步骤,直到协程4关闭通道。模拟接力完成。

示例代码如下,整体流程和上述差不多,唯一需要补充的就是增加了WaitGroup控制主线程的结束。

var wg sync.WaitGroup

// 基于无缓冲通道模拟接力赛跑
func main() {

	wg.Add(1)

	baton := make(chan int)

	//启动协程1模拟第一个起跑运动员
	go Runner(baton)

	fmt.Println("接力赛开始.......")
	//向通道发送数据,模拟开枪通知第1棒起跑
	baton <- 1

	//等待第4个协程wg.Done()
	wg.Wait()
	//关闭通道
	close(baton)
	fmt.Println("接力赛结束")

}

func Runner(baton chan int) {

	var newRunner int
	//等待接力棒
	runner := <-baton

	fmt.Println("选手", runner, "接到第", runner, "棒")

	if runner != 4 {
		newRunner = runner + 1
		fmt.Println("选手", runner, "准备将接力棒交给选手", newRunner)
		go Runner(baton)
	}

	time.Sleep(100 * time.Millisecond)

	if runner == 4 {
		fmt.Println("第", runner, "选手到达终点")
		wg.Done()
		return
	}

	fmt.Println("选手", runner, "将接力棒交给选手", newRunner)
	baton <- newRunner
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

输出结果如下,可以看到无缓冲区通道顺序的完成了接力。

接力赛开始.......
选手 1 接到第 1 棒
选手 1 准备将接力棒交给选手 2
选手 1 将接力棒交给选手 2
选手 2 接到第 2 棒
选手 2 准备将接力棒交给选手 3
选手 2 将接力棒交给选手 3
选手 3 接到第 3 棒
选手 3 准备将接力棒交给选手 4
选手 3 将接力棒交给选手 4
选手 4 接到第 4 棒
第 4 选手到达终点
接力赛结束

1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 有缓冲通道示例

有缓冲区通道则允许其中一方未准备好,亦或者允许发送方填满缓冲区时阻塞发送方,缓冲区无数据时阻塞接收方,是典型的生产者消费者模式。

对此我们模拟一个单上传者多消费者的例子,整体流程为:

  1. 主线程创建有缓冲区通道。
  2. 创建4个协程阻塞监听有缓冲通道,等待缓冲通道有值。
  3. 主线程往通道写入10条数据。
  4. 任何一个协程收到数据时,尝试和通道建立连接,从中获取一条数据并消费,其他通道阻塞等待。
  5. 所有数据都消费干净,完毕通道,所有协程结束工作。

最后我们给出的代码如下:

const (
	numberGoroutines = 4  //协程数设置为4
	taskLoad         = 10 //任务数为10
)

var wg sync.WaitGroup

func main() {

	wg.Add(4)

	//创建一个有缓冲通道
	taskChannel := make(chan string, taskLoad)

	//启动4个协程模拟消费者
	for i := 0; i < numberGoroutines; i++ {
		go Worker(i, taskChannel)
	}

	//主线程往通道里提交10个任务
	for i := 0; i < taskLoad; i++ {
		taskChannel <- fmt.Sprintf("task %d", i)
	}

	//关闭通道
	close(taskChannel)

	//等待4个协程执行完成
	wg.Wait()

}

func Worker(workNo int, taskChannel chan string) {
	//函数退出时扣减计数器
	defer wg.Done()

	for {
		task, ok := <-taskChannel

		//如果通道关闭则退出
		if !ok {
			fmt.Println("任务通道已关闭,worker", workNo, "退出")
			return
		}

		//输出通道收到的值,然后休眠2s
		fmt.Println("worker", workNo, "执行任务", task)

		time.Sleep(2000 * time.Millisecond)
	}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

输出结果如下,可以看到生产者和消费者有序调度:

worker 1 执行任务 task 1
worker 0 执行任务 task 0
worker 3 执行任务 task 2
worker 2 执行任务 task 3
worker 2 执行任务 task 4
worker 1 执行任务 task 5
worker 0 执行任务 task 6
worker 3 执行任务 task 7
worker 3 执行任务 task 8
worker 0 执行任务 task 9    
任务通道已关闭,worker 1 退出
任务通道已关闭,worker 2 退出
任务通道已关闭,worker 0 退出
任务通道已关闭,worker 3 退出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 小结

本文从一个Java开发视角简单的介绍了协程的基本概念及其优势和使用场景,可以发现对于高并发的IO密集任务,使用轻量级线程goroutine开销远远小于线程,于此同时我们也需要结合场景选用合适的工具确保协程安全。 对于简单的数值增减操作,我们建议使用原子函数确保安全读和安全写。而对于操作复杂的操作时,我们建议对操作代码块使用互斥锁保证协程安全。 而对于那些协程间共享数据交换,如果需要实时建立连接完成交换的,我们建议使用无缓冲通道。如果要考虑执行效率,我们建议结合场景创建尽可能多的协程并使用有缓冲区通道完成通信。

我是 sharkchili ,CSDN Java 领域博客专家,开源项目—JavaGuide contributor,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili 。 因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

# 参考

Go语言实战:https://book.douban.com/subject/27015617/ (opens new window)

计算机底层的秘密:https://book.douban.com/subject/36370606/ (opens new window)

编辑 (opens new window)
上次更新: 2026/03/26, 01:05:31
聊聊go语言channel中的一些小技巧
来聊聊我用go手写redis这件事

← 聊聊go语言channel中的一些小技巧 来聊聊我用go手写redis这件事→

最近更新
01
基于EasyExcel实现高效导出
03-25
02
从开源框架中学习那些实用的位运算技巧
03-25
03
浅谈分布式架构设计思想和常见优化手段
03-25
更多文章>
Theme by Vdoing | Copyright © 2025-2026 Evan Xu | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×
×