自在学

我们与你共同进步

  • 分类课程
  • 文章
  • 工作台
  • 订阅

  • 关于我们
  • 隐私政策
  • 使用条款

探索

  • 分类课程
  • 文章
  • 工作台
  • 订阅

网站信息

  • 关于我们
  • 隐私政策
  • 使用条款

加入社区

自在学学习社区微信二维码

微信扫码,交流学习

株洲市自在学教育科技有限公司© 2025 - 2026 版权所有

© 2025 - 2026 株洲市自在学教育科技有限公司 版权所有

湘公网安备43020302000292号|湘ICP备2025148919号-1
分类课程工作台文章订阅
分类课程工作台文章价格

操作系统

  1. 01操作系统到底在忙什么
  2. 02操作系统结构:从一次点击到整台机器
  3. 03进程:让许多程序有秩序地共享一台机器
  4. 04线程与并发:一座进程里的多条执行路线
  5. 05CPU 调度:让有限核心接住不断到来的工作
  6. 06进程同步:让并发的结果仍然可信
  7. 07死锁:当所有人都在等别人先松手
  8. 08主内存:给每个进程一张“独占内存”的地图
  9. 09虚拟内存:让有限的物理内存撑起更大的运行空间
  10. 10存储:把一次“保存”拆开来看
  11. 11I/O 系统:让快慢悬殊的世界可靠协作
  12. 12初识文件系统:从一个文件名走到磁盘数据
  13. 13文件系统的实现:一个文件到底怎样落到存储设备上
  14. 14一条路径背后的系统工程:文件系统内部机制
  15. 15系统安全:让计算机在不可信环境中保持可信
  16. 16虚拟机:让多个操作系统安全地共享一台计算机
  17. 17网络与分布式系统:当“远处”变成一次系统调用
正在加载课程章节内容
课程编程操作系统线程与并发:一座进程里的多条执行路线

线程与并发:一座进程里的多条执行路线

你在浏览器里点开一个很大的网页时,页面往往还能滚动,下载进度也在变化,地址栏照样能接受输入。看上去,浏览器像是在同一时刻做很多件事。可如果负责界面、网络、图片解码和脚本执行的工作全挤在一条执行路线里,只要其中一次磁盘读取或图片解码停得久一点,整个窗口就会像被冻住一样。

线程要解决的正是这个矛盾:一个应用需要共享同一批数据和资源,又希望不同工作能各自向前推进。线程把进程内部的一条执行路线拆成多条,每条路线都有自己的执行现场,但仍住在同一个进程里。这样做比创建多个彼此隔离的进程轻便,也让交换数据变得直接;代价是隔离变弱了,一个线程写错共享内存,可能把整个进程一起拖垮。

这一章不急着背接口。我们先弄清线程到底独有和共享什么,再看并发如何落到多核硬件上,最后把线程池、Fork-Join、取消、信号和线程本地存储串成一套完整的机制。


一条线程到底带着什么

把进程想成一间工作室。工作室里有公共图纸、材料柜和已经打开的设备,所有工人都能使用;每名工人则有自己的任务编号、做到哪一步的记录和一张私人工作台。线程就是工人,进程提供那间共享的工作室。

操作系统要暂停一条线程、改跑另一条线程,至少得知道它下次从哪条指令继续,还要恢复通用寄存器、栈指针等执行现场。因此,一条线程通常有自己的线程标识、程序计数器、寄存器状态和栈。栈里放着这一条调用链的局部变量、返回地址和函数参数。两个线程即使执行同一个函数,也会沿各自的栈进入和返回。

同一进程内的线程通常共享代码、全局数据、堆、打开的文件描述符以及其他进程级资源。共享让线程之间传递一个对象引用就能合作,不必先序列化再跨进程发送;但共享也意味着它们看到的是同一份可变状态。两个线程同时修改一个计数器时,读、改、写三个动作可能彼此穿插,最后得到一个谁也没预料到的结果。共享本身不是同步,正确性仍要靠锁、原子操作、不可变数据或消息队列来建立。

线程的独有状态与进程共享资源

线程崩溃通常不是“只坏掉这一条线程”。如果它发生非法内存访问、破坏了堆结构,或者触发了进程级终止条件,同一进程里的其他线程也可能一起结束。线程换来了低成本共享,却没有进程那样强的故障隔离。

