共享资源
一个可供线程访问的变量、设备或内存块等类型的实体被称为资源。可供多个线程访问的资源被称为共享资源;而同时访问共享资源的行为被称为共享资源竞争。
如果在访问共享资源时不独占该共享资源,可能会造成资源异常(如变量值混乱、设备出错或内存块内容不是期望值等),进而导致程序运行异常甚至崩溃。
现在有两个线程(线程 A 和线程 B)需要同时将同一个变量 V(初始值为 0)进行加一操作。
在 RISC 机器上,一般都是 load/store 体系结构,即访问内存只允许 load 和 store 操作;变量 V 自增操作的机器指令流程如下:
- 加载变量 V 的地址到 CPU 的工作寄存器 0。
- load 指令将工作寄存器 0 存储的地址里的内容加载到工作寄存器 1。
- inc 指令将工作寄存器 1 的值加一。
- store 指令将工作寄存器 1 的值保存到工作寄存器 0 指向的地址。
由上看到,变量 V 的自增操作不是一步完成的,如果线程 A 和线程 B 依次完成以上四步,那么最后变量 V 的值将会是 2。
如果线程 A 完成了前面三步,这时线程 B 打断了线程 A 的工作,线程 B 将变量 V 改写为 1;随后线程 A 继续执行第四步,那么最后变量 V 的值仍然是 1,这显然不是我们期望的。
为了解决这种问题,我们需要对该过程进行互斥访问。互斥是一种排它性行为,也即同一时间只允许一个线程访问共享资源。实现互斥有多种方法:关中断、禁止任务调度、信号量等。
针对上面的过程,我们可以加入一把锁(信号量),在进行变量 V 的自增操作前必须占有该锁,操作完成后释放该锁;假设锁已经被线程 A 占有,如果线程 B 也要申请该锁,因为锁具有排它性,线程 B 将被阻塞;这样确保同一时间只有一个线程能访问该变量,变量 V 的值就不会有混乱的风险。
我们称被锁保护的区域为临界区。如果临界区保护的代码不可被打断,那么过程是原子性的操作,不可打断意味着临界区内不存在阻塞和硬件中断发生,原子性操作屏蔽了当前 CPU 核心的硬件中断响应,所以原子性操作应该尽量简短。在多线程环境下,每一个变量的操作都需要小心对待。多线程编程可以使我们的程序清晰和易于实现,但需要我们谨慎地设计。SylixOS 为我们准备了大量解决多线程编程互斥问题的解决方案,例如信号量、互斥锁、消息队列等。