博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
24、线程控制
阅读量:6503 次
发布时间:2019-06-24

本文共 9198 字,大约阅读时间需要 30 分钟。

  线程有一套完整的与其有关的函数库可供调用,它们中的绝大多数函数名都以pthread_开头。为了调用这些函数库,必须在程序中包含头文件pthread.h,并且在比那一程序时使用选项-lpthread来链接线程库。

1、线程标识

  就像每个进程有一个进程ID一样,每个线程也有一个线程ID。进程ID在整个系统中是唯一的,但线程ID只有在它所属的进程上下文中才有意义。

  进程ID,用pid_t数据类型来表示,是一个非负整数。线程ID则用pthread_t数据类型来表示,实现的时候可以用一个结构来代表pthread_t数据类型,所以可移植的操作系统实现不能把它作为整数处理。因此必须使用函数来对两个线程ID进行比较。其函数原型如下:

#include 
int pthread_equal( pthread_t tid1, pthread_t tid2 );返回值:若相等则返回非0值,否则返回0

  用结构表示pthreadd_t数据类型的后果是不能用一种可移植的方式打印该数据类型的值。有时在程序调试过程中打印线程ID是非常有用的,而通常在其他情况下不需要打印线程ID。最坏的情况是:有可能出现不可移植的调试代码,当然这算不上是很大的局限性。

  线程可以通过调用pthread_self函数获得自身的线程ID。其函数原型如下:

#include 
pthread_t pthread_self(void);

当线程需要识别以线程ID作为标识的数据结构时,函数pthread_self()可以于函数pthread_equal()一起使用。例如,主线程可能把工作任务放到一个队列中,用线程ID来控制每个工作线程处理哪些作业。

2、线程创建

  启动程序后,产生的进程只有一条线程,称之为初始线程或主线程。在传统的UNIX进程模型中,每个进程只有一个控制线城。从概念上来讲,这与基于线程的模型中每个进程只包含一个线程是相同的的。在POSIX线程(pthread)的情况下,程序开始运行时,它也是以单进程中的单个控制线程启动的,在创建多个控制线程以前,程序的行为与传统的进程并没有什么区别。新增的线程可以通过调用pthread_create函数创建。其函数原型如下:

  

#include 
int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(*start_rtn)(void *), void *restrict arg);返回值:若成功则返回0,否则返回错误编号

  当pthread_create成功返回时,由tidp指向的内存单元被设置为新创建的线程的线程ID。attr参数用于定制各种不同的线程属性。线程属性在以后介绍,眼下暂时把它设置为NULL,创建默认属性的线程。

  新创建的线程从start_rtn函数的地址开始运行,该函数只有一个无类型指针参数arg,如果需要向start_rtn函数传递的参数不止一个,那么需要把这些参数放到一个结构中,然后把这个结构的地址作为arg参数传入。

  线程创建时并不能保证哪个线程会先运行:是新创建的线程还是调用线程。新创建的线程可以访问进程的地址空间,并且继承调用线程的浮点环境和信号屏蔽字,但是该线程的未决信号集被清除。

  注意pthread函数在调用失败时通常会返回错误码,它们并不像其他的POSIX函数一样设置errno。每个线程都提供errno的副本,这只是为了与使用errno的现有函数兼容。在线程中,从函数中返回错误码更为清晰整洁,不需要依赖那些随着函数执行不断变化的全局状态,因而可以把错误的范围限制在引起出错的函数中。

示例:使用函数pthread_create()创建新线程

pthread_creat.c

#include 
#include
#include
#include
pthread_t ntid;void printids(const char *s){ pid_t pid; pthread_t tid; pid = getpid(); tid = pthread_self(); printf("%s pid %u tid %u (0x%x) \n", s, (unsigned int)pid, (unsigned int)tid, (unsigned int)tid);}void *thr_fn(void *arg){ printids("new thread : "); return((void *)0);}int main(void){ int err; err = pthread_create(&ntid, NULL, thr_fn, NULL); if(err != 0) printf("can't create thread: %s\n", strerror(err)); printids("main thread: "); sleep(1); exit(0);}

