多线程并发基础

多线程基础知识

多线程并发基础_第1张图片

多线程这个词可以被翻译为多个控制线程多个控制流程。虽然传统的UNIX进程总是包含并且仍包含单个控制线程,但多线程(MT)将进程分成许多执行线程,每个执行线程独立运行。

多线程代码可以

  • 提高应用响应能力

  • 更有效地使用多处理器

  • 改善计划结构

  • 使用较少的系统资源

本章介绍了一些多线程术语,优点和概念。

 

目录

定义多线程术语

满足多线程标准

受益于多线程

理解基本的多线程概念

 


定义多线程术语

 

表1-1介绍了本书中使用的一些术语。

 

表1-1多线程术语

 

术语 

定义 

处理 

使用fork(2)系统调用创建的UNIX环境(例如文件描述符,用户ID等),它被设置为运行程序。

线

在进程上下文中执行的一系列指令。 

pthreads(POSIX线程) 

符合POSIX 1003.1c标准的线程接口。 

Solaris线程 

Sun Microsystems TM线程接口,不符合POSIX标准。pthreads的前身。

单线程

限制对单个线程的访问。 

多线程

允许访问两个或多个线程。 

用户级或应用级线程

由用户(而不是内核)空间中的线程库例程管理的线程。 

轻量级流程

内核中执行内核代码和系统调用的线程(也称为LWP)。 

绑定线程

永久绑定到LWP的线程。 

未绑定的线程

缺省内核支持的上下文切换非常快速的Solaris线程。 

属性对象 

包含不透明的数据类型和相关的操作函数,用于标准化POSIX线程,互斥锁(互斥锁)和条件变量的一些可配置方面。 

互斥锁 

锁定和解锁对共享数据的访问的函数。 

条件变量 

阻止线程直到状态更改的函数。 

计算信号量

基于内存的同步机制。 

排比 

当至少两个线程同时执行时出现的情况。

并发 

如果至少有两个线程都存在的条件作出 的进展。一种更通用的并行形式,可以包括时间切片作为虚拟并行的一种形式。

 

满足多线程标准

多线程编程的概念至少可以追溯到20世纪60年代。它在UNIX系统上的开发始于20世纪80年代中期。虽然对多线程是什么以及支持它所需的功能达成了一致意见,但用于实现多线程的接口却有很大差异。

几年来,一个名为POSIX(便携式操作系统接口)1003.4a的小组一直在研究多线程编程的标准。该标准现已获得批准。此多线程编程指南基于POSIX标准:P1003.1b最终草案14(实时)和P1003.1c最终草案10(多线程)。

本指南涵盖POSIX线程(也称为pthreads)和Solaris线程。Solaris 2.4发行版中提供了Solaris线程,与POSIX线程在功能上没有区别。但是,由于POSIX线程比Solaris线程更具可移植性,因此本指南涵盖了POSIX透视图中的多线程。

 

受益于多线程

提高应用程序响应能力

可以重新设计任何许多活动不相互依赖的程序,以便将每个活动定义为一个线程。例如,多线程GUI的用户不必在启动另一个活动之前等待一个活动完成。

有效地使用多处理器

通常,使用线程表达并发要求的应用程序不需要考虑可用处理器的数量。使用额外的处理器可以透明地提高应用程序的性能。

具有高度并行性的数值算法和应用程序(例如矩阵乘法)在使用多处理器上的线程实现时可以更快地运行。

改善计划结构

许多程序更有效地构建为多个独立或半独立的执行单元,而不是单个单一线程。与单线程程序相比,多线程程序可以更好地适应用户需求的变化。

使用更少的系统资源

使用两个或多个通过共享内存访问公共数据的进程的程序正在应用多个控制线程。

但是,每个进程都具有完整的地址空间和操作环境状态。创建和维护大量状态信息的成本使得每个进程在时间和空间上都比线程更昂贵。

此外,进程之间固有的分离可能需要程序员在不同进程中的线程之间进行通信,或者同步其操作。

结合线程和RPC