为什么线程通常比进程轻

创建新进程时,系统要建立独立的地址空间和一组进程级管理信息。创建同一进程里的线程,不需要再造一份完整地址空间,线程切换也通常不必更换整个进程的内存映射。于是,线程的创建、销毁和切换往往更便宜。

这里的“更便宜”不等于“没有成本”。每条线程仍需要栈和内核管理数据;切换时要保存与恢复执行现场,还可能打乱处理器缓存。线程数量远超处理器核心数后,系统会花更多时间在调度和切换上,内存也会被大量线程栈吃掉。为了每个短任务临时创建一条线程,往往比任务本身还贵,这正是线程池出现的原因。

响应性、吞吐量与并行能力

多线程常见的收益可以拆成三个不同目标。界面程序把耗时工作移出事件线程,是为了响应性;服务器让多个请求交错等待网络和磁盘,是为了吞吐量;计算程序把独立数据块交给不同核心,是为了并行加速。它们都用了线程,但优化目标不一样。

如果任务主要在等待 I/O,一条线程阻塞时让另一条线程运行,单核机器也能受益。如果任务一直占用 CPU,多条线程只有落在多个核心上才可能缩短总计算时间。把“用了多线程”直接等同于“程序更快”,会把这两类情况混在一起。


并发不等于并行

并发关心的是,多项任务能否在同一段时间内都取得进展;并行关心的是,某个瞬间是否真的有多项任务同时执行。单核处理器可以在几条线程之间快速切换,让下载、界面和脚本都不至于一直饿着,这叫并发,但任一瞬间仍只有一条指令流在核心上运行。多核处理器则可能把几条可运行线程放到不同核心上,形成真正的并行。

单核并发与多核并行的区别

下面的演示把四项任务放到一个或四个核心上。逐步推进时,观察“本步同时执行的任务数”:单核模式会轮流前进,多核模式才可能在同一步推进多项任务。

把问题拆成可以同时做的部分

多核不会自动理解业务。程序要先找出可以独立推进的工作,再决定怎样分配。常见的拆法有两种。

数据并行把一大批数据切成若干块,让不同核心执行同一种操作。给一百万张像素做相同滤镜、把大数组分段求和,都属于这一类。它的难点通常在切分是否均匀、数据是否连续,以及最后怎样合并局部结果。

任务并行让不同核心承担不同工作。例如浏览器的一条线程处理网络响应,另一条线程解码图片,渲染线程准备画面。任务之间可能有先后依赖:图片没下载完就不能完整解码,布局没算好也不能最终绘制。此时,多开线程并不能消除依赖,只是让没有依赖的部分尽量重叠。

实际程序常把两者混合。视频编辑器可以按帧做数据并行,也可以让读取、解码、特效和写出形成任务流水线。设计时真正要问的是:哪些数据会被共同修改,哪些步骤必须等前一步,最慢的阶段在哪里。

阿姆达尔定律:核心再多,也绕不过串行部分

假设一段程序中必须串行执行的比例是 SSS,其余部分可以平均分到 NNN 个核心上。理想加速比的上界是:

Speedup⁡(N)≤1S+1−SN\operatorname{Speedup}(N) \leq \frac{1}{S + \frac{1-S}{N}}Speedup(N)≤S+N1−S​1​

如果有 25% 的工作必须串行,四核时的理想加速比只有约 2.29 倍。即使核心数量趋近无穷大,上界也只是 4 倍。这个公式刻意忽略了线程创建、同步、通信和负载不均等额外成本,所以真实结果通常更低。

它提醒我们,优化并行程序时不能只盯着核心数量。减少串行瓶颈、让任务大小合适、降低共享数据争用,往往比继续加线程更有效。任务切得太粗,部分核心会早早空闲;切得太细,调度开销又会淹没计算收益。

多核程序最难的地方

第一道难题是依赖。两个任务都要写同一份数据时,就不能假装它们彼此独立。第二道难题是负载均衡:四块数据大小相同,不代表计算量相同。第三道难题是局部性:线程在不同核心间迁移,或者反复访问远处内存,会让缓存优势消失。

还有一项更隐蔽的成本是可重复性。线程调度顺序每次都可能不同,竞态条件也许运行几千次才出现一次。日志本身会改变时间关系,调试器暂停某条线程后,错误甚至会暂时消失。并发程序需要把不变量、同步边界和失败路径写得比单线程程序更清楚。


