ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

一个进程到底能创建多少线程?深度解析线程数上限与内存限制

一个进程到底能创建多少线程?深度解析线程数上限与内存限制 你有没有认真想过一个进程到底能创建多少个线程我记得刚接触并发编程那会儿总以为线程数量是某种固定的、写在系统手册里的数字比如1024或者2048。直到有一次做高并发网关压测压测脚本一跑服务端的线程数蹭蹭往上窜然后突然报错进程直接崩溃。翻日志才看到unable to create new native thread一行红字。当时心里就一个念头线程数量原来不是我自己说了算的。后来做了一些深入排查才慢慢弄明白进程能创建的线程数量不是一道简单的算术题而是内存布局、内核参数、栈空间分配、系统全局资源几方面合力的结果。搞清楚这套机制不仅对写高并发服务有帮助对排查线上问题、做容量评估同样关键。这篇文章我会从原理讲到实测再讲常见误区和排查思路尽量一次讲透。1. 先从基本概念说起进程和线程到底差在哪1.1 进程资源分配的最小单位大多数人对进程的理解是一个跑起来的程序比如打开一个浏览器就是一个进程启动一个 Java 服务也是一个进程。这个理解没有错但从操作系统内核的角度看进程的核心身份是资源容器它有独立的地址空间、独立的文件描述符表、独立的信号处理方式还有自己的页表和内存映射。进程之间彼此隔离A 进程崩溃了正常情况下不会把 B 进程带走。这种隔离是操作系统安全性和稳定性的基础。但也正因为隔离进程之间的数据交换成本很高需要通过管道、消息队列、共享内存等 IPC 机制才能完成。网上搜进程通信 ipc能查到一大堆方案每一种都有各自的适用场景和性能特性。1.2 线程调度执行的最小单元线程则是进程内部的执行流。同一个进程里的线程共享进程的地址空间和大部分资源但它们各自拥有独立的栈、寄存器上下文和程序计数器。正因为共享地址空间线程之间的数据交换非常方便——一个全局变量所有线程都能读能写。也正因为共享才出现了线程安全这个老生常谈的问题。我们常说的线程互斥、线程死锁、C# 线程安全 List、Java 线程安全本质上都是在处理同一件事多个执行流同时访问共享数据时如何保证数据一致性和执行顺序合理。这个问题跑不掉只要写了多线程程序就必须面对。1.3 两者的关系不是包含是多个执行流共用一个资源包进程和线程的关系我经常用一个比喻来理解进程就像一家公司注册了办公地址、租了工位、开通了水电网络线程就是公司里的员工所有员工共享这间办公室的资源但各自有自己的工作台和任务列表。公司可以只有一个人干活也可以雇几百个人同时干活——雇多少员工取决于办公场地能坐下多少人也取决于公司的钱能发多少工资。放到操作系统里办公场地就是进程的虚拟地址空间工资就是线程栈占用的内存。所以一个进程能创建多少线程首先要看它的办公场地能容纳多少线程栈。这不是工程师拍脑袋定的而是由系统配置和硬件条件决定的。2. 制约线程数量的几个硬指标2.1 虚拟内存最核心的瓶颈在 64 位 Linux 系统上每个进程默认拥有非常大的虚拟地址空间通常达到 128 TB 甚至更多理论上看线程栈的虚拟内存似乎不是问题。但现实是系统普遍会对进程的虚拟内存做限制常用的限制手段是ulimit -v有些服务出于稳定性考虑也会在配置里写入RLIMIT_AS。更关键的是物理内存。虽然虚拟内存可以很大但线程栈一旦被实际使用就会逐渐映射到物理内存。按默认的 8 MB 线程栈计算创建 1000 个线程光栈区域就可能实际占用数 GB 的物理内存——还没有算线程运行时需要的堆内存和其他资源。物理内存一旦吃紧操作系统只能靠 swap 或者 OOM 机制来处理而这两条路都会让服务性能急剧恶化甚至进程被杀掉。2.2 线程栈大小决定单个线程的入职成本每个线程都有自己的栈空间用于存放局部变量、函数调用帧和返回地址。这个栈大小在 Linux glibc 里默认是 8 MBpthread 创建线程时可以手动指定。8 MB 的栈听着不大算一算如果进程可用的实际内存是 4 GB理论上最多容纳 512 个线程的栈还没算代码段、数据段和堆内存的占用。所以单线程栈大小直接影响线程总预算。我把这个比喻成员工工位大小——工位越大能招的人越少。实际开发中如果确认线程不会做深层递归、不会在栈上放大数组就可以把栈调小一些比如 1 MB 甚至 512 KB。这样在保证安全的前提下能显著提高线程容量。网上有人发帖问freertos 中检查线程中内存使用大小的接口本质也是想搞清楚每个线程栈的实际水位避免溢出或浪费。2.3 系统全局限制内核级的天花板除了进程内部的地址空间和物理内存内核还有几个全局参数从操作系统层面限定线程总数上限。这里列几个关键项参数路径或命令默认值作用threads-max/proc/sys/kernel/threads-max取决于内存通常数千到数万系统全局线程总数上限pid_max/proc/sys/kernel/pid_max32768 或更大PID 编号上限间接约束线程总数max_map_count/proc/sys/vm/max_map_count65530进程可拥有的内存映射区域数量RLIMIT_NPROCulimit -u视系统而定单个用户可创建的进程/线程总数这些参数平时不会有人注意到但当线程数量逼近上限时任何一个都可能是压死骆驼的最后一根稻草。我在排查无法创建新线程这类问题时第一步就是先同时检查这几个数值缺一不可。2.4 其他隐藏限制cgroup 和容器环境如果你在 Docker 容器或者 Kubernetes Pod 里运行服务那情况就更加复杂了。容器化环境会通过 cgroup 限制 CPU 和内存当内存达到 cgroup 限额时即使宿主机还有大把空闲内存进程也会因为memory cgroup limit失败线程创建随之失败。这类问题最坑的地方在于在宿主机上明明可以跑进了容器就报错。排查时需要同时看/sys/fs/cgroup/memory/memory.limit_in_bytes和进程实际内存占用对照着分析。我有一个排查习惯在容器里先跑cat /proc/self/cgroup看看归属再用cat /sys/fs/cgroup/memory/memory.max查上限这样能快速确定是不是容器限制了线程创建。3. 动手实测一个进程到底能创建多少线程3.1 先看测试环境纸上谈兵没有意义我用自己的开发机做了一次实测。硬件配置8 核 CPU、16 GB 物理内存、系统为 Ubuntu 22.04内核 5.15通过ulimit -s查看线程栈大小默认为 8192 KB也就是 8 MB。为了避免把测试机搞挂我一开始设置了保守的循环上限比如先创建 2000 个线程观察一下再逐步增加。3.2 写一个线程风暴测试程序这里用 C 语言配合 pthread 来测因为这是最贴近操作系统底层的方式能直接验证系统能力不受虚拟机运行时的影响。#include pthread.h #include stdio.h #include stdlib.h #include unistd.h #include errno.h void *task(void *arg) { // 让线程挂起不要立刻退出方便统计并发数量 pause(); return NULL; } int main(int argc, char *argv[]) { long max_threads 5000; if (argc 1) { max_threads atol(argv[1]); } pthread_t *tids malloc(sizeof(pthread_t) * max_threads); if (!tids) { perror(malloc); exit(1); } int created 0; for (long i 0; i max_threads; i) { int rc pthread_create(tids[i], NULL, task, NULL); if (rc ! 0) { printf(创建第 %d 个线程时失败错误码: %d\n, created 1, rc); printf(失败原因: %s\n, strerror(rc)); break; } created; } printf(成功创建线程数: %d\n, created); printf(按回车退出...\n); getchar(); return 0; }编译命令很简单gcc -o thread_test thread_test.c -pthread3.3 实测过程和关键数据第一次运行我直接传了 20000想着看看极限在哪。程序启动后可以看到线程数一路飙升但到了大概 18000 左右系统开始出现明显的卡顿。top里看到每个线程的 CPU 占用虽然不高但系统整体负载上去了内存也在快速下降。最终程序在 18124 个线程时报错退出错误码是 11也就是EAGAIN表示系统资源不足以创建新线程。这个数字远超我预期因为按 8 MB 栈算18000 个线程栈就需要 144 GB 虚拟内存而我物理内存只有 16 GB——之所以能撑到这么多是因为大部分线程栈只是分配了虚拟地址空间并没有真正映射到物理内存。也就是说线程创建时的内存瓶颈通常先碰到的是虚拟内存限制或max_map_count限制而不是物理内存。为了确认这个推断我把/proc/sys/vm/max_map_count临时调大注意这需要 root 权限而且不建议在生产环境随意调整sysctl -w vm.max_map_count262144重新跑了一次这次线程创建数提升到了 32760 左右卡在了pid_max附近。由此基本可以确认在默认配置下max_map_count和pid_max是线程数量的两道主要闸门内存本身反而是其次。3.4 用 Java 再测一次很多后端服务跑在 JVM 上Java 的线程模型是基于操作系统的原生线程也就是 1:1 映射。所以在 Linux 上Java 能创建的线程数同样受上面那些系统参数约束JVM 本身并没有硬编码的线程上限。区别在于JVM 启动时会占一块比较大的内存哪怕只是启动一个什么都不做的 Spring Boot 应用堆内存可能就占几百 MB。另外 HotSpot JVM 默认的线程栈大小是 1 MB通过-Xss调整比 glibc 默认的 8 MB 小很多这意味着如果只算栈内存Java 进程的线程容量反而可能更大。我用一个简单的 Java 程序测了一下public class ThreadLimitTest { public static void main(String[] args) { int count 0; try { while (true) { Thread t new Thread(() - { try { Thread.sleep(Long.MAX_VALUE); } catch (InterruptedException e) { // ignore } }); t.start(); count; if (count % 1000 0) { System.out.println(已创建 count 个线程); } } } catch (Throwable e) { System.out.println(创建到 count 个线程时失败: e.getMessage()); e.printStackTrace(); } } }运行结果在我的环境上默认参数下 Java 进程大约能创建 25000 多个线程后报OutOfMemoryError: unable to create new native thread。这个数字受堆内存大小、Xss参数和系统限制影响比较大。如果我用-Xss256k减小线程栈同时调小堆内存线程数量能突破 40000 甚至更高。值得提醒一句这种压到极限的测试一定要在隔离环境里做。我在测试过程中有几次把 CPU 跑满了整个桌面几乎无响应最后只能强制重启。想复现的朋友建议使用虚拟机或者云主机别拿主力开发机硬试。4. 影响线程数量的因素一层层拆开看4.1ulimit系列限制绕不开的长辈在 Linux 上ulimit是一组用户级资源限制直接影响线程创建。与线程最相关的是-s栈大小和-u用户最大进程数。ulimit -a输出里会看到max user processes (-u) 63228 stack size (kbytes, -s) 8192 virtual memory (kbytes, -v) unlimitedmax user processes控制的是同一个用户能拥有的进程和线程总数。如果在机器上还跑着很多其他进程这个配额会被瓜分。有些环境里这个值是 1024意味着一个用户所有进程加起来只能有 1024 个线程这种情况下别说创建几千个线程了几百个都费劲。遇到这种情况可以临时提高ulimit -u 65535但要注意这个命令只对当前 shell 及其子进程生效。如果你是通过 systemd 管理的服务需要在 service 文件里配置LimitNPROC65535然后systemctl daemon-reload再重启服务。很多运维问题的根源就是我在命令行试了没问题但服务里就是不行其实区别就在于运行时资源的限制来源不同。4.2 线程栈大小省钱还是烧钱就看这里线程栈大小对线程数量的影响前面已经说了我再展开一下。在 C/C 里pthread 创建线程时可以指定栈大小pthread_attr_t attr; pthread_attr_init(attr); pthread_attr_setstacksize(attr, 1024 * 1024); // 1MB pthread_create(tid, attr, task, NULL);在 Java 里通过-Xss设置java -Xss512k -jar myapp.jar调小线程栈的风险在于如果线程内有深度递归或者大量栈上分配会导致StackOverflowError或段错误。所以调栈大小要结合代码实际情况别为了追求线程数量盲目压缩。我一般先观察线上线程的栈使用深度用jstack或 gdb 看一下典型线程的栈帧大小再决定能不能压缩。4.3 内核参数max_map_count与pid_maxmax_map_count默认值是 65530每个线程在创建时通常需要映射栈区域这就会增加一个 map 计数。当进程创建的线程多到一定程度映射区域数量就会触顶。这个参数可以通过下面的方式查看和修改# 查看当前值 cat /proc/sys/vm/max_map_count # 临时修改重启后失效 sysctl -w vm.max_map_count262144 # 永久修改 echo vm.max_map_count 262144 /etc/sysctl.conf sysctl -ppid_max控制 PID 编号最大值因为线程也被视作可调度实体每个线程都要分配一个内核 task_struct 和一个 PID 编号。默认 32768 意味着整个系统最多同时存在 32768 个 PID包括进程和线程。在高并发场景下只靠调max_map_count还不够要把pid_max也调大。提示修改内核参数要评估对系统的影响不要在生产环境轻率调整尤其是老内核版本。建议先在压测环境验证效果再考虑是否应用到生产。4.4 物理内存和 swap终极硬约束不管前面的虚拟内存和内核参数怎么调物理内存始终是终极约束。每个线程运行时栈会逐渐被实际使用如果有大量局部变量、函数调用深度大栈帧会增长对应的物理内存占用就会上升。还有一个常被忽视的点线程不仅仅是栈需要内存线程相关的内核栈kernel stack也要占内存通常每个线程对应 16 KB 左右的内核栈虽然不大但当线程数量上万时这部分内存也会成为不可忽略的消耗。内核栈分配失败同样会导致 pthread_create 返回错误。4.5 在 32 位 vs 64 位系统上的差异32 位系统的进程地址空间只有 4 GB其中用户空间通常只有 2 GB 或 3 GB。这种情况下地址空间本身就是巨大的瓶颈即使物理内存很大也无法创建太多线程。32 位系统下创建 2000 个线程都不是容易的事。现在主流服务器都是 64 位系统这个问题基本消失但如果你的程序运行在 32 位嵌入式环境或者老系统上对线程数量的预期要大幅调低。网上关于freertos 线程内存接口嵌入式多线程设计的讨论本质上也是受限于小内存环境才需要精确控制线程数。5. 线程多并不代表性能好隐性成本与反直觉现象5.1 上下文切换线程多的头号代价CPU 核心数是有限的假设机器是 8 核而你创建了 1000 个线程那么同一时刻最多只有 8 个线程在真正执行其他 992 个都在排队等待。操作系统负责调度这个过程叫上下文切换。上下文切换的成本包括保存当前线程的寄存器状态、加载新线程的状态、刷新 TLB 等。频繁切换会使 CPU 大量时间花在调度上而不是执行业务逻辑上。我用 perf 工具测过线程数量从 100 增加到 1000CPU 调度开销占比可能从个位数百分比飙升到 30% 甚至更高。这不是线性的增长而是指数级的恶化。这就是为什么多线程不等于高性能。更合理的方案是让线程数约等于 CPU 核心数或者核心数加一让每个线程都持续干活减少无意义的调度。5.2 锁竞争与线程饥饿线程多了共享资源的竞争必然加剧。比如一个服务里有 8 个线程时锁竞争很轻微增加到 80 个线程后大家抢同一把锁等待时间变长线程 A 等锁、线程 B 等锁、线程 C 也等锁CPU 时间全耗在锁等待和上下文切换上。这时候线程数量不但没带来吞吐量提升反而让响应时间变长。反直觉的现象来了在某些场景下减少线程数反而能提升 QPS。这就是我在实践中的切身体会。曾经把一个服务的线程池从 200 调到 50接口的 p99 时延反而下降了一半吞吐量还略有提升。原因就是锁竞争和上下文切换开销大幅下降。5.3 内存不断的增加OOM 是怎么来的线程创建本身需要内存栈需要内存内核栈也需要内存线程池如果无限制膨胀最终可能把堆内存挤爆。JVM 进程的堆外内存direct memory、线程栈、metaspace常常是 OOM 的隐性问题来源。举个例子一个服务用了默认线程池maxPoolSize设成非常大比如 5000程序在某个瞬间收到大量任务线程池创建了几千个线程每个线程栈按 1 MB 算就是几个 GB 的内存。如果堆内存又设得很大进程总体内存占用超过容器的 memory limit轻则触发频繁 GC重则被 OOM Killer 直接杀掉。这种事故在线上非常常见而且难以复现因为只有高流量峰值才有触发条件。6. 实操指南如何合理配置和调整线程数量6.1 先看清当前系统的家底排查问题、做容量规划之前先快速梳理一下环境的限制。我通常会依次执行这几条命令# 查看当前线程栈大小 ulimit -s # 查看用户级进程/线程数限制 ulimit -u # 查看系统全局线程上限 cat /proc/sys/kernel/threads-max # 查看 PID 上限 cat /proc/sys/kernel/pid_max # 查看内存映射数量上限 cat /proc/sys/vm/max_map_count # 查看当前系统总线程数 ps -eLf | wc -l # 查看某个进程当前线程数 ps -T -p PID | wc -l把这些数字收集起来对照上面的分析基本能判断出瓶颈在哪一层。比如ps -eLf | wc -l已经接近threads-max那就说明整个系统的线程接近上限要考虑扩容机器或者清理异常线程。6.2 线上应用建议不要用裸线程要用线程池很多人写代码时图省事需要并发就 new 一个线程需求结束后线程自然销毁。这种模式在低并发场景下可以但一旦请求量上来频繁创建和销毁线程会浪费大量资源线程数量也难以控制。线程池的价值在于复用线程并且限制并发数量。线程池的参数设计是一个经典话题网上搜线程池配置线程池的阻塞队列选择能看到很多讨论。核心参数就那么几个核心线程数core pool size最大线程数max pool size阻塞队列容量work queue capacity拒绝策略rejection policy线程空闲回收时间keep alive time参数怎么配取决于场景CPU 密集型任务线程数差不多等于 CPU 核数避免线程切换浪费IO 密集型任务线程数可以比核数多一些比如核数乘以 2或者按照 CPU 核数 / (1 - 阻塞系数)来估算。阻塞系数越高线程可以越多。我自己的经验不管公式怎么算都要加上限保护防止流量尖峰把线程池打爆。宁可拒绝一部分请求也不能让线程无限制增长把进程拖垮。6.3 C 线程池注意什么C 从 C11 开始有了std::thread配合std::async、std::future可以方便地处理异步任务。但std::thread的创建开销相对较大而且如果每来一个任务就创建一个线程性能会受到明显影响。所以 C 项目里一般也是自己封装线程池或者使用第三方库。C 自建线程池时要特别注意任务队列的线程安全性。多线程同时往队列里push任务、工作线程从队列里pop任务这里必须加锁否则会产生数据竞争。网上经常有人问C 单例模式线程安全其实线程池内部的任务队列通常会做成单例模式如果单例初始化本身不是线程安全的比如双重检查锁忘了加volatile就会出现偶发崩溃。6.4 Java 线程池的坑Java 的ThreadPoolExecutor用得好不好直接决定服务稳定性。我总结几个高频坑核心线程预热的问题。默认情况下ThreadPoolExecutor是懒创建线程的任务提交后才慢慢创建线程直到核心线程数。如果需要快速响应可以在初始化后调用prestartAllCoreThreads()。队列选择的问题。LinkedBlockingQueue无界队列虽然简单但在任务量爆发时队列会无限增长导致内存耗尽。网上常讨论线程池的阻塞队列选择我倾向于用有界队列再配合合理的拒绝策略比如CallerRunsPolicy让提交任务的线程自己执行反而是一种天然的背压保护。DiscardPolicy需要谨慎使用。这个策略静默丢弃任务业务上如果丢失的是订单消息、支付回调后果不堪设想。要么用DiscardOldestPolicy丢弃最老任务适合允许丢数据的场景要么干脆用AbortPolicy把异常打印出来引起注意。7. 常见问题排查速查表症状可能原因排查方向unable to create new native thread系统线程上限/虚拟内存不足检查threads-max、pid_max、ulimit -u、cgroup 限制创建线程到一定数量后进程退出栈内存不足或max_map_count到顶查看ulimit -s增大max_map_count调小线程栈容器里线程数比宿主机少很多cgroup 内存/CPU 限制查看容器 memory 限制和 cpu 限制Java 程序启动不久就 OOMXss过大或堆内存 线程栈超限调小-Xss调小堆内存检查线程是否泄漏进程 CPU 很高但吞吐量很低线程数过多上下文切换频繁用pidstat -w看上下文切换增加 CPU 或减少线程使用线程池但任务堆积核心线程数过小或者队列过小监控队列积压量适当调整核心线程数和队列容量线程数很多但 CPU 占用异常低线程大量阻塞在锁或者 IO 上抓线程 dump分析锁竞争和等待点还有两个高频问题我再单独点一下。一个是占用了端口无法结束怎么办。很多时候不是端口被某个进程占用而是进程有一堆线程每根线程占用一个 socket 连接看起来像端口被占死了。排查时可以用lsof -i :8080看到所有占用该端口的进程和线程句柄再用netstat -tlnp定位进程 PID。如果确认要强杀kill -9 PID是最后的办法但先要考虑这个进程是否有未持久化的数据。另一个是我看到有程序 PID 但看不到进程名。在有些系统上ps aux会截断进程名或者进程名显示成[]内核线程。如果怀疑是自己服务的线程用top -H -p PID或者ps -T -p PID就能把该进程下的所有线程列出来。我之前排查一个 Tomcat 实例显示线程数异常多就是用这个办法一眼看到了大量http-nio线程确认是连接数超过预期导致线程池自动扩容了。8. 在实际项目中我建议这样规划线程资源8.1 先做容量评估再写代码每接到一个需要多线程的服务我先在文档里回答三个问题业务场景是 CPU 密集还是 IO 密集期望并发量是多少单线程处理一个请求的内存开销大概是多少三个问题回答完线程数量级基本可以确定下来。举个例子一个文件解析服务每个文件处理需要 200 ms其中 80% 时间在等待磁盘 IO。假设目标 TPS 是 500那理论上需要多少线程粗略估算500 TPS × 0.2 秒/任务 100 个并发任务考虑到 IO 等待占比很高线程数可以压在 100 到 150 之间。同时要留有余量应对突刺流量一般再加 30% 的缓冲最终线程池最大线程数设成 200 比较合理。8.2 监控线程数量别等出事了才看线程数不是配好就完事的要持续监控。我习惯在监控面板上放四个指标当前线程数java.lang.Thread或从/proc读取线程池队列积压量线程池拒绝任务数进程上下文切换速率如果线程数到了一定水位还没下降说明代码里可能有线程泄漏——每次请求都创建一个线程但没正确结束或者线程池里的线程因为阻塞 IO 无法释放。这类问题靠日志通常很难发现但监控面板上一眼就能看出来。8.3 线程安全优先不要用加了锁当万能药实现线程安全有很多种手段互斥锁、读写锁、原子变量、无锁队列、线程本地存储ThreadLocal。每种手段的性能特点不同实际中要按场景选择。比如高频读低频写的场景读写锁或者CopyOnWriteArrayList都比粗暴加synchronized要好。但也要警惕过度使用无锁方案。无锁编程lock-free的调试难度远高于加锁一个 ABA 问题就能让人头秃好几天。我的原则是默认用最简单的互斥锁只有测试确认锁竞争是瓶颈时才考虑读写锁或无锁方案。没到那个点不折腾。9. 一些你可能会踩的坑9.1 线程栈调太小导致的崩溃前面提到调小线程栈能提高线程容量但如果栈调得过小比如低于 256 KB碰到稍微深一点的调用链就容易炸。我有个同事为了把线程数顶上去把-Xss调成 128 KB结果服务一上线就频繁StackOverflowError查了半天才发现是 JSON 序列化库内部递归比较深128 KB 不够用。安全起见Java 服务至少-Xss512k起步C 尽量保持默认。9.2 把最大线程数当成最佳线程数很多框架的线程池参数默认值写得很大比如某些 HTTP 服务器默认最大线程数 10000。但这只是允许的最大值不代表业务真的需要这么多。如果你的服务 QPS 只有 200核心线程数设 200、最大线程数设 400 就够了设成 10000 反而会在流量波动时瞬间创建大量线程把内存打满。9.3 线程泄露和连接池的纠缠有一种场景特别隐蔽数据库连接池的连接数是固定的但每来一个请求就新建一个线程线程从连接池拿连接连接用完后没有归还。随着线程数增长连接池被耗尽请求全部卡在等待连接线程越积越多最终 OOM。这个坑的本质是线程数量和连接池容量不匹配。排查的时候别只盯线程数还要同时看连接池的活跃连接数和等待线程数。9.4 Windows 和 Linux 的差异Windows 上的线程模型和 Linux 有些不同Windows 的线程栈默认是 1 MB而且 Windows 的线程创建 API 失败时不会返回 Linux 那样的EAGAIN而是直接抛异常或返回NULL句柄。在 Windows 上测试同样的并发程序线程数量上限和报错形态可能完全不同。如果团队同时维护两套环境建议在目标环境上分别做测试别把一套环境的数据硬套到另一套上。写在最后回到最初的问题一个进程到底能创建多少个线程这个问题没有标准答案。它取决于你的栈大小、物理内存、内核参数、容器限制甚至取决于你正在跑的业务代码是否存在线程泄漏。在默认的 Linux 环境上几百个线程是常态几千个不稀奇但到了上万就可能触碰系统天花板。而我们真正应该关心的不是能创建多少而是创建多少才是对的。我个人的体会是线程数量的真正上限不应该由系统强制报错来告诉你而应该在系统报错之前由工程师通过合理的线程池配置和容量规划主动控制住。高并发不是靠无限堆线程堆出来的而是靠理解系统资源、控制并发水位、做好监控告警一层层打磨出来的。希望这篇文章能把线程数量的底层逻辑讲透让你下次再看到相关的报错时不再是一脸懵而是能快速定位到具体是哪个参数、哪一层限制出了问题。
返回列表