Date: 2025-06-01


并行的限制

程序中无法并行的部分,决定了整体性能的上限。

Amdahl 定律

Image

一个程序中,假设其中 Lock / Unlock 是串行(不可并行),只占 30% 时间;

  • 你把其余的 70% 并行了,最多也只能加速约 1 / (0.3 + 0.7/N);

  • 即使 N=100,也不是 100 倍加速,可能只有 3 倍左右。

  • 任务中无法并行的部分就是瓶颈,再多线程、再多 CPU 也没用;

  • 可并行度越小,增加资源的收益越差

虽然Amdahl 定律 展现了一个令人失望的结果! 但我们仍然需要乐观

乐观视角

Gustafson 定律

Amdahl 假设任务总量是固定的,这在现实中不常见,反过来看:既然加速比有瓶颈,那我们不如做更多的任务!

核心思想: 并行不是为了加速固定的任务,而是为了让我们能处理更大的问题规模、更复杂的数据量。

Image

你银行系统如果每天只能处理 1 万笔转账(串行),用了并发系统之后,不是把 1 万笔处理更快 —— 而是你能处理 100 万笔!

并发和并行

Rob Pike说过的一句经典的话

“Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once.”

并发: 你能同时接收很多个转账请求,每个请求你都安排好流程、排队、处理顺序;

并行: 你有多个处理器,多个请求可以真的在同一时刻一起处理(比如一个核心处理用户 A 的转账,另一个处理用户 B 的转账)。

并发是编程模型,Go 语言提供了 goroutine + channel,容易表达并发逻辑;是否能并行,是操作系统 + CPU 的事。

进程和线程

为什么说没有线程的进程是不存在的?

现代操作系统的进程实现方式。从技术角度看,进程至少包含一个执行线程(即主线程)

进程类比为: 拥有者资源 线程类比为: 执行资源的人

线程(工人) 进程(建筑)

所以说 即使你拥有资源但是无法执行 也就相当于没有拥有!

谁又去管理 线程和进程呢?

也就是操作系统,操作系统_使用PCB和TCB_去管理

操作系统用PCB记录进程的资源(内存、文件等)。

用TCB记录线程的执行状态(寄存器、栈等)。

PCB必须关联至少一个TCB,否则进程无法被调度执行。

线程的优缺点

优点:

  1. 相比于进程,拥有更少的内存开销

  2. 更少的通信开销:因为 线程间使用相同的地址空间 直接读写其进程的共享地址空间 一个线程所做 立刻对所有线程可见

缺点:

  1. 需要同步: 因为操作系统对于进程的隔离做的非常好,通常 一个进程的崩溃 不会影响其他进程 但是对于线程来说 一个线程很可能会受到影响

问题:

为什么一个进程崩溃 常常不会影响另一个进程 (如何两个进程需要通信呢?) 一个线程的崩溃常常会影响所有线程使用的相同共享资源?

即使两个进程通信(如管道、socket、共享内存):

进程 A 崩溃不会导致进程 B 崩溃,最多导致 B 的通信读/写出错(比如管道断了、socket 断开)。

OS 保证隔离,通信机制只是连接而非绑定生死。

而:

所有线程共享同一个进程的 地址空间、堆、全局变量、文件描述符等资源。

一个线程写坏共享数据、释放了不该释放的资源、非法访问内存,会破坏整个进程的状态。

OS 管线程是以“进程整体”看待的,某线程崩溃时,整个进程可能被 OS 杀掉(例如段错误信号)。

进程间通信

通信是并发系统的核心: 我们应该保证进程间通信的高效执行,这样才能享受并发带来的性能提升

“进程内线程A 与 进程内线程B 的通信,只不过它们各自受进程隔离保护” 这是形象的理解进程间通信的本质

IPC

一旦决定应用程序从IPC中收益,必须决定在系统上使用哪一种可用的IPC方法!

**NOTE: ** 无论是进程还是线程 都可以将他们视为线程 每个进程都至少有一个线程 实际上通信只发生在线程之间

共享内存IPC

最简单的方式: 共享内存, 一个或者多个任务 通过公共内存进行通信, 这些内存在他们的虚拟地址空间中出现,它们就好像在阅读自己的内存空间,这样的更改通常会直接反应在其它进程或线程中,无需于操作系统交互

实例:

  1. 创建共享空间
  2. 生产者向共享空间写入数据
  3. 消费者向共享空间读取数据

优点:

提供了最快且资源消耗最少的通信方式(操作系统参与分配共享内存 但 并不参与任务间通信)实现了更高的速度 和 更少的数据

缺点:

  1. 不一定是最安全的, 操作系统不会为共享内存提供接口和保护,如果两个人一起访问了同一块空间呢?(保护共享内存重新设计代码)

  2. 仅用于本地机器 无法拓展多台机器, 在分布式系统中会出现问题: 多个数据无法容纳在一台机器上

消息传递IPC

