Unix环境高级编程学习笔记(七) 多线程



线程概述

线程(thread)技术早在60年代就被提出,但真正应用多线程到操作系统中去,是在80年代中期,solaris是这方面的佼佼者。传统的Unix也支持线程的概念,但是在一个进程(process)中只允许有一个线程,这样多线程就意味着多进程。现在,多线程技术已经被许多操作系统所支持,包括Windows/NT,当然,也包括Linux。  

为什么有了进程的概念后,还要再引入线程呢?使用多线程到底有哪些好处?什么的系统应该选用多线程?我们首先必须回答这些问题。  

使用多线程的理由之一是和进程相比,它是一种非常"节俭"的多任务操作方式。我们知道,在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右,当然,在具体的系统上,这个数据可能会有较大的区别。  

使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。  

除了以上所说的优点外,不和进程比较,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:  

1) 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。  

2) 使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。  

3) 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。

一个线程所包含的信息呈现出了它在一个进程中的执行环境,它们包括线程ID,线程栈,时刻优先级和策略(a scheduling priority and policy),信号屏蔽字,error变量以及线程相关的特定数据(线程私有数据)。在一个进程中几乎所有的东西都是可以共享的,包括代码段,全局变量以及堆、栈,还包括文件描述符等。一个线程的线程ID是用于在进程中唯一确定的标识,和进程ID不同,它只有在该线程所在的进程中才有意义。

线程的创建

首先我们来看一下调用函数:

[cpp] view plaincopy
  1. int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr,   
  2.         void *(*start_rtn)(void), void *restrict arg);  


它的第一个参数是O类型的,用于获取新创建的线程ID,attr是线程属性,默认时可赋值为空,start_rtn是一个函数指针,线程创建成功后,该函数将作为线程的入口函数开始运行,arg是传递给该线程函数的参数。

新创建的线程有权访问它所在进程的地址空间,它将继承父线程的浮点环境(floating-point environment)以及信号屏蔽字,不过,已经处于阻塞队列中的信号将被清空。

新创建的线程默认将以分离模式运行,也就是说,当线程结束时,系统将自动回收其资源,并扔掉其结束状态。当然,我们也可以通过设置其线程属性来使线程以非分离模式运行,在此模式下,线程结束时,必须由其他线程调用join函数来释放其资源并获取其结束状态。

我们来看看线程属性的设置方式,以下两个函数用于初始化以及销毁线程属性结构体(并非释放内存):

[cpp] view plaincopy
  1. int pthread_attr_init(pthread_attr_t *attr);// 将属性结构体初始化为默认值:  
  2. int pthread_attr_destroy(pthread_attr_t *attr);  

这样,在初始化后,属性结构体就被初始化为默认值,下面的函数可用于获取以及设置线程的分离属性:

[cpp] view plaincopy
  1. int pthread_attr_getdetachstate(const pthread_attr_t *restrict attr, int *detachstate);  
  2. int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);  


在设置时,第二个参数分离状态只能是这两种常量:PTHREAD_CREATE_DETACHED(分离模式)和PTHREAD_CREATE_JOINABLE。

当然,我们也可以在运行期间修改线程的分离属性,以下函数可以在线程运行时将其改变为分离模式:

[cpp] view plaincopy
  1. int pthread_detach(pthread_t thread);  

线程属性不止用于分离属性,我们知道,由于所有的线程都使用同一个地址空间,每一个线程栈的大小就受到了限制,根据应用的业务逻辑,我们可能需要修改一个线程的栈的默认大小,例如,当存在的线程过多,我们就希望它的栈能够小一点,而如果某个线程将要执行的业务逻辑需要进行很深的递归调用,我们就希望其运行栈能够大一点。通过修改属性的方式也能对这些进行设置:

[cpp] view plaincopy
  1. int pthread_attr_getstack(const pthread_attr_t *restrict attr,   
  2.     void **restrict stackaddr, size_t *restrict stacksize);  
  3. int pthread_attr_setstack(const pthread_attr_t *attr,   
  4.     void *stackaddr, size_t *stacksize);  

