logo资料库

Unix_Linux_Windows_OpenMP多线程编程.pdf

第1页 / 共105页
第2页 / 共105页
第3页 / 共105页
第4页 / 共105页
第5页 / 共105页
第6页 / 共105页
第7页 / 共105页
第8页 / 共105页
资料共105页,剩余部分请下载后查看
第三章 Unix/Linux 多线程编程 [引言]本章在前面章节多线程编程基础知识的基础上,着重介绍 Unix/Linux 系统下的多线 程编程接口及编程技术。 3.1 POSIX 的一些基本知识 POSIX 是可移植操作系统接口(Portable Operating System Interface)的首字母缩写。 POSIX 是基于 UNIX 的,这一标准意在期望获得源代码级的软件可移植性。换句话说,为一 个 POSIX 兼容的操作系统编写的程序,应该可以在任何其它的 POSIX 操作系统(即使是来自 另一个厂商)上编译执行。POSIX 标准定义了操作系统应该为应用程序提供的接口:系统调 用集。POSIX 是由 IEEE(Institute of Electrical and Electronic Engineering)开发的, 并由 ANSI(American National Standards Institute)和 ISO(International Standards Organization)标准化。大多数的操作系统(包括 Windows NT)都倾向于开发它们的变体 版本与 POSIX 兼容。 POSIX 现在已经发展成为一个非常庞大的标准族,某些部分正处在开发过程中。表 1-1 给 出了 POSIX 标准的几个重要组成部分。POSIX 与 IEEE 1003 和 2003 家族的标准是可互换 的。除 1003.1 之外,1003 和 2003 家族也包括在表中。 1003.0 管理 POSIX 开放式系统环境(OSE)。IEEE 在 1995 年通过了这项标准。 ISO 的 版本是 ISO/IEC 14252:1996。 1003.1 被广泛接受、用于源代码级别的可移植性标准。1003.1 提供一个操作系统的 C 语 言应用编程接口(API)。IEEE 和 ISO 已经在 1990 年通过了这个标准,IEEE 在 1995 年重新修订了该标准。 1003.1b 一个用于实时编程的标准(以前的 P1003.4 或 POSIX.4)。这个标准在 1993 年被 IEEE 通过,被合并进 ISO/IEC 9945-1。 1003.1c 一个用于线程(在一个程序中当前被执行的代码段)的标准。以前是 P1993.4 或 POSIX.4 的一 部 分 , 这个 标 准 已 经在 1995 年被 IEEE 通过 , 归 入 ISO/IEC 9945-1:1996。 1003.1g 一个关于协议独立接口的标准,该接口可以使一个应用程序通过网络与另一个应用 程序通讯。 1996 年,IEEE 通过了这个标准。 1003.2 一个应用于 shell 和工具软件的标准,它们分别是操作系统所必须提供的命令处 理器和工具程序。 1992 年 IEEE 通过了这个标准。ISO 也已经通过了这个标准 (ISO/IEC 9945-2:1993)。
1003.2d 改进的 1003.2 标准。 1003.5 一个相当于 1003.1 的 Ada 语言的 API。在 1992 年,IEEE 通过了这个标准。并 在 1997 年对其进行了修订。ISO 也通过了该标准。 1003.5b 一个相当于 1003.1b(实时扩展)的 Ada 语言的 API。IEEE 和 ISO 都已经通过 了这个标准。ISO 的标准是 ISO/IEC 14519:1999。 1003.5c 一个相当于 1003.1q(协议独立接口)的 Ada 语言的 API。在 1998 年, IEEE 通 过了这个标准。ISO 也通过了这个标准。 1003.9 一个相当于 1003.1 的 FORTRAN 语言的 API。在 1992 年,IEEE 通过了这个标准, 并于 1997 年对其再次确认。ISO 也已经通过了这个标准。 1003.10 一个应用于超级计算应用环境框架(Application Environment Profile,AEP)的 标准。在 1995 年,IEEE 通过了这个标准。 1003.13 一个关于应用环境框架的标准,主要针对使用 POSIX 接口的实时应用程序。在 1998 年,IEEE 通过了这个标准。 1003.22 一个针对 POSIX 的关于安全性框架的指南。 1003.23 一个针对用户组织的指南,主要是为了指导用户开发和使用支持操作需求的开放式 系统环境(OSE)框架 2003 针对指定和使用是否符合 POSIX 标准的测试方法,有关其定义、一般需求和指导 方针的一个标准。在 1997 年,IEEE 通过了这个标准。 2003.1 这个标准规定了针对 1003.1 的 POSIX 测试方法的提供商要提供的一些条件。在 1992 年,IEEE 通过了这个标准。 2003.2 一个定义了被用来检查与 IEEE 1003.2(shell 和 工具 API)是否符合的测试方 法的标准。在 1996 年,IEEE 通过了这个标准。 表 3.1 POSIX 标准的重要组成部分 本章将重点讲述“POSIX 线程”,即符合 POSIX 国际正式标准 POSIXl003.1c-1995 的部分。 本章假定用户使用的编程语言为 ANSI C 语言。 3.2 POSIX 线程库 首先,在编写 POSIX 多线程 C 程序时,需要包含头文件’pthread.h’。POSIX 线程函数都 以’pthread_’开头。在本章中,我们将介绍一下线程操作函数: POSIX 函数 pthread_cancel pthread_create pthread_detach pthread_equal pthread_exit 2 描述 终止另一个线程 创建一个线程 设置线程以释放资源 测试两个线程 ID 是否相等 退出线程,而不退出进程
pthread_join pthread_self 表 3.2 POSIX 线程管理函数 3.2.1 创建线程 等待一个线程 找出自己的线程 ID const pthread_attr_t* restrict attr, void *(*start_routine)(void *), void *restrict arg); int pthread_create(pthread_t *restrict thread, ‘pthread_create’ 函数创建一个线程。 参数 thread 指向保存线程 ID 的 pthread_t 结构。参数 attr 表示一个封装了线程的各种属 性的属性对象,用来配置线程的运行,如果为 NULL,则使新线程具有默认的属性。线程属 性将在后面的 XX 节讨论。第三个参数 start_routine 是线程开始执行的时候调用的函数的 名字。这个函数必须具有以下的格式: void* start_routine(void* arg); 返回的 void 指针将被 pthread_join 函数当做退出状态来处理。第四个参数 arg 正是传递给 start_routine 函数的参数。POSIX 的 pthread_create 函数会使创建的线程自动处于可运行 状态,而不需要一个单独的启动操作。 如果成功,pthread_create 返回 0,如果不成功,pthread_create 返回一个非零的错误码。 下表列出了 pthread_create 的错误形式及相应的错误码 错误 EAGAIN EINVAL EPERM 原因 系统没有创建线程所需的资源,或者创建线 程会超出系统对一个进程中线程总数的限制 attr 参数是无效的 调用程序没有适当的权限来设定调度策略或 attr 指定的参数 表 3.3 pthread_create 的错误形式及相应的错误码 每一个线程可以通过调用函数 pthread_self 得到本线程的 ID(数据结构类型:pthread_t), 它的形式为: 由于 pthread_t 可能是一个结构,因此 POSIX 提供了一个函数 pthread_equal 来比较线程 ID 是否相等。这个函数的形式为: pthread_t pthread_self(void); int pthread_equal(pthread_t t1, pthread_t t2); 3
两个参数 t1 和 t2 是两个线程 ID,如果它们相等,pthread_equal 就返回一个非零值,如果 不相等,则返回 0。 3.2.2 分离(Detach)和接合(Join)线程 int pthread_detach(pthread_t thread); POSIX 线程的一个特点是:除非线程是被分离了的,否则在线程退出时,它的资源是不会被 释放的。pthread_detach 函数用来分离线程: 它设置线程的内部选项来说明线程退出后,其所占有的资源可以被回收。参数 thread 是要 分离的线程的 ID。被分离的的线程退出时不会报告它们的状态。如果函数调用成功, pthread_detach 返回 0,如果不成功,pthread_detach 返回一个非零的错误码。下表列出 了 pthread_detach 的错误形式及相应的错误码 错误 EINVAL ESRCH 原因 thread 对应的不是一个可分离的线程. 没有 ID 为 thread 的线程 表 3.4 ‘pthread_detach’的错误形式及相应的错误码 pthread_join 函数可以使调用这个函数的线程等待指定的线程运行完成再继续执行。它的 形式为: int pthread_join(pthread_t thread, void **value_ptr); 参数 thread 为要等待的线程的 ID,参数 value_ptr 为指向返回值的指针提供一个位置,这 个返回值是由目标线程传递给 pthread_exit 或 return 的。如果 value_ptr 为 NULL,调用 程序就不会对目标线程的返回状态进行检索了。如果函数调用成功,pthread_join 返回 0, 如果不成功,pthread_join 返回一个非零的错误码。下表列出了 pthread_join 的错误形式 及相应的错误码 错误 EINVAL ESRCH 原因 thread 对应的不是一个可接合的线程 没有 ID 为 thread 的线程 表 3.5 pthread_join 的错误形式及相应的错误码 如果线程没有被分离,并且执行 pthread_join(pthread_self()),那么该线程将被一直挂 起,因为这条语句造成了死锁。有些 POSIX 的实现可以检测到死锁,并迫使 pthread_join 带着错误 EDEADLK 返回,但是,POSIX 并不要求一定要进行这种检测。 4
3.2.3 退出和取消线程 进程的终止可以通过在主函数 main()中直接调用 exit、return、或者通过进程中的任何其 它线程调用 exit 来实现。在任何一种情况下,该进程的所有线程都会终止。如果主线程在 创建了其它线程之后没有工作可做,它就应该阻塞到所有线程都结束为止,或者应该调用 pthread_exit(NULL)。 有时程序不必等待线程执行完成,这时程序需要使线程中途退出。POSIX 线程库提供了两个 撤销线程的函数 pthread_exit 和 pthread_cancel。下面对这两个函数分别进行介绍。 pthread_exit 函数可以使调用这个函数的线程中止运行,并且允许线程传递一个指针,这 个指针可以用来指向线程的返回值。它的形式为: void pthread_exit(void *value_ptr); 连接了这个线程可以获得参数 value_ptr 的值。回顾前面介绍的 pthread_join 函数,这个 函数的参数 void **value_ptr,正是保存 pthread_exit 函数的参数 void *value_ptr 的地 址。这里要注意,pthread_exit 的参数 value_ptr 必须指向线程退出后仍然存在的数据。 POSIX 没有为 pthread_exit 定义任何错误。 POSIX doesn't define any error code for ‘pthread_exit’. 线程也可以通过取消机制迫使其它的线程退出。线程可以调用函数 pthread_cancel 来请求 取消另一个线程。这个函数的形式是: int pthread_cancel(pthread_t thread); 参数 thread 是要取消的目标线程的线程 ID。该函数并不阻塞调用线程,它发出取消请求后 就返回了。如果成功,pthread_cancel 返回 0,如果不成功,pthread_cancel 返回一个非 零的错误码。 线 程 收 到 一 个 取 消 请 求 时 会 发 生 什 么 情 况 取 决 于 它 的 状 态 和 类 型 。 如 果 线 程 处 于 PTHREAD_CANCEL_ENABLE 状态,它就接受取消请求,如果线程处于 PTHREAD_CANCEL_DISABLE 状态,取消请求就会被保持在挂起状态。默认情况下,线程处于 PTHREAD_CANCEL_ENABLE 状态。 pthread_setcancelstate 函数用来改变调用线程的取消状态,它的形式为: int pthread_setcancelstate(int state, int *oldstate); 参数 state 表示要设置的新状态,参数 oldstate 为一个指向整形的指针,用于保存线程以 前的状态。如果成功,该函数返回 0,如果不成功,它返回一个非 0 的错误码。通常情况下, 线程函数在改变了线程的取消状态之后,应该在执行完某些操作之后恢复线程的取消状态, 5
否则,对于其它可能取消该线程的线程而言,取消操作的结果将无法预测,这很可能不利于 程序的正确执行。 当线程将退出作为对取消请求的响应时,取消类型允许线程控制它在什么地方退出。当它的 取消类型为 PTHREAD_CANCEL_ASYNCHRONOUS 时,线程在任何时候都可以响应取消请求。当它 的取消类型为 PTHREAD_CANCEL_DEFERRED 时,线程只能在特定的几个取消点上响应取消请 求。在默认情况下,线程的类型为 PTHREAD_CANCEL_DEFERRED。 pthread_setcanceltype 函数用来修改线程的取消类型。它的形式为: int pthread_setcanceltype(int type, int *oldtype); 参数 type 指定线程的取消类型,参数 oldtype 用来指定保存原来的取消类型的地址。如果 成功,该函数返回 0,如果不成功,它返回一个非 0 的错误码。 线程可以通过调用 pthread_testcancel 在代码中的特定的位置上设置一个取消点。当类型 为 PTHREAD_CANCEL_DEFERRED 的线程到达这样一个取消点时,就接受挂起的取消请求。该函 数的形式为: void pthread_testcancel(void); 3.2.4 用户级线程与内核级线程 用户级线程(user-level thread)和内核级线程(kernel-level thread)是两种传统的线程控 制模式。用户级线程通常都运行在一个现存的操作系统之上。这些线程对内核来说是不可见 的,它们被封装在进程里,并竞争分配给进程的资源。线程由一个线程运行系统来调度,这 个系统是进程代码的一部分。带有用户级线程的程序通常会连接到一个特殊的库上去,这个 库中的每个库函数都用外套(jacket)包装起来。在调用被外套包装的库函数之前,外套函数 要调用线程运行系统来进行线程管理,在调用了被外套包装的库函数之后,外套函数可能也 要进行这样的操作。 这样做的必要性是为了解决下面的情况:由于 read 或 sleep 这样的函数可能会使进程阻塞, 所以它们给用户级线程带来了一个问题,那就是要避免某个线程在调用这些阻塞型函数之 后,整个进程被阻塞。这就要求用户级线程库用一个无阻塞的版本来替换每一个外套包装的、 潜在的阻塞型调用。线程运行系统通过测试来查看调用是否会使线程阻塞,如果调用不会阻 塞,运行系统就立即进行调用,但是,如果调用会阻塞,运行系统就会将线程放在一个等待 线程的列表中,将调用添加到一个动作列表中,以便稍后再试,然后挑选另一个线程来运行。 所有这些控制过程对用户和操作系统来说都是不可见的。 用户级线程的开销很低,但是它们也有些缺点。用户线程模型假定线程运行系统最终会重新 获得控制权,这可能会受到 CPU 绑定线程(CPU-bound thread)的阻碍。CPU 绑定线程很少执 行库函数调用,这样就会阻止线程运行系统重新获得控制权来调度其它的线程。程序员必须 要显式地迫使 CPU 绑定线程在适当的地方放弃对 CPU 的控制,以避免出现封锁状态。第二个 问题是,用户级线程只能共享分配给它们的封装进程的处理器资源。因为线程一次只能运行 6
在一个处理器上,这种约束限制了可用的并行总量。使用线程的主要原因之一就是要利用多 处理器工作站的优势,所以仅使用用户级线程本身并不是一种能让人接受的方法。 对内核级线程来说,内核了解每一个作为可调度实体的线程,这些线程可以在全系统范围内 竞争处理器资源。内核级线程的调度开销可能和进程自身的调度差不多昂贵,但是,内核级 线程可以利用多处理器的优势。内核级线程的同步和数据共享比整个进程的同步和数据共享 的开销要低一些,但内核级线程的管理比用户级线程的管理代价更高。 还有一种模型,称作混合线程模型(hybrid thread model),它通过提供两个级别的控制, 同时具备了用户级和内核级模型的优点。用户用用户级线程编写程序,然后说明有多少个内 核可调度实体与这个进程相关。运行时,将用户级线程映射为系统的可调度实体,以实现并 行。用户拥有的映射控制级别取决于实现,例如在 Sun 的 Solaris 线程实现中,用户级线程 被称为线程,而内核可调度实体被称为轻量级进程(lightweight process)。用户可以指定 由一个特定的轻量级进程来运行制定的线程,或者由一个轻量级进程池来运行一组制定的线 程。 POSIX 线程调度模型是一个混合模型,它很灵活,足以在标准的特定实现中支持用户级和内 核级的线程。模型中包括两级调度——线程级和内核实体级。线程与用户级线程类似,内核 实体由内核调度。由线程库来决定它需要多少内核实体,以及它们是如何映射的。 POSIX 引入了一个线程调度竞争范围(thread-scheduling contention scope)的概念,这个 概念赋予了程序员一些控制权,使它们可以控制怎样将内核实体映射为线程。线程的 contentionscope 属性可以是 PTHREAD_SCOPE_PROCESS,也可以是 PTHREAD_SCOPE_SYSTEM。 带有 PTHREAD_SCOPE_PROCESS 属性的线程与它们所在的进程中的其它线程竞争处理器资源。 POSIX 没 有 说 明 这 样 一 个 线 程 怎 样 与 它 所 在 的 进 程 中 的 其 它 线 程 竞 争 , 因 此 PTHREAD_SCOPE_PROCESS 线程可以是严格的用户级线程,或者它们也可以使用某种更复杂的 方式映射到一个内核实体池中去。带有 PTHREAD_SCOPE_SYSTEM 属性的线程很像内核级线程, 他们在全系统范围内竞争处理器资源。POSIX 将 PTHREAD_SCOPE_SYSTEM 线程和内核实体之 间的映射留给具体实现来完成,但是一种明显的映射方式是,将这样一个线程直接与内核实 体 绑 定 起 来 。 POSIX 线 程 的 具 体 实 现 可 能 支 持 PTHREAD_SCOPE_PROCESS 、 或 PTHREAD_SCOPE_SYSTEM 或者两者都支持。 3.2.5 线程的属性 POSIX 将栈的大小和调度策略这样的特征封装到一个 pthread_attr_t 类型的对象中去,用 面向对象的方式表示和设置特征。属性对象只在线程创建的时候会对线程产生影响。编写程 序时可以先创建一个属性对象,然后再将栈的大小和调度策略这样的特征与属性对象关联起 来,之后就可以通过向 pthread_create 传递相同的线程属性对象来创建多个具有相同特征 的 线 程 。 通 过 将 各 种 特 征 组 合 到 单 个 对 象 中 去 , POSIX 避 免 了 用 大 量 参 数 来 调 用 pthread_create 的情况。 表 3.6 显示的是线程属性的可设置特征及其相关函数,后面我们将对这些特征和函数进行讨 论。 7
特征 属性对象 状态 栈 调度 函数 pthread_attr_destroy pthread_attr_init pthread_attr_getdetachstate pthread_attr_setdetachstate pthread_attr_getguardsize pthread_attr_setguardsize pthread_attr_getstack pthread_attr_setstack pthread_attr_getinheritsched pthread_attr_setinheritsched pthread_attr_getschedparam pthread_attr_setschedparam pthread_attr_getschedpolicy pthread_attr_setschedpolicy pthread_attr_getscope pthread_attr_setscope 表 3.6 线程属性的可设置特征及其相关函数 函数 pthread_attr_init 用默认值对一个线程属性对象进行初始化。pthread_attr_destroy 函数将属性对象的值设为无效的。被设为无效的属性对象可以再次被初始化为一个新的属性 对象。pthread_attr_init 和 pthread_attr_destroy 都只有一个参数,即一个指向属性对 象的指针。这两个函数的形式为: int pthread_attr_init(pthread_attr_t *attr); int pthread_attr_destroy(pthread_attr_t *attr); 如果成功,函数返回 0,如果不成功,函数返回一个非 0 的错误码。 大多数针对属性对象的函数都是获取或设置属性对象的属性。第一个参数是一个指向属性对 象的指针。对于获取操作,第二个参数是一个指向存放值的位置的指针,而对于设置操作, 第二个参数是属性的设置值。因此,后面读者可以根据函数参数的名称和类型推断出参数的 含义,我们就不一一介绍了。 3.2.5.1 线程状态 线 程 状 态 的 可 能 取 值 为 PTHREAD_CREATE_JOINABLE 和 PTHREAD_CREATE_DETACHED 。 pthread_attr_getdetachstate 函 数 用 来 查 看 一 个 属 性 对 象 中 的 线 程 状 态 , 而 pthread_attr_setdetachstate 函数用来设置一个属性对象中的线程状态。这两个函数的形 式为: int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate); int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate); 8
分享到:
收藏