【面经笔记】线程、多线程【续1】

来源:互联网 发布:河南都市频道网络直播 编辑:程序博客网 时间:2024/06/06 02:01

用户级线程、内核级线:

线程按照其调度者可以分为用户级线程内核级线程两种。

  • 用户级线程:

我们常用基本就是用户级线程,有关线程管理的工作由应用程序完成,用户级线程主要解决的是上下文切换的问题,它的调度算法和调度过程全部由用户自行选择决定,在运行时内核意识不到线程的存在。

用户级线程优点:
1. 不需要内核态与用户态切换
2. 可在任意操作系统运行

用户级线程缺点:
1. 一个线程阻塞则所有线程阻塞。
2. 纯粹用户级线程策略中,无法利用多核处理技术,内核一次只把一个进程分配给一个处理器,一个进程中只有一个线程可以执行。

  • 内核级线程:

有关线程管理的工作由内核完成,应用程序没有线程管理代码,只有一个内核提供的应用程序API

内核级线程优点:
1. 内核可将一个进程中的多个线程调度到多个处理器中,内核本身也可以多处理运行。
2. 一个线程阻塞,内核可以调用一个进程中的另一个线程

内核级线程缺点:
1. 线程切换需要到内核的状态切换


多线程使用了多核吗?

SMP:对称多处理系统,内核可以在任何处理器上执行。

  • windows:

windows使用两类与进程相关的对象:进程和线程。

windows支持SMP硬件配置:任何进程的线程,都可以在任何处理器上运行。同一进程中的多个线程可以在多个处理器上同时执行。

  • Linux

Linux提供一种不区分进程和线程的方案,用户级线程被映射到内核级进程上,组成一个用户级进程的多个用户级线程被映射到共享同一组ID的多个linux内核级进程上

Linux中线程和进程没有区别:Linux中通过复制当前的进程的属性可创建一个新进程,新进程被克隆出来,以使它可以共享资源,当两个进程共享虚拟内存时,他们可以被当做是一个进程中的线程。但是没有为线程单独定义数据结构。

linux内核编译时,CONFIG_SMP配置项控制内核是否支持SMP.

现在的内核包从2.4.23以后就没有专门的SMP内核包,在安装Linux系统时,会自动监测,如果检查到了多个CPU或多核,超线程时,会自动安装两个Linux内核,其中一个是带SMP的,在GRUB引导列表里会出现两个内核选择,默认使用SMP引导.


各种锁

pthread中提供的锁有:pthread_mutex_t, pthread_spinlock_t, pthread_rwlock_t

  • pthread_mutex_t是互斥锁,同一瞬间只能有一个线程能够获取锁,其他线程在等待获取锁的时候会进入休眠状态。因此pthread_mutex_t消耗的CPU资源很小,但是性能不高,因为会引起线程切换。

  • pthread_spinlock_t是自旋锁,同一瞬间也只能有一个线程能够获取锁,不同的是,其他线程在等待获取锁的过程中并不进入睡眠状态,而是在 CPU上进入“自旋”等待。自旋锁的性能很高,但是只适合对很小的代码段加锁(或短期持有的锁),自旋锁对CPU的占用相对较高。

  • pthread_rwlock_t是读写锁,同时可以有多个线程获得读锁,同时只允许有一个线程获得写锁。其他线程在等待锁的时候同样会进入睡眠。读写锁在互斥锁的基础上,允许多个线程“读”,在某些场景下能提高性能。

诸如pthread中的pthread_cond_t(条件变量), pthread_barrier_t(计数锁), semaphone(信号量)等,更像是一种同步原语,不属于单纯的锁。

互斥锁和自旋锁区别:

从实现原理上来讲,Mutex属于sleep-waiting类型的锁。例如在一个双核的机器上有两个线程(线程A和线程B),它们分别运行在Core0和 Core1上。假设线程A想要通过pthread_mutex_lock操作去得到一个临界区的锁,而此时这个锁正被线程B所持有,那么线程A就会被阻塞 (blocking),Core0 会在此时进行上下文切换(Context Switch)将线程A置于等待队列中,此时Core0就可以运行其他的任务(例如另一个线程C)而不必进行忙等待。而Spin lock则不然,它属于busy-waiting类型的锁,如果线程A是使用pthread_spin_lock操作去请求锁,那么线程A就会一直在 Core0上进行忙等待并不停的进行锁请求,直到得到这个锁为止。
所以,自旋锁一般用用多核的服务器


线程池

参考:http://www.cnblogs.com/cpper-kaixuan/p/3640485.html

线程池对线程的管理方式,包括初始化线程的方法、线程创建后的管理、指派任务的方式。线程池优点、调度处理方式和保护任务队列的方式。

为什么需要线程池、线程池优点:

1、 我们将传统方案中的线程执行过程分为三个过程:T1、T2、T3。
T1:线程创建时间
T2:线程执行时间,包括线程的同步等时间
T3:线程销毁时间
那么我们可以看出,线程本身的开销所占的比例为(T1+T3) / (T1+T2+T3)。如果线程执行的时间很短的话,这比开销可能占到20%-50%左右。如果任务执行时间很频繁的话,这笔开销将是不可忽略的。
2、 线程池能够减少创建的线程个数。通常线程池所允许的并发线程是有上界的,如果同时需要并发的线程数超过上界,那么一部分线程将会等待。而传统方案中,如果同时请求数目为2000,那么最坏情况下,系统可能需要产生2000个线程。尽管这不是一个很大的数目,但是也有部分机器可能达不到这种要求。

线程池对线程的管理方式:

  • 初始化线程的方法:

线程池采用预创建的技术,在应用程序启动之后,将立即创建一定数量的线程(N1),放入空闲队列中。这些线程都是处于阻塞(Suspended)状态,不消耗CPU,但占用较小的内存空间。
一旦这些线程创建完毕,我们将调用Start()启动该线程。Start方法最终会调用Run方法。Run方法是个无限循环的过程。在没有接受到实际的任务的时候,m_Job为NULL,此时线程将调用Wait方法进行等待,从而处于挂起状态。

while(m_Job == NULL)       m_JobCond.Wait(); 

一旦线程池将具体的任务分发给该线程,其将被唤醒,从而通知线程从挂起的地方继续执行

  • 指派任务的方式:

当任务到来后,缓冲池选择一个空闲线程,把任务传入此线程中运行,唤醒线程。

  • 线程创建后的管理:

存在两个链表,一个是空闲链表,一个是忙碌链表。空闲链表中存放所有的空闲进程,当线程执行任务时候,其状态变为忙碌状态,同时从空闲链表中删除,并移至忙碌链表中。

线程池中容纳的线程数目并不是一成不变的,其会根据执行负载进行自动伸缩。

为此在ThreadPool中设定四个变量:

m_InitNum:初始创建时线程池中的线程的个数。

m_MaxNum:当前线程池中所允许并发存在的线程的最大数目

m_AvailLow:当前线程池中所允许存在的空闲线程的最小数目,如果空闲数目低于该值,表明负载可能过重,此时有必要增加空闲线程池的数目。实现中我们总是将线程调整为m_InitNum个。

m_AvailHigh:当前线程池中所允许的空闲的线程的最大数目,如果空闲数目高于该值,表明当前负载可能较轻,此时将删除多余的空闲线程,删除后调整数也为m_InitNum个。

m_AvailNum:目前线程池中实际存在的线程的个数,其值介于m_AvailHigh和m_AvailLow之间。如果线程的个数始终维持在m_AvailLow和m_AvailHigh之间,则线程既不需要创建,也不需要删除,保持平衡状态。因此如何设定m_AvailLow和m_AvailHigh的值,使得线程池最大可能的保持平衡态,是线程池设计必须考虑的问题。

线程池在接受到新的任务之后,线程池首先要检查是否有足够的空闲线程可用。检查分为三个步骤:

(1)检查当前处于忙碌状态的线程是否达到了设定的最大值m_MaxNum,如果达到了,表明目前没有空闲线程可用,而且也不能创建新的线程,因此必须等待直到有线程执行完毕返回到空闲队列中。

(2)如果当前的空闲线程数目小于我们设定的最小的空闲数目m_AvailLow,则我们必须创建新的线程,默认情况下,创建后的线程数目应该为m_InitNum,因此创建的线程数目应该为(当前空闲线程数与m_InitNum之差;但是有一种特殊情况必须考虑,就是现有的线程总数加上创建后的线程数可能超过m_MaxNum,因此我们必须对线程的创建区别对待。
如果创建后总数不超过m_MaxNum,则创建后的线程为m_InitNum;如果超过了,则只创建( m_MaxNum-当前线程总数 )个。

(3)调用GetIdleThread方法查找空闲线程。如果当前没有空闲线程,则挂起;否则将任务指派给该线程,同时将其移入忙碌队列

当线程执行完毕后,其会调用MoveToIdleList方法移入空闲链表中,其中还调用m_IdleCond.Signal()方法,唤醒GetIdleThread()中可能阻塞的线程。

当线程都在处理任务,缓冲池自动创建一定数量的新线程,用于处理更多的任务。在任务执行完毕后线程也不退出,而是继续保持在池中等待下一次的任务。当系统比较空闲时,大部分线程都一直处于暂停状态,线程池自动销毁一部分线程,回收系统资源。

线程池适用场景:

(1) 单位时间内处理任务频繁而且任务处理时间短

(2) 对实时性要求较高。如果接受到任务后在创建线程,可能满足不了实时要求,因此必须采用线程池进行预创建。

(3) 必须经常面对高突发性事件,比如Web服务器,如果有足球转播,则服务器将产生巨大的冲击。此时如果采取传统方法,则必须不停的大量产生线程,销毁线程。此时采用动态线程池可以避免这种情况的发生。

线程池框架:

  1. 线程池管理器:用于创建并管理线程池

  2. 工作线程: 线程池中实际执行的线程

  3. 任务接口: 尽管线程池大多数情况下是用来支持网络服务器,但是我们将线程执行的任务抽象出来,形成任务接口,从而是的线程池与具体的任务无关。

  4. 任务队列:本质是生产者与消费者模型的应用。线程池的概念具体到实现则可能是队列,链表之类的数据结构,其中保存执行线程。

将实际的任务赋值给线程。当没有任何执行任务即m_Job为NULL的时候,线程将调用m_JobCond.Wait进行等待。一旦Job被赋值给线程,其将调用m_JobCond.Signal方法唤醒该线程。由于m_JobCond属于线程内部的变量,每个线程都维持一个m_JobCond,只有得到任务的线程才被唤醒,没有得到任务的将继续等待。无论一个线程何时被唤醒,其都将从等待的地方继续执行m_Job->Run(m_JobData),这是线程执行实际任务的地方。
在线程执行给定Job期间,我们必须防止另外一个Job又赋给该线程,因此在赋值之前,通过m_VarMutex进行锁定, Job执行期间,其余的Job将不能关联到该线程;任务执行完毕,我们调用m_VarMutex.Unlock()进行解锁,此时,线程又可以接受新的执行任务。
流程:
阻塞等待 -> 任务赋值 ->唤醒 -> 任务赋值锁定 -> 执行任务 -> 任务结束 -> 任务赋值解锁 -> 阻塞等待

简单线程池实现:
http://blog.csdn.net/chengonghao/article/details/51791917


下一篇:
死锁
死锁预防
死锁避免
生产者消费者问题
读者写者问题
无锁编程:
有锁编程的问题与解决方法:阻塞(效率下降)、死锁、优先级反转问题
无锁编程解决单生产者多消费者问题和多生产者多消费者问题
线程安全的接口

原创粉丝点击