Loading... ## 多线程 一般而言,一个程序只有一个执行点(一个程序计数器,用于存放要执行的指令),但多线程程序有多个执行点。换一个角度来说,每个线程都类似于一个独立的进程,只有一点区别:它们共享地址空间,可以访问相同的数据。 线程之间的切换类似于进程间的上下文切换。与进程间上下文切换相比的主要区别是:地址空间保持不变,即不需要切换当前使用的页表。 ![单线程和多线程的地址空间对比][1] 在多线程的进程中,每个线程独立运行,因此每个线程都有自己独立的栈。 可以发现在单线程的进程中,堆和栈可以互不影响的增长,直到空间耗尽,多个栈就不可以做到了。幸运的是,栈一般不会很大。 ## 锁-线程的竞争规范 由于多线程的加入,以及程序员对线程的调度基本是不可控的,我们不知道且无法对线程的调度时机做出具体限制,因此当多个线程进行竞争时,就会带来新的问题。 案例:变量x值为0,线程A对变量x循环自增1,循环10000次;线程B也对变量x自增循环10000次。 按照常理,结束后,变量x的值应当是20000。但实际上变量x的值却是小于20000,且不固定。 但考虑以下情形(事实上也是这么发生的):当我们说线程A给x自增1,这个过程并不是原子性的,它其实包含了几个步骤,首先从内存中获得x的值,将其放入寄存器中,然后给寄存器中的值加1,最后将这个寄存器的值存到x的内存地址。如果在这个过程中,发生线程之间的切换,即从线程A切换到B,由上文可知,这是由系统控制的,我们无法控制这一行为。那么此时线程B同样会从内存取得x的值,然后给它加1,再放回内存中,反复几个循环后。然后线程再次切换,从线程B切换到线程A,此时线程A将寄存器的值写入内存。此时大事不妙了,变量x不仅没有增加反而还比刚刚线程切换前更小了。 ![线程切换带来的错误-案例][2] 由此,可以看出并发编程中的一个最基本问题:我们希望原子性地执行一系列指令,使得多线程的切换不会导致指令中断,从而导致一系列错误的发生。 虽然我们无法控制具体的线程调度,但是我们可以有限度的控制线程的一些行为,从而让程序员获得一些对线程调度的间接控制权。利用锁,我们可以让没有获得锁的线程从运行态转为就绪态(yield),直到锁被释放了才会有重新唤醒的可能(可能需要排队或者竞争)。同时保证同一时刻,仅有一个线程能获得锁。这样,锁使得原本完全由操作系统调度的混乱状态变得稍稍可控了。 我们可以使用简单代码编写一个锁(又称硬件锁),但是这种锁会使得无法获得锁的线程处于不断自旋的状态,这比较浪费CPU时间。 更为常用的以及高效的是,使用操作系统系统调用支持的锁,这样的锁通过系统调用,实现线程从运行态转为就绪态,锁被释放后的重新唤醒线程等操作。 Linux上采用一种古老的锁方案,称为两阶段锁,两阶段锁意识到自旋锁并不是一无是处,当锁很快就会被释放的场景中,自旋锁的效率很高,所以两阶段锁在第一阶段会自旋一段时间(更常见的方式是自旋固定的次数),希望可以获得锁,但是若第一阶段没有获得锁后,会进入第二阶段,此时线程会进入休眠(就绪态),直到锁可用才会有唤醒的可能。 ## 条件变量-线程的合作规范 多个线程之间的竞争关系是我们需要锁来规范线程的竞争。但线程之间的关系不仅有竞争,还有合作! 有些时候,父线程需要检查子线程是否执行完毕,当子线程执行完毕才会执行父线程。 [1]: https://assets.fangshirui.cn/typecho/uploads/2024/09/1268122085.png [2]: https://assets.fangshirui.cn/typecho/uploads/2024/09/199665188.png 最后修改:2024 年 09 月 04 日 04 : 01 PM © 允许规范转载