线程,协程对比

本文探讨了Java不采用协程的原因,包括内存节省、线程开销、线程切换效率、与NIO结合的优势以及Java生态的灵活性。尽管协程在某些场景中高效,但Java依靠线程池和多技术组合同样能解决问题,并指出Java生态丰富的工具弥补了协程的缺失。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

协程相对线程的好处

为什么Java不换成协程呢

  1. 先返回到问题的本源。当我们希望引入协程,我们想解决什么问题。我想不外乎下面几点:
  • 节省资源,轻量,具体就是: 节省内存,每个线程需要分配一段栈内存,以及内核里的一些资源
  • 节省分配线程的开销(创建和销毁线程要各做一次syscall)
  • 节省大量线程切换带来的开销
  • 与NIO配合实现非阻塞的编程,提高系统的吞吐
  • 使用起来更加舒服顺畅(async+await,跑起来是异步的,但写起来感觉上是同步的)

线程+其他技术综合也能实现协程达到的效果

  • 节省资源:比如IM服务器,需要同时处理大量空闲的链接(可能要几十万,上百万)。这时候用connection per thread就很不划算了。但是可以直接改用netty去处理这类问题。你可以理解为NIO + woker thread大致就是一套“协程”,只不过没有实现在语法层面,写起来不优雅而已。问题是,你的场景真的处理了并发几十万,上百万的连接吗?
  • 再说创建/销毁线程的开销。这个问题在Java里通过线程池得到了很好的解决。你会发现即便你用vert.x或者kotlin的协程,归根到底也是要靠线程池工作的。goroutine相当于设置一个全局的“线程池”,GOMAXPROCS就是线程池的最大数量;而Java可以自由设置多个不同的线程池(比如处理请求一套,异步任务另外一套等)。kotlin利用这个机制来构建多个不同的协程scope。这看起来似乎会更灵活一点。
  • 然后是线程的切换开销。线程的切换实际上只会发生在那些“活跃”的线程上。对于类似于Web的场景,大量的线程实际上因为IO(发请求/读DB)而挂起,根本不会参与OS的线程切换。现实当中一个最大200线程的服务器可能同一时刻的“活跃线程”总数只有数十而已。其开销没有想象的那么大。为了避免过大的线程切换开销,真正要防范的是同时有大量“活跃线程”。
  • 此外说说与NIO的配合。在Java这个生态里Java NIO/Netty/Vert.X/rxJava/Akka可以任意选择。一般来讲,Netty可以解决绝大部分因为IO的等待造成资源浪费的问题。Vert.X/rxJava。可以让程序写的更加“优雅”一点(见仁见智)。Akka就是Java世界里对“原教旨OO“的实现,很有特色。的确,用NIO + completedFuture/handler/lambda不如async+await写起来舒服,但起码是可以干活的。
  • 觉得线程耗资源,可以控制线程总数,可以减少线程stack的大小,可以用线程池配置max和min idle等等。想要go的channel,可以上disruptor。可以说,Java这个生态里尽管没有“协程”这个第一级别的概念,但是要解决问题的工具并不缺

换成线程底层机制需要改变

  • 如果真的要较真Java的NIO用于业务的问题,其核心痛点应该是JDBC。这是个诞生了几十年的,必须使用Blocking IO的DB交互协议。其上承载了Java庞大的生态和业务逻辑。Java要改自己的编程方式,必须得重新设计和实现JDBC
  • 反过来,如果java社区全力推进这个事情,Java历史上的生态的积累却因为协程的出现而进行大换血。想像一下如果没有thread,也没有ThreadLocal,@Transactional不起作用了,又没有等价的工具,是不是很郁闷?
  • 其他新的语言历史包袱少,比较容易重新思考“什么是现代的multi-task编程的方式“这个大主题。

多线程容易出问题的地方使用协程也不能改变

  • 多线程容易出bug主要因为:
    “抢占“式的线程切换 —— 你无法确定两个线程访问数据的顺序,一切都很随机
    “同步“不可组装 —— 同步的代码组装起来也不同步,必须加个更大的同步块
  • 协程也是抢占的和顺序不一定
  • 解决:
  • 主要是对可变状态的访问更新
  • 读取和写入操作限制在一个线程里,使用队列基于生产者消费者模型进行操作
### 进程、线程协程的区别与原理 #### 1. **进程 (Process)** 进程是操作系统资源分配的基本单位,每个进程都有独立的地址空间系统资源。当一个程序被执行时,会启动一个新的进程来运行该程序。 - 每个进程拥有自己的虚拟内存空间 `struct mm_struct` 唯一标识符 `pid_t pid`[^4]。 - 创建新进程通常通过调用 `fork()` 函数实现,子进程继承父进程的状态并获得新的 PID。 - 进程之间的通信需要借助 IPC(Inter-Process Communication),如管道、消息队列等机制。 #### 2. **线程 (Thread)** 线程是进程中可调度的最小单元,同一进程中的多个线程共享相同的内存空间其他资源。相比进程,线程轻量化,切换成本低。 - 线程间的切换由 CPU 控制,属于被动行为;其状态变化依赖于操作系统的调度策略[^1]。 - 同一进程内的线程可以直接访问彼此的数据结构,因此无需额外的通信开销。 - 多线程编程可能带来并发问题,例如竞态条件死锁,需引入同步工具(如互斥锁、信号量)解决这些问题。 #### 3. **协程 (Coroutine)** 协程是一种用户级别的轻量级线程,允许在一个单一的操作系统线程内实现多任务协作式调度。 - 协程的核心特点是主动控制权转移,即程序员可以通过显式的指令决定何时暂停或恢复某个任务。 - Python 中使用 `async/await` 实现异步编程模型,其中 `await` 表达式用于挂起当前协程直至其他协程完成工作[^3]。 - 不同于线程协程不涉及底层硬件资源的竞争,也不需要频繁地上下文切换,从而降低了性能损耗[^2]。 --- ### 对比总结 | 特性 | 进程 | 线程 | 协程 | |--------------|-------------------------------|------------------------------|-----------------------------| | 地址空间 | 独立 | 共享同一个进程 | 共享同一个线程 | | 切换方式 | 高代价 OS 调度 | 较低代价 OS 调度 | 极低成本 用户级别调度 | | 并发能力 | 自然隔离 | 高效利用 CPU | 完全无竞争 提高吞吐量 | | 编程复杂度 | 易管理但消耗大 | 需要小心处理同步问题 | 最简单但受限于单一线程 | --- ### 示例代码:Python 中的协程 以下是基于 Python 的协程示例,展示如何定义执行简单的异步函数: ```python import asyncio async def say_hello(): print("Hello") await asyncio.sleep(1) # 模拟耗时操作 print("World") async def main(): task1 = asyncio.create_task(say_hello()) task2 = asyncio.create_task(say_hello()) await task1 await task2 if __name__ == "__main__": asyncio.run(main()) ``` 上述代码展示了两个协程实例化并通过事件循环依次运行的过程。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值