多线程编程的问题,对于大多数程序员来说,都是一个绕不开的坎。在编了一些程序后,我也来谈谈自己的感受。我并不想把本文写成一篇教课书式的文章,而期望是一个工程的入门指引,能够覆盖到大部分工程技巧和解决问题的思路,但又不过于深入而难于理解。因此我会从逻辑层次和实现层次两个部分来讲述多线程编程,更深入的讨论我把它们列在了在参考资料中,这可不是一篇文章能够讨论完的。
首先来看一个不是很恰当的比方。假设你面对这么一个情况,
1. 手上存在2件事或n件事
2. 2件或n件事情不存在依赖关系,相互独立。
你会如何完成这两件事呢?
上面的比方有点非正式。让我们来重新阐述一下重点。抢占式操作系统根据时间片决定进程(或线程)调度方案,操作系统会剥夺耗时长的进程(或线程)的时间片,提供给其它进程(或线程)。通常情况下,CPU负载都是不饱和的,且一个线程只会运行在一个核上。在多核情况下,增加线程会使进程更加充分的利用硬件资源。在单核情况下,在假设操作系统按照线程来决定调度方案的情况下,可以认为一个线程多的进程获得的CPU计算时间要多于只有一个线程的进程。这是多线程的优势吗?某种程度上是的。在OpenMP的框架中,OpenMP会把一个for循环的计算拆成多个小循环分交给不同的线程去完成。在CPU还存在富余能力的情况下,合理的创建多线程可以减少任务的执行时间。
那当CPU已经满负荷工作时,情况是怎样呢?此时创建多线程的进程仍然要比没有多线程的进程分到更多的时间片,这相当于穷庙里出了个富方丈。虽然此时作为整体而言已经没有意义,系统的瓶颈是CPU的执行能力。
线程的创建和销毁在实现上是存在开销的,当线程运行的时间同线程创建的时间相比,差不太多的时候,创建线程的意义就不大了。假设线程创建的时间和线程执行任务的时间相同,那么明显的,不创建线程完成任务的效率是最高的。在实现中,当线程创建和销毁的开销成为系统的瓶颈时,解决方案就是线程池。
在上面的所有讨论中,其讨论的基础都是任务A和任务B不相关联的情况,那如果A任务和B任务之间存在某种联系呢?让我们来看一下任务之间会存在哪些关系。这些关系对应着多线程中锁的原语。
1. 任务A和任务B都需要同一资源的支持,并且对资源的访问为独享。对应锁mutex。
2. 任务A的完成会触发任务B,即任务之间存在依赖。对应锁event或者条件事件。
3. 任务A与任务B需要某类资源支持,由于资源有限,A和B存在竞争关系,但却可以不是严格的独享制。对应锁semaphore。
在这3种关系中,第3种关系可以看成是第1和第2种的基础。在工程实现上也是,我们可以用sempahore来
1. 逻辑层次
逻辑层次的多线程编程和操作系统和编程语言应该是无关的。在这个层次上,多线程的概念应该是非常干净的。多线程可以被看成人们并行处理事件时的一种策略。事实上,人们在遇到类似问题,提出的解决方法也通常是类似的。首先来看一个不是很恰当的比方。假设你面对这么一个情况,
1. 手上存在2件事或n件事
2. 2件或n件事情不存在依赖关系,相互独立。
你会如何完成这两件事呢?
这个问题很简单,解决的方法自然也很多。有些人喜欢一心一意的做某件事情,在做完一件事后,再做另外一件事情,这是典型的串行化思维。另外一些人喜欢两件事情同时做,做点A,碰到难题了,做点B,调节一下,再回过去做B。还有一些人情商比较高,门路广,兄弟多,事情自己做一点,另外的事情全部交给兄弟去做就可以了。
计算机是人发明的,在让计算机并行处理数据的思路上,碰到的问题和解决的方式同上面的情况相比并没有太大的区别。抢占式操作系统按时间片来分配线程的时间,对于操作系统而言(其本身也是一个进程),任何进程或线程都是其业务,必须同时进行。这毫无疑问相当于一个人同时做n件事情。使用单线程完成一个多任务的操作,类似于人们串行做n件事情。剩下的,多线程作业则像把事情交给多个人同时完成。
上面的比方有点非正式。让我们来重新阐述一下重点。抢占式操作系统根据时间片决定进程(或线程)调度方案,操作系统会剥夺耗时长的进程(或线程)的时间片,提供给其它进程(或线程)。通常情况下,CPU负载都是不饱和的,且一个线程只会运行在一个核上。在多核情况下,增加线程会使进程更加充分的利用硬件资源。在单核情况下,在假设操作系统按照线程来决定调度方案的情况下,可以认为一个线程多的进程获得的CPU计算时间要多于只有一个线程的进程。这是多线程的优势吗?某种程度上是的。在OpenMP的框架中,OpenMP会把一个for循环的计算拆成多个小循环分交给不同的线程去完成。在CPU还存在富余能力的情况下,合理的创建多线程可以减少任务的执行时间。
那当CPU已经满负荷工作时,情况是怎样呢?此时创建多线程的进程仍然要比没有多线程的进程分到更多的时间片,这相当于穷庙里出了个富方丈。虽然此时作为整体而言已经没有意义,系统的瓶颈是CPU的执行能力。
线程的创建和销毁在实现上是存在开销的,当线程运行的时间同线程创建的时间相比,差不太多的时候,创建线程的意义就不大了。假设线程创建的时间和线程执行任务的时间相同,那么明显的,不创建线程完成任务的效率是最高的。在实现中,当线程创建和销毁的开销成为系统的瓶颈时,解决方案就是线程池。
在上面的所有讨论中,其讨论的基础都是任务A和任务B不相关联的情况,那如果A任务和B任务之间存在某种联系呢?让我们来看一下任务之间会存在哪些关系。这些关系对应着多线程中锁的原语。
1. 任务A和任务B都需要同一资源的支持,并且对资源的访问为独享。对应锁mutex。
2. 任务A的完成会触发任务B,即任务之间存在依赖。对应锁event或者条件事件。
3. 任务A与任务B需要某类资源支持,由于资源有限,A和B存在竞争关系,但却可以不是严格的独享制。对应锁semaphore。
在这3种关系中,第3种关系可以看成是第1和第2种的基础。在工程实现上也是,我们可以用sempahore来