stackaddr是栈的首地址,stacksize则是栈的大小。当然,许多时候,我们不希望自己来处理内存的分配等事宜,也可以通过以下的函数只指定栈的大小:

[cpp] view plaincopy
  1. int pthread_attr_getstacksize(const pthread_attr_t *restrict attr,  
  2.     size_t *restrict stacksize);  
  3. int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);  

一个属性对象在被设置之后可以用于多个线程的创建,同时,当属性对象使用完毕后,我们再对属性对象的更改或是destroy都不会影响到那些已经创建完成的线程。


线程终止

以下是线程正常终止的三种方式:

1. 线程从线程入口函数处返回,其返回值将是该线程的终止状态。

2. 线程调用pthread_exit函数终止当前线程,该函数的参数将被作为终止状态。

3. 该线程被相同进程中的其他线程所取消。

由于任意一个线程调用exit系列函数都将终止整个进程,因此当我们只想要终止线程时可以使用pthread_exit函数:

[cpp] view plaincopy
  1. void pthread_exit(void *rval_ptr);  

如果线程被取消了,则其终止状态将是:PTHREAD_CANCELED。那么,我们该如何获得线程的终止状态呢?前面我们讲过线程的两种运行模式:分离模式和接合(join)模式。当处于分离状态终止时,其终止状态将自动被系统所舍弃,只有处于接合模式下,我们才能获得其终止状态,参看以下函数:

[cpp] view plaincopy
  1. int pthread_join(pthread_t threadvoid **rval_ptr);  

这个方法将阻塞直到它所指定的线程终止,参数rval_ptr是O类型的,用于获取线程的终止状态。

类似于atexit函数一样,我们也可以为线程提供清理函数,以下是线程清理函数的注册函数;

[cpp] view plaincopy
  1. void pthread_cleanup_push(void (*rtn)(void *), void *arg);  
  2. void pthread_cleanup_poppthread_cleanup_pop(int execute);  

这两个函数分别用于增加与删除清理函数,清理函数可以不止一个,其组织形式按照栈的结构组织,所以增加删除都是在栈顶操作,并且其调用顺序也是与其添加顺序相反的。出口函数在以下三种情况下会被调用:

1. 调用pthread_exit函数

2. 响应对线程的取消请求

3. 使用非0参数调用pthread_cleanup_pop函数。

从上面可以看出来,有一点也许会另我们意外,那就是,当线程从入口函数处正常返回时,清理函数并不会得到调用。

pthread_cleanup_push用于增加清理函数,我们来看一下pthread_cleanup_pop函数,它的作用是删除最后一个被注册的清理函数,如果其调用参数非0,那么在删除该清理函数的同时,它也将得到调用。有一点需要注意的是,由于pthread_cleanup_push和pthread_cleanup_push可能被宏来实现,所以我们必须成对的使用它们,否则会报编译错误。下面是linux的实现方式:

[cpp] view plaincopy
  1. #  define pthread_cleanup_push(routine, arg) \  
  2. do {                                        \  
  3. __pthread_cleanup_class __clframe (routine, arg)  
  4.   
  5. #  define pthread_cleanup_pop(execute) \  
  6. __clframe.__setdoit (execute);                        \  
  7. while (0)  

线程取消(cancellation)

[cpp] view plaincopy
  1. int pthread_cancel(pthread_t tid);  

该函数的默认效果是取消tid线程,这使得该线程仿佛自己调用了pthread_exit,使用PTHREAD_CANCELED作为其结束状态。不过实际上,一个线程不必马上对该取消请求进行响应,甚至可以忽略该请求。

在默认情况下,只有当线程运行到取消点(cancellation point)时才会对取消请求进行响应,一个取消点是指线程检查其是否已经被取消的地方,POSIX.1定义了如下的一些函数作为取消点,当这些函数被调用时,将检查是否被取消,从而作出响应:


当然,还有一些其他的函数也有可能作为取消点,不过那些是可选的,依照具体的实现而不同,不具备可移植性。实际上,我们也可以自己定义取消点。请看下面的函数:

[cpp] view plaincopy
  1. void pthread_testcancel(void);  

当取消请求发生时,默认情况下,它会被阻塞,直到线程对它进行响应,该函数调用时,如果已有取消请求被阻塞住,并且线程取消功能并没有被关闭(这个等会儿解释)的话,该线程将被取消。

前面说的这些都是默认操作,实际上我们也可以修改取消的状态和类型。先说取消类型(cancellation type),通过对该属性进行设置可以决定线程是否在取消点才被取消,请看函数声明:

[cpp] view plaincopy
  1. int pthread_setcanceltype(int type, int *oldtype);  

type参数只能是如下常量之一:PTHREAD_CANCEL_DEFERRED(这个是默认的) or PTHREAD_CANCEL_ASYNCHRONOUS。使用第一个常量,则线程只有到取消点才会检查取消请求,但后者则决定线程可以在任何时候被取消,不必等到取消点。oldtype是一个O类型参数,用于获取历史取消类型。

我们也可以修改其取消状态使线程忽略其他线程的取消请求,使用如下函数:

[cpp] view plaincopy
  1. int pthread_setcancelstate(int state, int *oldstate);  

通过此函数可以设置线程是否对线程取消进行响应,state只能是如下常量之一:PTHREAD_CANCEL_ENABLE 或是 PTHREAD_CANCEL_DISABLE。需要注意的是,当取消状态被设置为PTHREAD_CANCEL_DISABLE时,取消请求并没有被舍弃,它只是被阻塞住了,直到当该功能再此被启用时,如果有阻塞的取消请求,线程将会被取消。


多线程下的信号量机制

每一个线程都有它自己的信号量屏蔽字,但是信号处理方式(signal disposition)却是在进程内共享的。如果一个信号量是有硬件错误或是时钟到点导致的,那么该信号将被发送给发生这些事件的线程,而如果不是这些情况引发的信号,它们将被发送给该进程下的任意一个线程。在多线程环境下使用sigprocmask函数修改信号屏蔽字的行为是未定义的,我们应该使用另外一个函数代替,那就是pthread_sigmask。

[cpp] view plaincopy
  1. int pthread_sigmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);  

他的使用方式和sigprocmask函数是一致的,这里就不再多作讨论了。

在多线程环境下,我们通常可以指定某个特定的线程来专门完成信号处理的工作,从而可以防止因其他工作线程被打断而引发的异常情况,先来看一个函数:

[cpp] view plaincopy
  1. int sigwait(const sigset_t *restrict set, int *restrict signop);  


set参数的类型是我们前面将信号机制时所提到过的信号集,在这里它被用来指定我们想要处理的信号,而第二个参数是O类型参数,当该函数返回时,它存有我们实际接收到的信号number。当该函数调用后,线程会被阻塞,直到有它所等待的信号发生(实际上,该函数也是可能会被其他信号所中断的)。如果在调用时,已经有它要等待的信号在阻塞队列里了,那么该函数将立即返回,而不需要再阻塞。在返回之前,sigwait函数将移除掉阻塞队列中它所等待的信号。为避免可能出现的空档,造成错误的信号处理行为,线程在调用sigwait函数之前应该先把它要等待的信号给阻塞调。而在掉用sigwait函数的时候,它将自动unblock这些信号并开始等待直到那些信号中的一个被交付。在该函数返回以前,这些被unblock了的信号会被再次自动恢复阻塞。如果多个线程在调用wigwait时等待了相同的信号,当该信号发生后,只有一个线程会获得该信号并从阻塞中返回。