编译运行结果如下:

这个实例有两个需要注意的地方:

(1)需要处理主线程和新线程之间的竞争。首先是主线程需要休眠,如果主线程不休眠,它就可能退出,这样在新线程有机会运行之前整个进程可能就已经终止了。这种行为特征依赖于操作系统中的线程实现和调度算法。

(2)新线程是通过调用pthread_self函数获取自己的线程ID,而不是从共享内存中读出或者从线程的启动例程中以参数的形式接收到。回忆pthread_create函数,它会通过第一个参数(tidp)返回新建线程的线程ID。在本例中,主线程把新线程ID存放在ntid中,但是新建的线程并不能安全地使用它,如果新线程在主线程调用pthread_create返回之前就运行了,那么新线程看到的是未经初始化的ntid的内容,这个内容并不是正确的线程ID。

 

3、线程终止

如果进程中的任一线程调用了exit、_Exit或者_exit,那么整个进程就会终止。与此类似,如果信号的默认动作是终止进程,那么,把该信号发送到线程会终止整个进程。

单个线程可以通过下列三种方式退出,在不终止整个进程的情况下停止它的控制流。

(1)线程只是从启动例程中返回,返回值是线程的退出码。

(2)线程可以被同一进程中的其他线程取消。

(3)线程调用pthread_exit。

1、函数pthread_exit()

   函数原型如下

#include 
void pthread_exit(void *rval_ptr);

rval_ptr是一个无类型指针,与传给启动例程的单个参数类似。

示例:

程序给出了用自动变量(分配在栈上)作为pthread_exit的参数时出现的问题

#include 
#include
#include
#include
struct foo { int a, b, c, d;};void printfoo(const char *s, const struct foo *fp){ printf(s); printf(" structure at 0x%x\n", (unsigned)fp); printf(" foo.a = %d\n", fp->a); printf(" foo.b = %d\n", fp->b); printf(" foo.c = %d\n", fp->c); printf(" foo.d = %d\n", fp->d);}void *thr_fn1(void *arg){ struct foo foo = {
1, 2, 3, 4}; printfoo("thread 1:\n", &foo); pthread_exit((void *)&foo); printfoo("thread 1:\n", &foo);}void *thr_fn2(void *arg){ printf("thread 2: ID is %d\n", pthread_self()); pthread_exit((void *)0);}int main(void){ int err; pthread_t tid1, tid2; struct foo *fp; err = pthread_create(&tid1, NULL, thr_fn1, NULL); if(err != 0) printf("can't create thread 1: %s\n", strerror(err)); err = pthread_join(tid1, (void *)&fp); if(err != 0) printf("can't join with thread 1: %s\n", strerror(err)); sleep(2); printfoo("parent: \n", fp); exit(0);}

 编译运行结果如下:

 

2、函数pthread_join()

进程中的其他线程可以通过调用pthread_join函数等待线程的结束,其函数原型如下:

#include 
int pthread_join(pthread_t thread, void **rval_ptr);返回值:若成功则返回0,否则返回错误编号

调用线程将一直阻塞,直到指定的线程调用pthread_exit、从启动例程中返回或者被取消。如果线程只是从它的启动例程返回,rval_ptr将包含返回码。如果线程被取消,由rval_ptr指定的内存单元就置为PTHREAD_CANCELED。

可以通过调用pthread_join自动把线程置于分离状态,这样资源就可以恢复。如果线程已经处于分离状态,pthread_join调用就会失败,返回EINVAL。

如果对线程的返回值并不感兴趣,可以把rval_ptr置为NULL。在这种情况下,调用pthread_join函数将等待指定的线程终止,但并不获取线程的终止状态。

示例:获取已终止的线程的退出码。

 