通过组合线程和远程过程调用(RPC)包,您可以利用非共享内存多处理器(例如工作站集合)。这种组合相对容易地分发您的应用程序,并将工作站集合视为多处理器。

例如,一个线程可能会创建子线程。然后,这些孩子中的每一个都可以进行远程过程调用,在另一个工作站上调用过程。虽然原始线程只创建了现在并行运行的线程,但这种并行性涉及其他计算机。

理解基本的多线程概念

并发和并行

在单个处理器上的多线程进程中,处理器可以在线程之间切换执行资源,从而导致并发执行。

在共享内存多处理器环境中的同一多线程进程中,进程中的每个线程可以同时在单独的处理器上运行,从而实现并行执行。

当进程具有比处理器少或多的线程时,线程支持系统与操作环境一起确保每个线程在不同的处理器上运行。

例如,在具有相同数量的线程和处理器的矩阵乘法中,每个线程(和每个处理器)计算结果的一行。

看多线程结构

传统的UNIX已经支持线程的概念 - 每个进程都包含一个线程,因此使用多个线程进行编程时使用多个进程进行编程。但是进程也是一个地址空间,创建进程涉及创建新的地址空间。

与创建新进程相比,创建线程要便宜得多,因为新创建的线程使用当前进程地址空间。在线程之间切换所花费的时间远远少于在进程之间切换所花费的时间,部分原因是线程之间的切换不涉及在地址空间之间切换。

在一个进程的线程之间进行通信很简单,因为线程共享所有内容 - 特别是地址空间。因此,一个线程生成的数据立即可供所有其他线程使用。

多线程支持的接口是通过子例程库,用于POSIX线程的libpthread和用于Solaris线程的libthread。多线程通过解耦内核级和用户级资源来提供灵活性。

用户级线程

线程是多线程编程中的主要编程接口。用户级线程[命名用户级线程以区别于内核级线程,这只是系统程序员所关注的问题。因为本书适用于应用程序编程人员,所以不讨论内核级线程。]在用户空间中处理并避免内核上下文切换惩罚。应用程序可以拥有数百个线程,但仍然不会消耗许多内核资源。应用程序使用多少内核资源很大程度上取决于应用程序。

线程仅在进程内可见,它们共享所有进程资源,如地址空间,打开文件等。以下状态对于每个线程都是唯一的。

  • 线程ID

  • 注册状态(包括PC和堆栈指针)

  • 信号掩码

  • 优先

  • 线程私有存储

由于线程共享进程指令和大多数进程数据,因此进程中的其他线程可以看到一个线程对共享数据的更改。当一个线程需要与同一进程中的其他线程进行交互时,它可以在不涉及操作环境的情况下进行交互。

默认情况下,线程非常轻量级。但是,为了获得对线程的更多控制(例如,更多地控制调度策略),应用程序可以绑定线程。当应用程序将线程绑定到执行资源时,线程将成为内核资源

总而言之,用户级线程是:

  • 创建成本低,因为他们不需要创建自己的地址空间。它们是在运行时从地址空间分配的虚拟内存位。

  • 快速同步,因为同步是在应用程序级别完成的,而不是在内核级别完成的。

  • 由线程库轻松管理; 任一的libpthread或的libthread。

轻量级进程

线程库使用内核支持的称为轻量级进程的底层控制线程。您可以将LWP视为执行代码或系统调用的虚拟CPU。

您通常不需要关心LWP来使用线程进行编程。


注意 -

在Solaris 2,的Solaris 7和Solaris 8操作环境的轻量级进程是一样在SunOS所述的LWP TM 4.0 LWP库,其在Solaris 2,的Solaris 7支撑,和Solaris 8操作环境。


就像stdio库例程一样fopen()fread()使用open()read()函数,线程接口使用LWP接口,并且出于许多相同的原因。

轻量级进程(LWP)桥接用户级别和内核级别。每个进程包含一个或多个LWP,每个LWP运行一个或多个用户线程。(参见图1-1。)线程的创建通常只涉及创建一些用户上下文,而不是创建LWP。