在linux的实现中,必须要注意的是,由于linux中实际上没有真正的线程,它所谓的线程实际上只是一个轻量级的进程,所以,当信号发生时,如果主线程没有阻塞这个信号,其他线程是sigwait不到那个信号的。因此,在使用sigwait函数时,我们最好让其他线程都把那些信号都给阻塞住。

实际上,我们也可以将信号发送给某个特定的线程,类似于kill,其函数声明如下:

[cpp] view plaincopy
  1. int pthread_kill(pthread_t threadint signo);  

该函数的作用就是向指定的线程发送指定的信号量,这里有一个小技巧,当我们指定信号量为0时,该函数可以用来检测该线程是否存在。如果接受到信号的线程对信号的默认操作是终止进程的话,那么整个进程都将被终止。

对于信号量,还有一点值得注意的是,alarm timers是属于进程的资源,所有的线程都共享了同一个alarm,所以,对于同一个进程中的多个线程来说,不用担心其他线程的干扰而使用alarm timers的方法是不存在的。

参考文献

《Linux下的多线程编程》 姚继锋

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/352605.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

testflight开发者已将您从测试计划中移除_使用 TestFlight 测?试 App

目前市面上对于iOS开发的签名样式大致分为三种: a、企业签 依赖于苹果企业级开发者账号b、超级签 依赖于苹果个人开发者账号c、tf签 TestFlight Beta 版测试让您可以分发您 App 的 Beta 版构建版本给测试员并收集反馈。您可以在您的 App Store Connect 帐户中一次为…

MFC操作ini文件方法

转自:http://blog.csdn.net/rayborn1105/article/details/8192142 一个不错的接口:http://blog.csdn.net/qq575787460/article/details/8185339 在我们的程序设计中经常需要对一些参数进行配置,配置好后还要在下一次启动仍然有效,…

Java 8流中的数据库CRUD操作

在开始使用新工具时要克服的最大障碍是让您着手处理小事情。 到目前为止,您可能对新的Java 8 Stream API的工作方式充满信心,但是您可能尚未将其用于数据库查询。 为了帮助您开始使用Stream API创建,修改和读取SQL数据库,我整理了…

网络时间同步

linux yum install ntp ntpdate -y 时间同步命令:ntpdate time.windows.com 开机启动: chkconfig ntpd on 查看开机启动:chkconfig --list ntpd ntpd 0:off 1:off 2:on 3:on 4:on 5:on 6:off 转载于:https://www.cnblogs.com/…

java 权限url权限_SpringBootSecurity学习(11)网页版登录之URL动态权限

动态权限前面讨论用户登录认证的时候,根据用户名查询用户会将用户拥有的角色一起查询出来,自动实现判断当前登录用户拥有哪些角色。可以说用户与角色之间的动态配置和判断security做的非常不错。不过在配置方法级别的权限的时候,使用注解虽然…

线程使用

嵌入式中线程应用还是看需求,一般不常用(在不会使用的情况下)一、编译有线程的应用程序需要编译时指定编译lib库 ( -l pthread) 如:gcc main.c -o main -l pthread 才能编译通过。二、线程使用。1、线程运行…

C++ MFC string转Cstring为什么会乱码

前段时间学习mfc编了一个小程序,其中涉及到CString 与string的转换的时候感觉特别蛋疼,因此再此总结一下经验。希望能够对大家能有所帮助 通常有两种字符集模式 unicode字符集 和 ascii字符集,其中unicode有多种编码方式。utf8, …

metaq原理简介

1. 前言 本文档旨在描述RocketMQ的多个关键特性的实现原理,并对消息中间件遇到的各种问题进行总结,阐述RocketMQ如何解决这些问题。文中主要引用了JMS规范与CORBA Notification规范,规范为我们设计系统指明了方向,但是仍有不少问题…

3d展示网页开发_超实用:一篇文章带你了解市面上主流通用的3D模型格式