#include 
#include
#include
#include
void *thr_fn1(void *arg){ printf("thread 1 returning\n"); return((void *)1);}void *thr_fn2(void *arg){ printf("thread 2 exiting\n"); pthread_exit((void *)2);}int main(void){ int err; pthread_t tid1, tid2; void *tret; err = pthread_create(&tid1, NULL, thr_fn1, NULL); if(err != 0) printf("can't create thread 1: %s\n", strerror(err)); err = pthread_create(&tid2, NULL, thr_fn2, NULL); if(err != 0) printf("can't create thread 2: %s\n", strerror(err)); err = pthread_join(tid1, &tret); if(err != 0) printf("can't join with thread 1: %s\n", strerror(err)); printf("thread 1 exit code %d\n", (int)tret); err = pthread_join(tid2, &tret); if(err != 0) printf("can't join with thread 2: %s\n", strerror(err)); printf("thread 2 exit code %d\n", (int)tret); exit(0);}

程序编译运行结果如下:

可以看出,当一个线程通过调用pthread_exit退出或者简单地从启动例程中返回时,进程中的其他线程可以通过调用pthread_join函数获得该线程的退出状态

pthread_create和pthread_exit函数的无类型指针参数能传递的数值可以不止一个,该指针可以传递包含更复杂信息的结构的地址,但是注意这个结构所使用的内存在调用者完成调用以后必须仍然是有效的,否则就会出现无效或非法内存访问。

3、函数pthread_cancel()

  函数pthread_cancel()用于请求取消同一进程中的其他线程,其函数原型如下:

#include 
int pthread_cancel(pthread_t tid);返回值:若成功则返回0,否则返回错误编号

在默认情况下,pthread_cancel函数会使得由tid标识的线程的行为表现为如同调用了参数为PTHREAD_CANCELED的pthread_exit函数,但是,线程可以选择忽略取消方式或是控制取消方式。注意,pthread_cancel并不等待线程终止,它仅仅提出请求。

线程可以安排它退出时需要调用的函数,这与进程可以用atexit函数安排进程退出时需要调用的函数是类似的。这样的函数称为线程清理处理程序(thread cleanup handler)。线程可以建立多个清理处理程序。处理程序记录在栈中,也就是说它们的执行顺序与它们注册时的顺序相反。

#include 
void pthread_cleanup_push(void (*rtn)(void *), void *arg);void pthread_cleanup_pop(int execute);

当线程执行以下动作时调用清理函数(调用参数为arg,清理函数rtn的调用顺序是由pthread_cleanup_push函数来安排的):

  • 调用pthread_exit时。
  • 相应取消请求时。
  • 用非零execute参数调用pthread_cleanup_pop时。

如果execute参数置为0,清理函数将不被调用。无论哪种情况,pthread_cleanup_pop都将删除上次pthread_cleanup_push调用建立的清理处理程序。

这些函数有一个限制,由于它们可以实现为宏,所以必须在与线程相同的作用域中以匹配对的形式使用,pthread_cleanup_push的宏定义可以包含字符{,在这种情况下对应的匹配字符}就要在pthread_cleanup_pop定义中出现。

示例:如何使用线程清理处理程序。需要把pthread_cleanup_pop调用和pthread_cleanup_push调用匹配起来,否则,程序编译可能通不过。