用户线程怎样落到内核线程

程序员说的“线程”不一定就是内核直接调度的对象。语言运行时可以在用户空间保存许多任务的状态,内核则只看见较少的可调度线程。用户线程与内核线程怎样对应,会决定阻塞行为、并行能力和管理成本。

用户线程与内核线程的三种映射模型

多对一、一对一与多对多

模型映射方式解决了什么主要代价
多对一多个用户线程共用一个内核线程用户空间切换快,可以容纳很多执行单元一次阻塞系统调用可能卡住整组线程,也不能在多个核心上同时执行
一对一每个用户线程对应一个内核线程一个线程阻塞时其他线程仍可运行,也能直接利用多核每条线程都要消耗内核资源,大量线程会增加内存和调度压力
多对多大量用户线程复用一组内核线程用户级任务数量与实际并行度解耦运行时必须处理映射、阻塞、唤醒和调度,设计复杂

现代桌面和服务器操作系统通常为原生线程提供一对一的内核支持,Pthreads、Windows 线程以及常见 JVM 平台线程最终都会落到操作系统线程上。与此同时,协程、纤程、虚拟线程和任务调度器又在应用层采用类似多对多的思路:程序可以创建大量轻量任务,运行时只用有限的内核线程承载它们。

这两层并不矛盾。内核负责把内核线程放到 CPU 核心上,运行时负责把更轻的任务放到内核线程上。真正棘手的是阻塞:如果运行时不知道某个任务即将做阻塞调用,它可能连承载这个任务的内核线程也一起卡住。成熟运行时会用异步 I/O、阻塞补偿或专用线程池处理这种情况。

轻量级进程与调度器激活

较早的多对多实现常在用户线程与内核线程之间放一层轻量级进程,可以把它理解为运行时可使用的“虚拟处理器”。用户线程库把任务调度到这些虚拟处理器上,内核再把对应的执行实体调度到物理核心。

调度器激活试图解决内核与用户线程库彼此看不见的问题:内核发现某条执行路径即将阻塞或已经唤醒时,通过上行通知告诉用户空间线程库;线程库据此保存状态、改派其他用户线程,或请求新的执行承载。它能让多对多映射更聪明,但内核与运行时的接口十分复杂,工程上并没有成为今天通用操作系统的主流方案。

理解这段历史仍有价值。今天的任务运行时虽然接口不同,仍在处理同一组矛盾:任务数量可以很大,内核线程不能无限增加;阻塞不能拖住整个运行时;调度器还要尽量让各核心有活可做。


从创建线程到等待结果

线程库通常提供四类基本能力:创建执行路线、传入参数、等待完成、取得结果或结束状态。不同平台的名字不同,生命周期却很相似。

Pthreads:创建之后别忘了回收

Pthreads 是 POSIX 线程接口。下面的例子让工作线程计算从 1 到给定上界的和,主线程用 pthread_join() 等待它结束,并取得返回值。

c
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
 
static void *sum_to(void *arg) {
    int upper = *(int *)arg;
    long long *result = malloc(sizeof(*result));
    if (result == NULL) return NULL;
 
    *result = 0;
    for (int i = 1; i <= upper; ++i) {
        *result += i;
    }
    return result;
}
 
int main(void) {
    pthread_t worker;
    int upper = 100;
 
    if (pthread_create(&worker, NULL, sum_to, &upper) != 0) {
        return 1;
    }
 
    void *raw = NULL;
    if (pthread_join(worker, &raw) != 0 || raw == NULL) {
        return 1;
    }
 
    long long *answer = raw;
    printf("结果:%lld\n", *answer);
    free(answer);
    return 0;
}

pthread_create() 成功返回,只说明线程已经进入可调度状态,并不保证它已经执行到哪一步。pthread_join() 建立了明确的等待关系:调用者在目标线程终止前不能继续越过这个点。可连接线程如果结束后一直没人 join,其部分系统资源不能及时回收;若调用者确实永远不需要等待结果,可以把线程设为分离状态。

参数指针的生命周期也很重要。示例里的 upper 在主线程栈上,但主线程会等待工作线程完成,因此指针一直有效。如果创建线程后立刻从当前函数返回,工作线程再解引用这个地址就可能读到失效内存。线程共享地址空间让传参很方便,也要求我们对对象寿命负责。

