网站首页 > java教程 正文
关于协程
Go 语言现在很火,其中一个很重要的原因就是 Go 语言优越的性能表现,而这个优势与 Go 实现的轻量级线程 Goroutines(协程 Coroutine)不无关系。
了解协程和线程的区别之前,我们现需要了解下底层实现线程几种方式。
线程实现模型
相对于协程,你可能对进程和线程更为熟悉。进程一般代表一个应用服务,在一个应用服务中可以创建多个线程,实现线程主要有以下三种方式
1:1 线程模型
在 Linux 操作系统编程中,都是通过 fork() 函数创建一个子进程来代表一个内核中的线程。一个进程调用 fork() 函数后,系统会先给新的进程分配资源,例如,存储数据和代码的空间。然后把原来进程的所有值都复制到新的进程中,只有少数值与原来进程的值(比如 PID)不同,这相当于复制了一个主进程。
采用 fork() 创建子进程的方式来实现并行运行,会产生大量冗余数据,即占用大量内存空间,又消耗大量 CPU 时间用来初始化内存空间以及复制数据。
这时候轻量级进程(Light Weight Process,即 LWP)出现了。相对于 fork() 系统调用创建的线程来说,LWP 使用 clone() 系统调用创建线程,该函数是将部分父进程的资源的数据结构进行复制,复制内容可选,且没有被复制的资源可以通过指针共享给子进程。因此,轻量级进程的运行单元更小,运行速度更快。LWP 是跟内核线程一对一映射的,每个 LWP 都是由一个内核线程支持。
N:1 线程模型
1:1 线程模型由于跟内核是一对一映射,所以在线程创建、切换上都存在用户态和内核态的切换,性能开销比较大。除此之外,它还存在局限性,主要就是指系统的资源有限,不能支持创建大量的 LWP。
N:1 线程模型就可以很好地解决 1:1 线程模型的这两个问题。该线程模型是在用户空间完成了线程的创建、同步、销毁和调度,已经不需要内核的帮助了,也就是说在线程创建、同步、销毁的过程中不会产生用户态和内核态的空间切换,因此线程的操作非常快速且低消耗。
N:M 线程模型
N:1 线程模型的缺点在于操作系统不能感知用户态的线程,因此容易造成某一个线程进行系统调用内核线程时被阻塞,从而导致整个进程被阻塞。
N:M 线程模型是基于上述两种线程模型实现的一种混合线程管理模型,即支持用户态线程通过 LWP 与内核线程连接,用户态的线程数量和内核态的 LWP 数量是 N:M 的映射关系。
协程是什么?
JDK 1.8 Thread.java 中 Thread#start 方法的实现,实际上是通过 Native 调用 start0 方法实现的;在 Linux 下, JVM Thread 的实现是基于 pthread_create 实现的,而 pthread_create 实际上是调用了 clone() 完成系统调用创建线程的。
所以,目前 Java 在 Linux 操作系统下采用的是用户线程加轻量级线程,一个用户线程映射到一个内核线程,即 1:1 线程模型。由于线程是通过内核调度,从一个线程切换到另一个线程就涉及到了上下文切换。
Java 在 Linux 操作系统下采用的是用户线程加轻量级线程,一个用户线程映射到一个内核线程,即 1:1 线程模型。由于线程是通过内核调度,从一个线程切换到另一个线程就涉及到了上下文切换。
而 Go 语言是使用了 N:M 线程模型实现了自己的调度器,它在 N 个内核线程上多路复用(或调度)M 个协程,协程的上下文切换是在用户态由协程调度器完成的,因此不需要陷入内核,相比之下,这个代价就很小了。
协程的实现原理
而协程与进程、线程的概念不一样,我们可以将协程看作是一个类函数或者一块函数中的代码,我们可以在一个主线程里面轻松创建多个协程。
程序调用协程与调用函数不一样的是,协程可以通过暂停或者阻塞的方式将协程的执行挂起,而其它协程可以继续执行。这里的挂起只是在程序中(用户态)的挂起,同时将代码执行权转让给其它协程使用,待获取执行权的协程执行完成之后,将从挂起点唤醒挂起的协程。 协程的挂起和唤醒是通过一个调度器来完成的。
基于 N:M 线程模型实现的协程
假设程序中默认创建两个线程为协程使用,在主线程中创建协程 ABCD…,分别存储在就绪队列中,调度器首先会分配一个工作线程 A 执行协程 A,另外一个工作线程 B 执行协程 B,其它创建的协程将会放在队列中进行排队等待。
当协程 A 调用暂停方法或被阻塞时,协程 A 会进入到挂起队列,调度器会调用等待队列中的其它协程抢占线程 A 执行。当协程 A 被唤醒时,它需要重新进入到就绪队列中,通过调度器抢占线程,如果抢占成功,就继续执行协程 A,失败则继续等待抢占线程。
相比线程,协程少了由于同步资源竞争带来的 CPU 上下文切换,I/O 密集型的应用比较适合使用,特别是在网络请求中,有较多的时间在等待后端响应,协程可以保证线程不会阻塞在等待网络响应中,充分利用了多核多线程的能力。而对于 CPU 密集型的应用,由于在多数情况下 CPU 都比较繁忙,协程的优势就不是特别明显了。
在Java中使用Kilim 协程框架
目前 Java 原生语言暂时还不支持协程。不过我们可以通过协程框架在 Java 中使用协程。目前 Kilim 协程框架在 Java 中应用得比较多,通过这个框架,我们可以方便的在Java 中使用协程了。
在 Java 中引入 Kilim ,需要引入 jar 包
Kilim 框架包含了四个核心组件,分别为:任务载体(Task)、任务上下文(Fiber)、任务调度器(Scheduler)以及通信载体(Mailbox)。
Task 对象主要用来执行业务逻辑,我们可以把这个比作多线程的 Thread,与 Thread 类似,Task 中也有一个 run 方法,不过在 Task 中方法名为 execute,我们可以将协程里面要做的业务逻辑操作写在 execute 方法中。
与 Thread 实现的线程一样,Task 实现的协程也有状态,包括:Ready、Running、Pausing、Paused 以及 Done 总共五种。Task 对象被创建后,处于 Ready 状态,在调用 execute() 方法后,协程处于 Running 状态,在运行期间,协程可以被暂停,暂停中的状态为 Pausing,暂停后的状态为 Paused,暂停后的协程可以被再次唤醒。协程正常结束后的状态为 Done。
Fiber 对象与 Java 的线程栈类似,主要用来维护 Task 的执行堆栈,Fiber 是实现 N:M 线程映射的关键。
Scheduler 是 Kilim 实现协程的核心调度器,Scheduler 负责分派 Task 给指定的工作者线程 WorkerThread 执行,工作者线程 WorkerThread 默认初始化个数为机器的 CPU 个数。
Mailbox 对象类似一个邮箱,协程之间可以依靠邮箱来进行通信和数据共享。协程与线程最大的不同就是,线程是通过共享内存来实现数据共享,而协程是使用了通信的方式来实现了数据共享,主要就是为了避免内存共享数据而带来的线程安全问题。
协程与线程的代码案例
我们通过一个简单的生产者和消费者的案例,来对比下协程和线程的性能。
在这个案例中,我创建了 1000 个生产者和 1000 个消费者,每个生产者生产 10 个产品,1000 个消费者同时消费产品。
线程实现代码如下:
携程程实现代码如下:
线程执行效果如下:
协程执行效果如下执行:
通过上述性能对比,我们可以发现:在有严重阻塞的场景下,协程的性能更胜一筹。在I/O 阻塞型场景就是协程在 Java 中的主要应用。
总结
协程和线程密切相关,协程可以认为是运行在线程上的代码块,协程提供的挂起操作会使协程暂停执行,而不会导致线程阻塞。协程又是一种轻量级资源,即使创建了上千个协程,对于系统来说也不是很大的负担,但如果在程序中创建上千个线程,那系统压力就很大。所以说协程的设计方式极大地提高了线程的使用率。
Java未来的三个主要项目之一Loom项目 引入了被称为 fibers 的新型轻量级用户线程,甲骨文公司 Loom 项目技术负责人 Ron Pressler 在 QCon 伦敦 2019 大会上指出:“利用 fibers,如果我们确保其轻量化程度高于内核提供的线程,那么问题就得到了解决。大家将能够尽可能多地使用这些用户模式下的轻量级线程,且基本不会出现任何阻塞问题”。
有没有具体的优化场景呢?在socket通信中,可以使用协程优化IO读写这块。例如,可以尝试用协程写个非阻塞Socket通信,或用协程简单改造下Netty。
同时小编整理了一套“金三银四”的面试题资料
想要的小伙伴都可以转发+私信小编 免费领取!
“金三银四”程序员不得不刷的四大系列面试题
- 上一篇: 爱奇艺网络协程编写高并发应用实践
- 下一篇: 「协程」ViewModelScope源码解析
猜你喜欢
- 2024-11-23 阿里P7大牛论:Kotlin协程优雅的与Retrofit缠绵
- 2024-11-23 Android技术分享|利用Kotlin协程,多任务并行,测试RTM SDK性能
- 2024-11-23 Go 的调度器是怎么实现协程中断的?远离手撕源码用 GIF 动图告诉你
- 2024-11-23 「协程」ViewModelScope源码解析
- 2024-11-23 爱奇艺网络协程编写高并发应用实践
- 2024-11-23 如何用 Netty 写一个高性能的分布式服务框架?
- 2024-11-23 进程、线程、线程池和协程如何理解?
- 2024-11-23 直击灵魂!美团大牛手撸并发原理笔记,由浅入深剖析JDK源码
- 2024-11-23 Kotlin 协程一
- 2024-11-23 Kotlin 协程与架构组件一起使用及底层原理分析
你 发表评论:
欢迎- 最近发表
-
- 五,网络安全IDA Pro反汇编工具初识及逆向工程解密实战
- 「JAVA8」- Lambda 表达式(java lambda表达式原理)
- 深入探讨Java代码保护:虚拟机保护技术的新时代
- Nginx反向代理原理详解(图文全面总结)
- 逆向拆解日本IT,哪些Java技术栈薪资溢价高
- mybatis 逆向工程使用姿势不对,把表清空了,心里慌的一比
- Spring Boot集成ProGuard轻松实现Java 代码混淆, Java 应用固若金汤
- 从 Java 代码逆向工程生成 UML 类图和序列图
- 人与人相处:尊重是标配,靠谱是高配,厚道是顶配
- Windows系统安装日期如何修改(windows10怎么修改安装日期)
- 标签列表
-
- java反编译工具 (77)
- java反射 (57)
- java接口 (61)
- java随机数 (63)
- java7下载 (59)
- java数据结构 (61)
- java 三目运算符 (65)
- java对象转map (63)
- Java继承 (69)
- java字符串替换 (60)
- 快速排序java (59)
- java并发编程 (58)
- java api文档 (60)
- centos安装java (57)
- java调用webservice接口 (61)
- java深拷贝 (61)
- 工厂模式java (59)
- java代理模式 (59)
- java.lang (57)
- java连接mysql数据库 (67)
- java重载 (68)
- java 循环语句 (66)
- java反序列化 (58)
- java时间函数 (60)
- java是值传递还是引用传递 (62)
本文暂时没有评论,来添加一个吧(●'◡'●)