多线程基本知识

本文主要介绍多线程的相关知识。

基本概念

并发与并行

并发:虚假并行。即是多个任务的占用同一资源,CPU交替处理。某一时间段内同时执行多个任务,某一时刻只执行单个任务。由操作系统进行多任务时间分配,需要进行不同的任务切换。任务切换本身需要一定时间。(单核CPU)

并行:真实并行,某一时间段与某一时刻均可执行多个任务。(多核CPU)

并发与并行可以提高性能。因为CPU的处理速度远高于信息交换的速度,因此会出现在处理同一任务时CPU等待信息交换的情况,此时处理其他已经加载好数据的任务是合理的。

可执行程序

可以执行某项任务的文件。

进程

一个正在运行的程序,程序运行的一个实例。一个程序可以有多个进程,如同时开多个word,但是每个进程只对应一个程序。程序就是文件,只是文件。

进程与可执行程序是多对一的关系。

线程

每个进程都有一个主线程,主线程是唯一的。主线程与进程是一一对应关系。

但是线程和进程是多对一的关系:一个进程可以有多个线程。

同一进程的多个线程共享同进程的资源。

线程不是越多越好,线程需要独立的堆栈空间,同时不同线程的切换也需要时间。

并发实现

多进程并发

同时启动多个程序。

进程之间通信:

  • 同一电脑:管道、文件共享、消息队列、共享内存等
  • 不同电脑:socket等

多线程并发

单个进程中创建多个线程。一个进程中的所有线程共享地址空间(共享内存)。共享全局变量、指针、引用等。

锁是实现多线程通信的关键点。多线程是通过共享内存实现相互之间通信的。一般而言,这个变量在A线程中体现为只写,在B线程中体现为只读。如何避免在A线程写入过程中B线程的读和B在读的过程中A线程的写就要通过锁来实现。因为普通变量的读取和写入是可以被中断的过程,很有可能B线程正在读,然后变量值就被A修改了,那么B读到的就是修改后的值,会出现问题。

锁,顾名思义就是给共享内存加锁以实现保护。在进行读写之前必学要进行加锁。

加锁和解锁的过程属于操作系统内核函数,不允许被中断。这样就保证了对同一个锁变量访问不会出现问题。

死锁

所谓死锁,就是指不同线程之间相互等待。举例来说有AB两个线程,a和b两个共享变量。A对a加了锁还没有释放,现在要访问b,就要对b进行加锁,而b已经被B加锁还未释放,这么A就要阻塞等待B释放b的锁。而B此时就在等待对a的访问需要对其进行加锁,但a被A保持,因此B也陷入阻塞等待。这就形成了循环等待的死锁。

信号量

信号量也是实现线程通信的机制之一。信号量是为了体现资源数量而产生的。使用信号量的地方往往资源数量大于一。

举例生产者消费者问题来说,有一个缓冲区H可以容纳三个int。有5个进程ABCDE要通过缓冲区H进行相互通信。其中AB线程只对H写,也即是生产者。CDE只对H进行读操作,也即是消费者。当H一旦有空闲区域,AB线程就要可以对空白区域写入。CDE一旦发现有未读过的数字产生,就可以进行读取。这就需要引入信号量机制了。

对H加两个信号量,readCount=0(初始时没有可供读取的资源), writeCount=3(初始时有三个缓冲区可供写入)。

首先AB线程要加锁访问(非官方术语,自己造的,不知道怎么表述了)信号量writeCount(这时writeCount--,信号量本身的机制保证了同时只会有一个线程对writeCount进行操作,且操作本身不可中断,这是由系统决定的。信号量小于等于0时,线程将被阻塞等待)。然后可以对缓冲区进行写入操作,(对H进行读写时还是要设置锁,保证每次只能有一个线程对H进行访问)。写入之后,对readCount进行解锁(借用锁的概念,不知道官方术语怎么说,自己造了一个词汇。所谓解锁即是readCount++,上文提到信号量值小于等于0时,线程被阻塞,大于0的时候线程就可以继续运行了)。

当readCount>0的时候,读线程这时会被唤醒,对缓冲区进行读操作(对缓冲区加锁访问),读结束之后即可对writeCount解锁(即是信号量++)。

这样通过信号量和锁机制就解决了生产者消费者问题。这是操作系统经典问题,浅尝辄止,不建议深究。

线程调度

线程之间的通信由锁和信号量进行。线程之间的通信就是用户本身实现的辅助操作系统进行线程调度的。操作系统依据线程通信来对线程就行调度。

在单核状态下,多线程是被操作系统轮流调度执行的,即并发。多核CPU可以实现真正的并行。

注意事项

线程越多越好吗?什么时候才有必要用多线程?

线程必然不是越多越好,线程切换也是要开销的,当你增加一个线程的时候,增加的额外开销要小于该线程能够消除的阻塞时间,这才叫物有所值。

Linux自从2.6内核开始,就会把不同的线程交给不同的核心去处理。Windows也从NT.4.0开始支持这一特性。

什么时候该使用多线程呢?这要分四种情况讨论:

a.多核CPU——计算密集型任务。此时要尽量使用多线程,可以提高任务执行效率,例如加密解密,数据压缩解压缩(视频、音频、普通数据),否则只能使一个核心满载,而其他核心闲置。

b.单核CPU——计算密集型任务。此时的任务已经把CPU资源100%消耗了,就没必要也不可能使用多线程来提高计算效率了;相反,如果要做人机交互,最好还是要用多线程,避免用户没法对计算机进行操作。

c.单核CPU——IO密集型任务,使用多线程还是为了人机交互方便,

d.多核CPU——IO密集型任务,这就更不用说了,跟单核时候原因一样。

CPU数量、核心数量与线程的关系

单核CPU上运行的多线程程序, 同一时间只能一个线程在跑, 系统帮你切换线程而已, 系统给每个线程分配时间片来执行, 每个时间片大概10ms左右, 看起来像是同时跑, 但实际上是每个线程跑一点点就换到其它线程继续跑

线程编程的关键问题

  1. 避免死锁
  2. 严格设计线程运行流程,尽最大可能实现利用线程对CPU的压榨,不要多线程的实现不仅没有提升性能,反而降低性能
  3. 避免多线程的运行变成单线程

新手很容易出现这个问题,明明是要设计多线程,但是设计完一运行其实还是顺序执行,没有实现并行。

  1. 线程切换本身消耗比较大,对于不必要代码段没必要设计单独线程

学习资源推荐

通过调用库函数实现多线程本身并不困难。真正难的是如何对线程进行管理和控制,也即是如何实现不同进程之间的有效通信。在学习过程千万不要陷进去,学会用即可。不要理论知识纠缠太多,简单理解就成。在实践中深化对多线程编程的认识。

目前队内使用的是pthread库(仅在Linux可以使用),c++11之后也提供了多线程编程的官方库(跨平台)。从学习的角度讲,使用什么库并没有什么影响,关键要学会如何进行线程管理。

C++11多线程编程是视频介绍,建议大家先简单学习这里的内容,相对来说比较完整,是上述概念的一个实践。英语听力比较好的也可以看下面这个教程C++ Threading。同时,对于c++11的相关函数的介绍可以参阅C++多线程编程

看完之后,可以看下面的网页了解pthread编程:

Pthreads入门教程

POSIX多线程设计

posted @ 2020-11-28 12:14  LightningStar  阅读(329)  评论(0编辑  收藏  举报