(通常由操作系统支持) 每个任务由唯一名称标识, 任务通过向命名任务发送和接收消息进行交互 操作系统建立通信通道, 适当提供系统调用

优点:

操作系统管理通道,提供接口发送和接收数据 避免冲突

另一方面 : 通信成本较高 想要传输信息 必须通过系统调用 将信息从任务的用户空间复制到操作系统管道,然后再复制回接收任务的地址空间

消息传递 IPC也就是:用户空间数据走系统调用到内核,内核中转再送到目标进程用户空间,性能开销高但安全可控。

可以轻松部署到多台机器的分布式系统

NOTE: Go中的通过通信来共享内存!

很多技术都可以实现消息传递 类似的思想!

同样!也是Linux中的重要思想

管道

最简单的进程间通信(任务之间的的单向数据流 一端数据写入端 一端数据读取端 )双向通信必须创建两个管道

类比: 水管 再水流中 一段放入鸭子 从另一侧流出

代码: 调用写入端方法发送数据, 调用读取方 读取传入的数据

管道是一个临时对象,任何一方被丢弃则管道被关闭

**管道本质:**文件描述符

单向字节流:数据没有明确的消息边界,读端需自行解析(如按\n分隔)。

半双工限制:若需双向通信,必须创建两个管道。

亲缘限制:无名管道仅限父子进程;命名管道(FIFO)虽可跨进程,但仍需文件路径协调。

局限:

  1. 如果多个消费者进程同时读取同一个管道,数据可能被任意一个进程抢走(无法定向投递)。

  2. 如果多个生产者写入同一个管道,数据可能混杂(需额外同步机制)。

**NOTE: ** Go中channel 被称为 goroutine之间通信的管道

无名管道

无名管道的作用范围:

子进程-父进程-同一进程中的线程 相关任务共享文件描述符 无名管道在任务使用完毕后消失

命名管道

解决了无名管道(pipe())的局限性允许无亲缘关系的进程通过读写该文件进行数据交换。

跨进程通信需求

客户端和服务器的通信(如数据库服务监听 FIFO 请求)。

创建一个命名管道: mkfifo myfifo 这将 在当前目录下生成一个特殊文件:

echo "hello from writer" > myfifo(这会阻塞,直到有进程读)

在终端2 cat myfifo

文件描述符

参考文章

Linux中规定每一个文件对应一个索引,这样要操作文件的时候,我们直接找到索引就可以对其进行操作了。

**大体思想:**内核为了高效管理这些已经被打开的文件所创建的_索引_,其是一个非负整数(通常是小整数),用于指代被_打开的文件_

0 1 2 是linux 固定的三个文件描述符 剩下的文件也就是依次增加 4 5 ...

每一个连接、管道、文件都占一个 fd 所以一个进程会有很多的文件描述符

Shell 命令执行(如 ls | grep "txt" 就是 fork() + pipe() 实现的)。 这期间发生了什么?

  1. Shell 调用 pipe()

它就是内核中一段缓冲区(队列),写进去的字节可以从读端读出来。

  1. Shell 调用 fork() 两次(或一次+循环):

第一次 fork 出 ls 子进程

第二次 fork 出 grep 子进程

dup重定向 -> 把进程的标准输入(fd 0)、标准输出(fd 1)、标准错误(fd 2)重新指到你想让它流向的地方,比如 pipe、文件、socket。

消息队列
  1. 传输的是结构化消息,每条消息包含: 类型字段(msg_type,用于区分消息类别)。 数据部分(长度固定或可变)。

  2. 多进程可同时读写,通过_消息类型(mtype_)区分目标

生产者通过 mtype 标记消息类别。

消费者通过 mtype 选择性接收,互不干扰。

  1. 显式创建(msgget())和销毁(msgctl(IPC_RMID)),即使进程退出仍存在,除非主动删除。

  2. 并且 消息持久化在队列中,即使消费者未启动,生产者仍可发送消息。

多进程协作怎么体现?

进程A(生产者1):发送类型为1的消息(任务A)。

进程B(生产者2):发送类型为2的消息(任务B)。

进程C(消费者1):只接收类型1的消息。

进程D(消费者2):只接收类型2的消息。

**消息队列模式:**生产者 → 队列 → 消费者

UDSs

同一系统上线程使用的UNIX域套接字

一个线程向套接字写入消息,另一个线程从套接字接收消息( 序列化 = 对象 → 字节流/文本)

问题:

IP 地址 决定了数据包送到哪台计算机。 为什么不是MAC地址+端口号?

MAC地址是链路层概念,只能在同一局域网内定位设备(如你的电脑和路由器)。

IP地址是网络层概念,可以在全球互联网范围内定位主机。

每一跳的MAC地址都会变化,但源IP(192.168.1.2)和目标IP(180.101.49.12)始终不变

回答:

Socket 不直接用 MAC,是因为它处在更高层(传输层 + 应用层)。

MAC 地址仍然存在,只是由操作系统自动处理。

程序员只关心 (IP, Port),而 MAC 是链路层的“底层细节”。

线程池