Windows 与 Java:接口不同,问题相同

Windows 原生接口使用线程句柄创建、等待和关闭内核线程。句柄是内核对象的引用,等待线程结束后仍要关闭句柄,否则会泄漏系统资源。生产代码通常还会优先考虑更高层的线程池或任务接口,而不是为每项短工作直接调用底层创建函数。

Java 把“要做的任务”和“在哪条线程上做”分开表达。Runnable 只执行动作,没有返回值;Callable<V> 可以返回结果或抛出异常;提交任务后得到的 Future<V> 代表尚未完成的结果。

java
import java.util.concurrent.*;
 
public class SumDemo {
    public static void main(String[] args) throws Exception {
        ExecutorService pool = Executors.newFixedThreadPool(2);
 
        Callable<Long> task = () -> {
            long sum = 0;
            for (long i = 1; i <= 100; i++) sum += i;
            return sum;
        };
 
        Future<Long> future = pool.submit(task);
        System.out.println("结果:" + future.get());
        pool.shutdown();
    }
}

submit() 把任务交给执行器,调用者可以继续做别的事;future.get() 才会在结果未完成时等待。这里的同步点不是“创建任务”而是“读取结果”。把等待放得太早,会把本可并行的程序重新写成串行;完全不等待,又可能让后续代码在结果尚未准备好时使用它。

调用 Java 线程对象的 run() 只是一次普通方法调用,代码仍在当前线程执行;调用 start() 才会启动新的执行路线。这个区别很小,却是初学线程时最常见的误判之一。

异步启动与汇合

父线程创建工作线程后立即继续,属于异步启动。服务器接到请求后把任务交给后台工作者,接收线程马上回来接下一个请求,就是典型例子。

父线程在某个阶段必须等待所有子任务完成,再合并结果,这个等待点叫汇合。Pthreads 的 pthread_join()、Java 的 Future.get() 或并行框架里的 join 都在表达同一个依赖:后面的步骤缺少前面结果就不能继续。好的并行结构会把汇合放在真正需要结果的地方,而不是每创建一个任务就立刻等待。


把线程交给运行时管理

如果服务器每收到一个请求就创建一条线程,请求突增时,线程数会跟着无边界上涨。最后系统可能还没处理业务,就把内存耗在线程栈上,把 CPU 耗在上下文切换上。任务抽象要解决的是:业务提交“要做什么”,运行时决定用哪条工作线程、什么时候做,以及系统满载时怎么办。

线程池不是一袋无限线程

一个可控线程池至少有三个边界:工作线程数决定同时执行多少任务,任务队列容量决定能暂存多少等待工作,拒绝或回压策略决定两者都满时怎样处理新任务。只配置线程数而留下无限队列,压力只是从线程转移到了内存,延迟仍可能越积越长。

线程池、有界队列与回压机制

CPU 密集任务通常从接近可用核心数的工作线程开始调优,因为再多线程只会争抢 CPU。I/O 密集任务在等待期间不占核心,可以容纳更多并发,但上限仍要结合连接数、内存、下游容量和超时来定。线程池大小不是一个脱离场景的固定公式。

下面的实验允许你调整工作线程数和队列容量。连续点击“突发 6 个任务”,观察满载时新任务被拒绝,以及扩大队列为什么只会暂时吸收突发,并不能提高处理速度。

Fork-Join 与工作窃取

有些计算天然适合递归分解。求大数组之和时,可以把范围一分为二,子任务继续拆,直到每块足够小就顺序计算,再逐层合并。Fork 表示产生可并行的子任务,Join 表示等待并组合结果。

真正让它保持忙碌的是工作窃取。每条工作线程维护自己的待办队列;某条线程先做完时,会从其他线程的队列中拿走尚未执行的任务。它不保证每个任务一定并行,只是在可用工作线程范围内尽量平衡负载。

分治、工作窃取与结果合并

java
class SumTask extends RecursiveTask<Long> {
    private static final int THRESHOLD = 10_000;
    private final long[] values;
    private final int from, to;
 
    SumTask(long[] values, int from, int to) {
        this.values = values;
        this.from = from;
        this.to = to;
    }
 