图1-1用户级线程和轻量级进程


多线程并发基础_第2张图片

 

每个LWP都是内核池中的内核资源,并且基于每个线程分配(附加)和解除分配(分离)到线程。这是在调度或创建和销毁线程时发生的。

调度

POSIX指定了三种调度策略:先进先出(SCHED_FIFO),循环(SCHED_RR)和自定义(SCHED_OTHER)。SCHED_FIFO是一个基于队列的调度程序,每个优先级都有不同的队列。除了每个线程都有一个执行时间配额外,SCHED_RR与FIFO类似。

无论SCHED_FIFO和SCHED_RR是对POSIX Realtime的扩展。SCHED_OTHER是默认的调度策略。

 

有两个调度范围:未绑定线程的进程范围和绑定线程的系统范围。具有不同范围状态的线程可以在同一系统上共存,甚至可以在同一进程中共存。通常,作用域设置线程调度策略生效的范围。

进程范围(未绑定线程)

未绑定的线程创建PTHREAD_SCOPE_PROCESS。这些线程在用户空间中进行调度,以附加和分离LWP池中的可用LWP。LWP仅适用于此过程中的线程; 即在这些LWP上安排线程。

在大多数情况下,线程应为PTHREAD_SCOPE_PROCESS。这允许线程在LWP之间浮动,这样可以提高线程性能(相当于在THR_UNBOUND状态下创建Solaris线程)。线程库针对其他线程决定哪些线程由内核提供服务。

系统范围(绑定线程)

绑定线程创建PTHREAD_SCOPE_SYSTEM。绑定线程永久附加到LWP。

每个绑定线程在线程的生命周期内绑定到LWP。这相当于在THR_BOUND状态下创建Solaris线程。您可以绑定线程以为其提供备用信号堆栈,或者使用具有实时调度的特殊调度属性。所有调度都由操作环境完成。


注意 -

在任何情况下,绑定或未绑定,线程都可以直接访问或移动到另一个进程。


消除

线程取消允许线程终止进程中任何其他线程的执行。目标线程(被取消的线程)可以保留取消请求,并且可以在取消通知时执行特定于应用程序的清理。

pthreads取消功能允许线程的异步或延迟终止。异步取消可以随时进行; 延迟取消只能在定义的点上进行。延迟取消是默认类型。

同步

同步允许您控制程序流和对共享数据的访问,以便同时执行线程。

四种同步模型是互斥锁,读/写锁,条件变量和信号量。

  • 互斥锁 一次只允许一个线程执行特定的代码段,或访问特定的数据。

  • 读/写锁允许对受保护的共享资源进行并发读取和独占写入。要修改资源,线程必须首先获取独占写锁。在释放所有读锁之前,不允许使用独占写锁。

  • 条件变量 阻止线程直到特定条件为真。

  • 计算信号量通常协调对资源的访问。计数是有多少线程可以访问信号量的限制。达到计数时,信号量会阻塞。

锁的分类  


锁的类别有两种分法:  

1. 从数据库系统的角度来看:分为独占锁(即排它锁),共享锁和更新锁  

MS-SQL Server 使用以下资源锁模式。  

锁模式 描述  
共享 (S) :读锁,用于不更改或不更新数据的操作(只读操作),如 SELECT 语句。  
更新 (U) :(介于共享和排它锁之间),可以让其他程序在不加锁的条件下读,但本程序可以随时更改。

  
排它 (X):写锁。 用于数据修改操作,例如 INSERT、UPDATE 或 DELETE。确保不会同时同一资源进行多重更新。  
意向锁 用于建立锁的层次结构。意向锁的类型为:意向共享 (IS)、意向排它 (IX) 以及与意向排它共享 (SIX)。  
架构锁 在执行依赖于表架构的操作时使用。架构锁的类型为:架构修改 (Sch-M) 和架构稳定性 (Sch-S)。  
大容量更新 (BU) 向表中大容量复制数据并指定了 TABLOCK 提示时使用。  

