530 likes | 720 Views
多核结构与程序设计. 杨全胜 http://www.njyangqs.com/. 东南大学成贤学院计算机系. 进程、线程和并行程序设计. 内容 进程的概念 什么是线程 线程的设计 互斥与同步 并行程序设计的常见问题. 进程的概念. 现代操作系统以进程的形式来加载程序 进程是程序的一次动态执行 进程是资源的拥有者 进程是一个四元组 (P,C,D,S) P- 程序代码 C- 进程控制状态 D- 进程的数据 S- 进程的执行状态 进程的特征 资源特征,包括程序执行所必需的计算资源 执行特征,包括在进程执行过程中动态改变的特征. 进程的概念.
E N D
多核结构与程序设计 杨全胜 http://www.njyangqs.com/ 东南大学成贤学院计算机系
进程、线程和并行程序设计 • 内容 • 进程的概念 • 什么是线程 • 线程的设计 • 互斥与同步 • 并行程序设计的常见问题
进程的概念 • 现代操作系统以进程的形式来加载程序 • 进程是程序的一次动态执行 • 进程是资源的拥有者 • 进程是一个四元组(P,C,D,S) • P-程序代码 • C-进程控制状态 • D-进程的数据 • S-进程的执行状态 • 进程的特征 • 资源特征,包括程序执行所必需的计算资源 • 执行特征,包括在进程执行过程中动态改变的特征
进程的概念 • 进程的状态 退出状态 非存在 状态 运行状态 就绪状态 挂起状态
进程的概念 • 进程间的通信 • 现代操作系统提供基本的系统调用函数,允许位于同一台处理机或不同处理机的多个进程之间相互交流信息 • 表现形式 • 通信 • 同步 • 聚集(归约) • 实现方法 • 在共享存储模式下,通信可以通过操作系统读/写共享缓存来实现。 • 在分布式存储模式下,通信要依赖网络。
进程、线程和并行程序设计 • 内容 • 进程的概念 • 什么是线程 • 线程的设计 • 互斥与同步 • 并行程序设计的常见问题
什么是线程 • 进程(process)与线程(thread) • 进程不适合细粒度的共享存储并行程序设计 • 一个进程有一个主线程来初始化进程和开始执行指令。 • 线程能在进程内创建其他线程 • 每个线程有它自己的堆栈 • 进程内的所有的线程共享代码和数据段
什么是线程 • 线程是进程上下文中执行的代码序列,又称为轻量级的进程。它是操作系统能够调度的最小单元 • 进程中可以只有一个线程串行执行,也可以是多个线程共享资源下并行执行。 DATA FILE CODE FILE CODE DATA REG REG REG STACK REG STACK STACK STACK thread thread
什么是线程 • 使用线程优于进程的地方 • 创建一个线程比创建一个进程的代价要小 • 线程的切换比进程间的切换代价小 • 多线程可以充分利用多处理器 • 数据共享 • 数据共享使得线程之间的通信比进程间的通信更高效 • 快速响应特性 • 在系统繁忙的情况下,进程通过独立的线程及时响应用户的输入
什么是线程 • 线程级别 • 用户级线程 • 有关线程的所有管理工作都由在用户级实现的线程库来支持 • 因操作系统调度进程而被同时调度 • 由线程API来创建和管理,无需内核参与,操作更快 • OpenMP, Pthreads, Windows thread API • 进程中的所有线程将共享相同的时间片 • 当一个线程被挂起,同一进程中的其他线程也会被挂起,因此并行性不高
什么是线程 • 线程的级别 • 内核级线程 • 内核级线程由操作系统内核调度与管理 • 并行度高 • 当一个线程被挂起,同一进程的其他线程依然可以运行。 • 在进程中的不同内核线程能够运行在不同的CPU或核中。 • 内核创建和管理内核级线程的代价高,但好于对进程的代价
什么是线程 • 线程的级别 • 硬件级线程 • 由硬件来调度 • SMT: 同时多线程 • 超线程技术(intel的HT) • UltraSPARC (SUN) • CMT: 芯片多线程 • 芯片多进程+多线程 • 也许是简单核,但是多线程 • 多核 • 众核
什么是线程 • 多线程的映射模型 • 对于实现了用户级线程和内核级线程的操作系统,用户级线程和内核级线程之间的可以有不同的映射方式 • 多对一模型 • 把多个用户级线程映射到一个内核级线程 • 线程的管理在用户空间实现,所以效率高。 • 当一个线程因调用系统调用被阻塞时,整个进程被阻塞 • 一对一模型 • 把每个用户级线程影射到一个内核级线程。 • 当一个线程阻塞时,其他线程仍然可以运行。 • 多对多模型 • 将m个用户级线程影射到n个内核级线程,m≥n • 用户可以创建所需要的用户级线程,通过分配适当数目的内核级线程获得并发执行的优势并节省系统资源。
什么是线程 • 线程的生命周期 • 线程的标识 • 通常用一个整数来标识一个线程 • 线程的创建 • 自动创建从main函数开始的主线程 • 调用函数库接口创建一个新的线程(pthread_create) • 线程的终止 • 执行完毕,或者调用了pthread_exit • 主线程退出导致整个进程会终止
什么是线程 • 线程的状态 • 就绪(ready):线程等待可用的处理器。 • 运行(running):线程正在被执行。 • 阻塞(blocked):线程正在等待某个事件的发生(比如I/O的完成,试图加锁一个被上锁的互斥量)。 • 终止(terminated):线程从起始函数中返回或者调用pthread_exit。
什么是线程 • 线程状态的变迁
进程、线程和并行程序设计 • 内容 • 进程的概念 • 什么是线程 • 线程的设计 • 互斥与同步 • 并行程序设计的常见问题
线程的设计 • 为了功能而线程化 • 为了性能而线程化 • 为了节省周转时间而线程化 • 为了吞吐量提高而线程化 • 分解工作 • 任务分解 • 数据分解 开发应用程序的时候进行线程化的最佳时机是设计阶段
线程的设计 • 为功能而线程化 • 分配不同的线程来完成应用程序的不同功能 • 这是最容易的方法,因为功能重叠的机会很罕见。 • 在一个应用程序中控制并发功能的执行是比较容易的。 • 即使在计算间没有直接的影响,功能之间的依赖性还会维持。
线程的设计 • 为功能而线程化 • 举例: • 为了简化代码,为下列部分设计不同的线程 • 输入、图形用户界面、计算和输出。 • 考虑在建一个房屋中的不同的人: • 泥瓦匠, 木匠, 盖屋顶的人, 水暖工和油漆匠。
线程的设计 • 为性能而线程化 • 通过将执行在并行环境下的大量的计算分解开来进行应用程序的并行化,能够提高计算的性能。 • 线程化是为了改善周转周期和吞吐量 • 比如: • 搜索太空实验室碎片 • 把全部搜索区域分成多个分段,并安排一个工人去搜索一个分段
线程的设计 • 为缩短周转周期而线程化 • 用可能的最小的时间完成一个任务 • 举例:安排一个饭桌时候的不同任务: • 一个侍者摆放盘子。 • 一个侍者折叠和放置餐巾。 • 一个侍者摆放花和蜡烛。 • 一个侍者摆放器皿 • 汤匙、刀子和叉子 • 一个侍者放玻璃杯
线程的设计 • 为了吞吐量而线程化 • 在固定的时间内完成最多的任务 • 举例:安排一个饭局时候的不同任务: • 对多个侍者的安排: • 每个桌子安排一个侍者。 • 一个侍者能摆放所有桌子的所有盘子;另一个可以摆放所有的玻璃杯;以此类推。
线程的设计 • 任务分解(客户/服务器编程模式) • 数据分解(工作组编程模式) • 数据流分解(流水线编程模式)
线程的设计 • 线程化的好处: • 提高性能 • 能够使用多核处理器 • 线程共享数据会比较快,因为他们共享相同地址空间。 • 在多核处理器中使用多线程,你可以用更少的时间完成更多的任务。 • 更好的资源利用 • 线程甚至可以减少单核处理器的延迟。 • 有效的数据共享 • 使用共享存储来共享数据
线程的设计 • 使用线程的难点 • 数据竞争 • 死锁 • 代码复杂性 • 可移植性问题 • 测试和调试的难度
进程、线程和并行程序设计 • 内容 • 进程的概念 • 什么是线程 • 线程的设计 • 互斥与同步 • 并行程序设计的常见问题
互斥与同步 • 竞争条件 • 为了使用共享资源,线程彼此竞争 • 虽设定了执行顺序,但是不能保证就按照这个顺序执行,而结果却由执行的顺序决定 • 是并发程序中最常见的错误(与时间有关的错误). • 数据竞争 • 指存储器访问冲突的情况 • 多个线程并发访问同一个存储单元时,至少有一个线程要改变那个单元的值,就会出现数据竞争 • 引发两种可能的冲突: • 读/写冲突 • 写/写冲突
互斥与同步 • 数据竞争 隐含的数据竞争 如果一开始 x=0,那么结束的时候x=? 注意: x+=1 编译成 t=x; x=t+1
互斥与同步 进入区 临界区 • 互斥 • 临界区 • 是代码中访问(读和写)共享变量的那部分代码 • 多个线程访问同一个临界区的原则: • 一次最多只能一个线程停留在临界区内 • 不能让一个线程无限地停留在临界区内,否则其他线程将不能进入该临界区 • 互斥 • 线程互斥是指对于共享资源,在各线程访问时的排它性 • 举例:银行的保管箱 • 维护人员确保互斥 退出区
互斥与同步 • 同步 • 线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一线程的消息,当它没有得到另一个线程的消息时应该等待,直到消息到达时才被唤醒 • 使用同步对象来确保互斥: • 信号量、互斥量、条件变量、读/写锁、事件和栅障。 • 一个线程获得同步对象,其他线程必须等待 • 当获得同步对象的线程完成,释放对象,将对象给等待的线程。 • 举例: 图书馆 • 一个顾客借了一本书 • 其他人必须等着书被还回来
互斥与同步 • 栅障同步 • 如果多个线程在继续向下执行前,需要完成各自任务并达到某个新起点,则在此点设置栅障 • 是用来确保在栅障之前代码段做的修改在线程要越过栅障继续执行前全部完成。 • 线程在栅障的地方暂停 • 当所有的线程到达栅障的时候,所有线程被释放继续执行 • 举例:跑步
互斥与同步 • 死锁 • 当两个线程因为互相等待被对方拥有并且不会释放的资源而被阻塞的时候,会发生死锁。 • 产生死锁的原因主要是: • 因为系统资源不足。 • 进程运行推进的顺序不合适。 • 资源分配不当等。 • 死锁的四个必要条件: • 互斥条件 • 请求与保持条件 • 不可剥夺条件 • 循环等待条件
互斥与同步 • 死锁 • 死锁的预防 • 破坏“互斥”条件 • 在系统里取消互斥。但一般来说“互斥”条件是无法破坏的。 • 破坏“请求与保持”条件 • 不允许进程在已获得某种资源的情况下,申请其他资源。 • 创建进程时,要求它申请所需的全部资源。 • 要求每个进程提出新的资源申请前,释放它所占有的资源。 • 破坏“不可抢占”条件 • 允许对资源实行抢夺。 • 破坏“循环等待”条件 • 将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,但所有申请必须按照资源的编号顺序(升序)提出。
互斥与同步 • 死锁 • 死锁的避免 • 一个进程序列{P1,…Pn}是安全的,如果对于其中每一个进程Pi(1<=i<=n),它以后尚需要的资源量不超过系统当前剩余资源量与所有进程Pj(j<i)当前占有资源量之和 • 现有12个资源供3个进程共享,进程P1总共需要9个资源,但第一次先申请2个;进程P2总共需要10个资源.第一次申请5个;进程P3总共需要4个资源,第一次请求2个,1)这样请求后,系统安全吗?2)如果接着P1第二次申请1个,能给它吗? • 银行家算法 • (1)当一个顾客对资金的最大需求量不超过银行家现有的资金时就可接纳该顾客; • (2) 顾客可以分期贷款,但贷款的总数不能超过最大需求量; • (3)当银行家现有的资金不能满足顾客尚需的贷款数额时,对顾客的贷款可推迟支付,但总能使顾客在有限的时间里得到贷款; • (4) 当顾客得到所需的全部资金后,一定能在有限的时间里归还所有的资金.
互斥与同步 • 饿死 • 当一个线程正在等一个资源而该资源被其他线程拥有,但由于某种原因这个资源永远不能被这个线程使用的时候,发生饥饿(但没死锁)。如果等待是永久的,那就是饿死。 • 举例 • 在使用小文件优先的打印系统中一个大文件请求打印。 • 活锁 • 忙等待的时候发生的饥饿 • 锁的粒度 • 锁的粒度是上锁后保护的共享数据的多少 • 减小锁的粒度可以提高对共享数据访问的并行性
P(S) { // P操作的定义 S.value--; if(S.value<0) { 加本线程到S.L; Block(); } } V(S) { //V操作的定义 S.value++; if(S.value<=0) { 从S.L中删除某线程P; Wakeup(P); } } 互斥与同步 • 同步原语 • 信号量 • 信号量可以表示为一个整数,并且被两个基本原语操作所界定: • P: Proberen,意味着测试。 • 如果信号量的值大于0,P操作把信号量的值减1并返回;如果当前信号量的值为非正数则P会等待。 • V: Verhogen,意味着增加 • V操作对信号量的值加1,并唤醒那些等待的进线程 • 信号量的物理含义 • 当信号量S.value>0时,表示有S.value个可用资源 • 当信号量S.value=0时,表示所有资源被用,但无线程等待 • 当信号量S.value<0时,表示所有资源被用,且还有|S.value|个线程在等待资源 P(mutex); sum++;V(mutex);
互斥与同步 • 同步原语 • 信号量 • 信号量用于互斥 • 一个单向的独木桥,一次只能走一个人,若用进程表示每个人,用P、V操作给出各人的过桥过程 • 一个售票厅只能容纳300人,当少于300人时可以进入,否则,需在外等候,若将每一个购票者作为一个进程,请用P、V操作表示该购票过程。
互斥与同步 • 同步原语 • 信号量 • 信号量用于同步 • 生产者-消费者问题(缓冲区为空,消费者不能再消费,缓冲区为满,生产者不能再生产) • 一个生产者,一个消费者,公用一个缓冲区 • 一个生产者,一个消费者,公用n个环形缓冲区 • 多个生产者,多个消费者,公用n个环形缓冲区 • 桌上有一空盘,允许存放一只水果。爸爸可向盘中放苹果或者桔子,儿子专等吃盘中的桔子,女儿专等吃盘中的苹果。规定当盘空时一次只能放一只水果供吃者取用,请用P、V操作实现爸爸、儿子、女儿三个并发进程的同步。
互斥与同步 Thread A …… mutex lock(); sum=sun+1;mutex unlock(); Thread B …… mutex lock(); sum=sun*2;mutex unlock(); • 同步原语 • 互斥量(锁) • 锁类似于信号量,但在一个实例中只有一个线程能操作锁。也就是说锁实际上是特殊的信号量,其资源只有1个。 • 锁上的两个基本的原子操作: • Acquire()或lock():以原子形式等待锁状态到“开锁”,等到后进临界区操作,并设置锁状态为“上锁”。 • Release()或unlock(): 以原子状态改变锁状态从“上锁”到“开锁”。
互斥与同步 • 同步原语 • 事件 • 事件(Event)是WIN32提供的最灵活的线程间同步方式。 • 事件存在两种状态: • 激发状态(signaled or true) • 未激发状态(unsignal or false) • 事件可分为两类: • 人工重置:这种对象只能用程序来手动设置,在需要该事件或者事件发生时,采用SetEvent及ResetEvent来进行设置。 • 自动重置:一旦事件发生并被处理后,自动恢复到没有事件状态,不需要再次设置。
进程、线程和并行程序设计 • 内容 • 进程的概念 • 什么是线程 • 线程的设计 • 互斥与同步 • 并行程序设计的常见问题
并行程序设计的常见问题 • 更多的线程意味着更高的性能吗?
并行程序设计的常见问题 • 更多的线程意味着更高的性能吗? • 原因: • 线程启动和终止的代价掩盖了有用的工作 • 共享固有硬件资源的开销 • 频繁切换进程或线程容易引起Cache颠簸 • 切换线程本身有代价 • 有用的尝试 • 运行的线程数量最好低于等于硬件线程数 • 用OpenMP来做工作 • 使用线程池 • 任务窃取
并行程序设计的常见问题 • 竞争激烈的锁 • 优先级倒置 • 如果不是资源抢占式优先级,则有可能一个低优先级的线程占用了锁,而高优先级的线程等待并可能错过临界期限。 • 优先级倒置的解决办法 • 优先级继承 • 优先级顶置 • 竞争激烈的锁的解决方法 • 资源复制 • 将资源分区,并用一个独立的锁来保护每个分区 • 细粒度锁 • 读者-写者锁 • 同一时刻只有一个写者可以获得锁,但多个读者可以同时获得锁,注意可能造成写者被饿死
并行程序设计的常见问题 • 非阻塞算法 • 非阻塞算法具有的特征 • 无阻塞:只要没竞争,线程就可以持续执行 • 无锁:系统整体持续执行 • 无等待:每个线程都可以持续执行,哪怕遇到竞争 • 比较并交换 • CAS原语操作 • 内存位置V • 预期原值A • 新值B Public class NonblockingCounter { private AtomicInteger Value; Public int getValue() {return value.get()}; public int increment() { int v; do { v=value.get(); } while(!value.compareAndSet(v,v+1)); return v+1; } }
并行程序设计的常见问题 • 非阻塞算法 • ABA问题 • CAS原语操作的时候,如果有一个线程将该数字从A改到B又改回A,其他线程可能就感知不到这一变化,从而引起混乱。 • 不要重用A • 可以利用加值的版本号等方法来解决 • Cache行乒乓效应 • 由于cache行没有锁,所以多核线程使用同一行的话会引起强烈颠簸
并行程序设计的常见问题 • 非阻塞算法 • 内存回收问题 • 比如C语言的一个线程在回收一个指针的时候不知道是否有别的线程在用(因为没有锁) • 一些建议 • 直接使用原子增和原子减一般来说是安全的 • 对链状结构构造非阻塞算法要使用公认正确的算法
并行程序设计的常见问题 • 可重入函数 • 一个函数可能会被多个执行流并发访问,因此该函数需要是可重入的。 • 一个可重入函数在执行中不使用静态数据,不返回指向静态数据的指针。所有使用到的数据都有函数的调用者提供。 • 对于返回指向静态数据的指针的非可重入函数的改造 • 返回指向动态分配空间的指针,调用者负责释放资源,函数的参数不用修改,但这样不安全,所以不推荐。 • 使用由调用者提供的存储空间(推荐使用),要修改函数的参数 • 在连续的调用之间(由函数)保存信息的改造 • 由调用者负责保存
并行程序设计的常见问题 • 可重入函数 char *strtoupper(char *string) { static char buffer[MAX_STRING_SIZE]; int index; for(index=0;string[index];index++) buffer[index]=toupper(string[index]); buffer[index]=0; return buffer; } char *strtoupper_r( char *in_str, char *out_str ) { int index; for(index=0;in_str[index];index++) out_str[index]=toupper(in_str[index]); out_str[index]=0; return out_str; } 不可重入函数 可重入函数