    @Override
    protected Long compute() {
        if (to - from <= THRESHOLD) {
            long sum = 0;
            for (int i = from; i < to; i++) sum += values[i];
            return sum;
        }
 
        int mid = (from + to) >>> 1;
        SumTask right = new SumTask(values, mid, to);
        right.fork();
        long leftResult = new SumTask(values, from, mid).compute();
        return leftResult + right.join();
    }
}

阈值决定任务粒度。阈值太大,并行机会不足;太小,任务对象、队列操作和合并成本会变得突出。Fork-Join 更适合计算密集、能递归拆分且子任务相对独立的工作。让大量任务长期阻塞在网络或磁盘上,会占住工作线程,削弱窃取调度的效果。

OpenMP:告诉编译器哪里可以并行

OpenMP 为 C、C++ 和 Fortran 提供指令与运行时接口。开发者标出并行区域或可拆分循环,运行时建立线程团队并分配迭代。例如:

c
#pragma omp parallel for
for (int i = 0; i < n; ++i) {
    output[i] = transform(input[i]);
}

这段写法成立的前提是各次迭代之间没有未处理的数据依赖。OpenMP 不会替程序员证明共享访问安全,也不会自动发现所有竞态或死锁。它降低的是线程管理的样板代码,不是并发正确性的难度。

GCD:提交任务,而不是认领线程

Grand Central Dispatch 使用调度队列接收工作。串行队列一次只执行一个任务,很适合保护某份状态;并发队列允许多项任务重叠执行,具体使用多少线程由系统根据负载决定。主队列是绑定主线程的串行队列,界面更新通常回到这里完成。

swift
let queue = DispatchQueue.global(qos: .userInitiated)
 
queue.async {
    let result = computeReport()
    DispatchQueue.main.async {
        show(result)
    }
}

async 提交后立即返回,sync 则等待任务完成。在主线程上对主队列做同步提交会形成自我等待:主线程等队列任务结束,而队列任务又只能由正在等待的主线程执行,于是死锁。队列抽象让顺序关系更清楚,却不能消除错误的依赖环。

oneTBB:用算法模板表达并行

oneTBB 面向 C++ 提供并行循环、任务组和并发容器。开发者描述迭代范围和操作,任务调度器把计算拆成任务,再映射到工作线程上。它同样会根据可用工作线程调整实际并行度,所以“可以并行”不是“保证同时运行”。

这类运行时的共同思路很一致:任务可以很多,工作线程必须有限;任务说明逻辑,运行时负责复用线程与平衡负载。它们减少手工管理线程的负担,但程序员仍要给出正确的依赖、共享边界和取消方式。


多线程程序里的特殊边界

线程共享进程资源,所以一些在单线程程序里很直接的操作,到了多线程环境就会出现“到底影响哪一条线程”的问题。fork()、信号、取消和线程本地存储尤其容易被误解。

多线程进程调用 fork 与 exec

POSIX 多线程进程调用 fork() 后,子进程只复制调用 fork() 的那条线程。地址空间看似来自父进程,其他线程却不会出现在子进程里。这会留下危险状态:父进程里某把互斥锁可能正被另一条线程持有,子进程复制到了“已加锁”的内存,却没有复制能够解锁它的线程。

因此,多线程程序通常让子进程在 fork() 后尽快调用 exec(),用新程序映像替换整个进程。在 fork() 与 exec() 之间只能非常谨慎地调用允许的安全操作。pthread_atfork() 可以在复制前后安排锁处理,但它很难覆盖大型程序和第三方库里的全部内部锁。

任何线程成功调用 exec() 后,当前进程的程序映像都会被新程序替换,原来的其他线程也不再存在。exec() 不是“只替换调用线程”,因为线程共享的地址空间已经整体换掉了。

信号到底交给谁

同步信号由当前执行动作直接触发,例如某条线程进行了非法内存访问,它自然与触发线程相关。异步信号来自外部事件,例如终端中断或其他进程发送通知,目标通常先描述为进程,再由系统结合各线程的信号屏蔽字选择可接收线程。

多线程程序常在工作线程里屏蔽一组异步信号,再安排专门线程同步等待并处理它们,把难以控制的异步入口变成普通控制流。pthread_kill() 可以把信号定向到同一进程中的指定线程,但信号的处理动作仍可能具有进程级效果。信号处理函数里可安全调用的函数非常有限,不能把普通业务逻辑随意搬进去。

取消不是强行拔电源