共享锁 
共享 (S) 锁允许并发事务读取 (SELECT) 一个资源。资源上存在共享 (S) 锁时,任何其它事务都不能修改数据。一旦已经读取数据,便立即释放资源上的共享 (S) 锁,除非将事务隔离级别设置为可重复读或更高级别,或者在事务生存周期内用锁定提示保留共享 (S) 锁。  

更新锁 
更新 (U) 锁可以防止通常形式的死锁。一般更新模式由一个事务组成,此事务读取记录,获取资源(页或行)的共享 (S) 锁,然后修改行,此操作要求锁转换为排它 (X) 锁。如果两个事务获得了资源上的共享模式锁,然后试图同时更新数据,则一个事务尝试将锁转换为排它 (X) 锁。共享模式到排它锁的转换必须等待一段时间,因为一个事务的排它锁与其它事务的共享模式锁不兼容;发生锁等待。第二个事务试图获取排它 (X) 锁以进行更新。由于两个事务都要转换为排它 (X) 锁,并且每个事务都等待另一个事务释放共享模式锁,因此发生死锁。  

若要避免这种潜在的死锁问题,请使用更新 (U) 锁。一次只有一个事务可以获得资源的更新 (U) 锁。如果事务修改资源,则更新 (U) 锁转换为排它 (X) 锁。否则,锁转换为共享锁。  

排它锁 
排它 (X) 锁可以防止并发事务对资源进行访问。其它事务不能读取或修改排它 (X) 锁锁定的数据。  

意向锁 
意向锁表示 SQL Server 需要在层次结构中的某些底层资源上获取共享 (S) 锁或排它 (X) 锁。例如,放置在表级的共享意向锁表示事务打算在表中的页或行上放置共享 (S) 锁。在表级设置意向锁可防止另一个事务随后在包含那一页的表上获取排它 (X) 锁。意向锁可以提高性能,因为 SQL Server 仅在表级检查意向锁来确定事务是否可以安全地获取该表上的锁。而无须检查表中的每行或每页上的锁以确定事务是否可以锁定整个表。 

意向锁包括意向共享 (IS)、意向排它 (IX) 以及与意向排它共享 (SIX)。  

  

死锁原理

    根据操作系统中的定义:死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。

    死锁的四个必要条件:
互斥条件(Mutual exclusion):资源不能被共享,只能由一个进程使用。
请求与保持条件(Hold and wait):已经得到资源的进程可以再次申请新的资源。
非剥夺条件(No pre-emption):已经分配的资源不能从相应的进程中被强制地剥夺。
循环等待条件(Circular wait):系统中若干进程组成环路,该环路中每个进程都在等待相邻进程正占用的资源。

对应到SQL Server中,当在两个或多个任务中,如果每个任务锁定了其他任务试图锁定的资源,此时会造成这些任务永久阻塞,从而出现死锁;这些资源可能是:单行(RID,堆中的单行)、索引中的键(KEY,行锁)、页(PAG,8KB)、区结构(EXT,连续的8页)、堆或B树(HOBT) 、表(TAB,包括数据和索引)、文件(File,数据库文件)、应用程序专用资源(APP)、元数据(METADATA)、分配单元(Allocation_Unit)、整个数据库(DB)。一个死锁示例如下图所示:


    说明:T1、T2表示两个任务;R1和R2表示两个资源;由资源指向任务的箭头(如R1->T1,R2->T2)表示该资源被改任务所持有;由任务指向资源的箭头(如T1->S2,T2->S1)表示该任务正在请求对应目标资源;
    其满足上面死锁的四个必要条件:
(1).互斥:资源S1和S2不能被共享,同一时间只能由一个任务使用;
(2).请求与保持条件:T1持有S1的同时,请求S2;T2持有S2的同时请求S1;
(3).非剥夺条件:T1无法从T2上剥夺S2,T2也无法从T1上剥夺S1;
(4).循环等待条件:上图中的箭头构成环路,存在循环等待。

你可能感兴趣的:(高并发)