说到格式,相信大家都不陌生。随着互联网的普及,我们几乎每天都会和不同的格式打交道,文本的TXT、图片的JPG、视频的MP4,就连压缩包也有不同的格式。通俗来说,你可以把“格式”理解成基于同一规范的技术表征&#xff0c…

java三件套_Java开发人员应该知道的三件事

java三件套对于那些长期关注JavaOne 2012会议的读者来说,这是一篇有趣的文章。 我最近对Java冠军Heinz Kabutz的采访引起了我的注意; 包括他的Java内存难题程序,从Java内存管理的角度来看,这很有启发性。 采访中有一个特别的部分吸…

Python 的变量作用域和 LEGB 原则

在 Python 程序中创建、改变或查找变量名时,都是在一个保存变量名的地方进行中,那个地方我们称之为命名空间。作用域这个术语也称之为命名空间。 具体地说,在代码中变量名被赋值(Python 中变量声明即赋值,global 声明的…

让MessageBox在最前面弹出来的信息在

nt MessageBox( HWND hWnd, // handle of owner window LPCTSTR lpText, // address of text in message box LPCTSTR lpCaption, // address of title of message box UINT uType // style of message box ); 在uType参数中设置MB_SYSTEMMODAL就能让MessageBox在最前面 消息框…

Confluence 6 管理协同编辑 - 关于 Synchrony

协同编辑能够让项目小组中的协同合作达到下一个高度。这个页面对相关协同编辑中的问题进行了讨论,能够提供给你所有希望了解的内容。 进入 Collaborative editing 页面来获得项目小组是如何进行实时的协同工作的,这些协同工作包括在软件需求,…

如何初始化局部变量c语言_【C语言更新】C语言中如何来定义一个指针,并且对其进行初始化...

文/Edward上一节中,我们利用了一个小知识来介绍了一下指针,在上面这个例子中,小明和小丽手中的这个杯子就好比我们C语言中的变量,它确实是实实在在存放一些有具体意义的数据。这个杯套就类似于C语言中指针的作用,假设我…

Spring Environment仅用于初始化代码

从3.1版开始, Spring框架提供了对几种不同来源的抽象,通过它们您可以轻松地配置应用程序: Environment 。 在这篇文章中,我描述了一个微基准测试,我可以证明该基准测试是一个方便的API(如果您在应用程序中…

linux查看和关闭后台执行程序

后台执行的程序会有一个pid,查看后台程序主要用到jobs,ps 关闭后台程序用到kill详细说明转自http://blog.csdn.net/tianlesoftware/article/details/6165753 一. 有关进程的几种常用方法 1.1 & 符号 在命令后面加上一个 & 符号,表示该命令放在后台执行&…

Mysql 多表联合查询效率分析及优化

1. 多表连接类型 1. 笛卡尔积(交叉连接) 在MySQL中可以为CROSS JOIN或者省略CROSS即JOIN,或者使用, 如: SELECT * FROM table1 CROSS JOIN table2 SELECT * FROM table1 JOIN table2 SELECT * FROM table1,table2 由于其返回的结果为被连接的两…

Python的__getattr__方法学习

内容部分来自网络 __getattr__函数的作用: 如果属性查找(attribute lookup)在实例以及对应的类中(通过__dict__)失败, 那么会调用到类的__getattr__函数; 如果没有定义这个函数,那么抛出Attribu…

python to_excel新增sheet_Python使用xlrd和xlwt读写Excel的简单用法

前言数据处理是 Python 的一大应用场景,而 Excel 则是最流行的数据处理软件。因此用Python 进行数据相关的工作时,难免要和 Excel 打交道。标准的 Excel 文件(xls/xlsx)具有较复杂的格式,并不方便像普通文本文件一样直…

linux 线程管理、同步机制等

线程学了那么多有关进程的东西,一个作业从一个进程开始,如果你需要执行其他的东西你可以添加一些进程,进程之间可以通信、同步、异步。似乎所有的事情都可以做了。 对的,进程是当初面向执行任务而开发出来的,每个进程代…