用户点下“停止下载”时,我们希望工作线程尽快结束,但它可能正持有锁、写一半文件或修改共享结构。如果在任意指令位置把线程强行终止,资源与不变量都会停在半完成状态。

更稳妥的方式是协作式取消:发起者只设置取消请求,目标线程在定义好的安全点检查请求,先释放资源、恢复共享状态,再退出。Pthreads 默认使用延迟取消,目标线程在取消点响应请求,并可执行清理处理程序;Java 的 interrupt() 设置中断状态或让可中断阻塞操作抛出异常,也需要任务代码正确传播和处理。

c
static void *worker(void *unused) {
    for (;;) {
        do_one_small_piece();
        pthread_testcancel();
    }
}

取消点要布置在“工作已经推进一小步,但共享状态仍完整”的位置。循环完全不检查,请求就迟迟不能生效;检查得过密,又会增加分支和清理设计的负担。超时、取消标志和资源释放应当作为同一条失败路径一起设计。

线程本地存储:每条线程一份上下文

线程本地存储让同一个键在不同线程上得到不同值。日志请求编号、解析器状态或不适合共享的缓存,都可以放在线程本地空间。它和普通局部变量不同:局部变量跟随一次函数调用,线程本地值可以跨越这条线程上的多层函数调用;它也和全局变量不同:每条线程看到的是自己的副本。

协作式取消与线程本地存储

线程池让这里多了一层陷阱。工作线程会被下一个请求复用,如果任务结束时没有清理线程本地值,下一个请求可能读到上一个请求留下的身份或追踪编号。线程本地存储提供的是访问隔离,不会自动替你管理业务生命周期。最稳妥的做法是在 finally 或等价清理路径中移除上下文。

下面的实验有两个页签。取消实验展示“立即终止”为什么会留下锁;上下文实验则让同一工作线程连续处理两个请求,比较是否清理线程本地值的结果。


把整套机制连起来

现在回到开头的浏览器。界面线程要保持响应,网络和解码工作要能在后台推进,这是并发;解码任务落到多个核心同时计算,才是并行。线程共享浏览器进程的堆和文件,因此传递页面数据很方便,也必须控制写入顺序。任务多到一定程度后,运行时用有界队列和工作线程限制资源;任务可以递归拆分时,工作窃取帮助各核心分担负载;用户取消页面加载时,工作任务在安全点清理连接和缓冲区再退出。

这套设计没有免费午餐。线程比进程轻,却牺牲隔离;共享数据传递快,却带来竞态;并行能缩短计算,却受串行部分、同步和调度开销限制;线程池复用资源,却必须处理队列满载与上下文残留。真正可靠的并发程序,不是“尽可能多地开线程”,而是把任务、资源边界、依赖和失败路径一起设计。

快速检查

1
同一进程中的两条线程,通常各自独有的是什么?
2
四项任务在单核上轮流前进,这最准确地描述了什么?
3
一个有边界的线程池在满载时,哪些设计能阻止资源无限增长?
4
POSIX 多线程进程调用 fork() 后,子进程会自动复制父进程中的全部线程。
5
向线程发送取消请求,就等于该线程已经完成清理并终止。

思考题

一台四核服务器处理两类工作:一类是持续计算图片滤镜,另一类是等待远程接口返回。它们应该使用同样大小的线程池吗?

不应该直接套用同一个大小。滤镜任务持续占用 CPU,工作线程接近可用核心数时通常已经能把计算资源用满,继续增加只会加重切换与缓存竞争。远程接口任务大部分时间在等待,允许更多并发有机会提高吞吐量,但上限还要受连接数、内存、对方服务容量和超时约束。两类工作最好使用分开的资源池和队列,避免大量等待任务堵住计算任务,也避免计算任务占满本该处理 I/O 完成事件的工作线程。

为什么说“把共享变量换成线程本地变量”不能自动修复所有并发问题?

线程本地存储只让某个值按线程隔离。如果业务本来需要多条线程共同更新同一份结果,改成每线程一份后还需要正确合并;如果线程池复用工作线程,旧任务的本地值还可能泄漏给新任务;文件、数据库连接和外部系统等共享资源也不会因为某个变量放进线程本地空间就自动安全。它是管理上下文的一种工具,不是同步机制的替代品。

上一章进程:让许多程序有秩序地共享一台机器下一章CPU 调度:让有限核心接住不断到来的工作