#include 
#include
#include
#include
void cleanup(void *arg){ printf("cleanup: %s\n", (char *)arg);}void *thr_fn1(void *arg){ printf("thread 1 start\n"); pthread_cleanup_push(cleanup, "thread 1 first hanlder"); pthread_cleanup_push(cleanup, "thread 1 second handler"); printf("thread 1 push complete\n"); if(arg) return((void *)1); pthread_cleanup_pop(0); pthread_cleanup_pop(0); return((void *)1);}void *thr_fn2(void *arg){ printf("thread 2 start\n"); pthread_cleanup_push(cleanup, "thread 2 first handler"); pthread_cleanup_push(cleanup, "thread 2 second handler"); printf("thread 2 push complete\n"); if (arg) pthread_exit((void *)2); pthread_cleanup_pop(0); pthread_cleanup_pop(0); pthread_exit((void *)2);}intmain(void){ int err; pthread_t tid1, tid2; void *tret; err = pthread_create(&tid1, NULL, thr_fn1, (void *)1); if(err != 0) printf("can't create thread 1: %s\n", strerror(err)); err = pthread_create(&tid2, NULL, thr_fn2, (void *)1); if(err != 0) printf("can't create thread 2: %s\n", strerror(err)); err = pthread_join(tid1, &tret); if(err != 0) printf("can't join with thread 1: %s\n", strerror(err)); printf("thread 1 exit code %d\n", (int)tret); err = pthread_join(tid2, &tret); if(err != 0) printf("can't join with thread 2: %s\n", strerror(err)); printf("thread 2 exit code %d\n", (int)tret); exit(0);}

编译运行结果如下:

从输出结果可以看出,两个线程都正确地启动和退出了,但是只调用了第二个线程的清理处理程序,所以如果线程是通过从它的启动例程中返回而终止的话,那么它的清理处理程序就不会被调用,还要注意清理处理程序是按照与它们安装时相反的顺序被调用的。

现在可以开始看出线程函数和进程函数之间的相似之处。表11-1总结了这些相似的函数。

   进程原语和线程原语的比较

 

4、线程的分离与结合

   在默认情况下,线程的终止状态会保存一直等到对该线程调用pthread_join。如果线程已经处于分离状态线程的底层存储资源可以在线程终止时立即被收回。

 

  在任何一个时间点上,线程是可结合的(joinable),或者是分离的(detached)。一个可结合的线程能够被其他线程收回其资源和杀死;在被其他线程回收之前,它的存储器资源(如栈)是不释放的。相反,一个分离的线程是不能被其他线程回收或杀死的,它的存储器资源在它终止时由系统自动释放。

 

  线程的分离状态决定一个线程以什么样的方式来终止自己。线程的默认属性,即为非分离状态(即可结合的,joinable,需要回收),这种情况下,原有的线程等待创建的线程结束;只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。而分离线程不是这样子的,它没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。程序员应该根据自己的需要,选择适当的分离状态。

 

  当线程被分离时,并不能用pthread_join函数等待它的终止状态。对分离状态的线程进行pthread_join的调用会产生失败,返回EINVAL。pthread_detach调用可以用于使线程进入分离状态。

#include 
int pthread_detach(pthread_t tid);返回值:若成功则返回0,否则返回错误编号

 

参考资料:

转载于:https://www.cnblogs.com/noticeable/p/8544420.html

你可能感兴趣的文章
追求绿色数据中心
查看>>
Web开发初学指南
查看>>
OpenStack Days China:华云数据CTO郑军分享OpenStack创新实践
查看>>
探寻光存储没落的真正原因
查看>>
高通64位ARMv8系列服务器芯片商标命名:Centriq
查看>>
中国人工智能学会通讯——融合经济学原理的个性化推荐 1.1 互联网经济系统的基本问题...
查看>>
盘点大数据商业智能的十大戒律
查看>>
戴尔为保护数据安全 推出新款服务器PowerEdge T30
查看>>
今年以来硅晶圆涨幅约达40%
查看>>
构建智能的新一代网络——专访Mellanox市场部副总裁 Gilad Shainer
查看>>
《数字视频和高清:算法和接口》一导读
查看>>
《中国人工智能学会通讯》——6.6 实体消歧技术研究
查看>>
如何在Windows查看端口占用情况及查杀进程
查看>>
一分钟秒懂公有云、私有云、混合云......
查看>>
云存储应用Upthere获7700万美元股权债务融资
查看>>
国家互联网应急中心何世平博士主题演讲
查看>>
洗茶,你误会了多少年?
查看>>
移动大数据“后市场”受青睐 亟需数据深度学习人才
查看>>
大数据时代的财务人员须与时俱进
查看>>
开放网络的承诺:它真的做到了吗?
查看>>