热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

(十)一起学Unix环境高级编程(APUE)之线程控制

..目录(一)一起学Unix环境高级编程(APUE)之标准IO(二)一起学Unix环境高级编程(APUE)之文件IO(三)一起学Unix环境高级编程(APUE)之文件和目录(

.

.

.

.

.

目录

(一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO

(二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO

(三) 一起学 Unix 环境高级编程 (APUE) 之 文件和目录

(四) 一起学 Unix 环境高级编程 (APUE) 之 系统数据文件和信息

(五) 一起学 Unix 环境高级编程 (APUE) 之 进程环境

(六) 一起学 Unix 环境高级编程 (APUE) 之 进程控制

(七) 一起学 Unix 环境高级编程 (APUE) 之 进程关系 和 守护进程

(八) 一起学 Unix 环境高级编程 (APUE) 之 信号

(九) 一起学 Unix 环境高级编程 (APUE) 之 线程

(十) 一起学 Unix 环境高级编程 (APUE) 之 线程控制

(十一) 一起学 Unix 环境高级编程 (APUE) 之 高级 IO

(十二) 一起学 Unix 环境高级编程 (APUE) 之 进程间通信(IPC)

(十三) [终篇] 一起学 Unix 环境高级编程 (APUE) 之 网络 IPC:套接字

 

 

之前我们在创建线程的时候都是使用的默认属性,本章主要讨论的是自定义线程的属性。

使用默认属性基本上能解决掉遇到的大部分问题,所以自定义属性在实际项目中用得比较少。

 

1.线程属性

《APUE》第三版 P341 表中的属性可以用来限定一个进程能创建线程的最大数量,但是限定线程数量的宏不必太当真,因为在上一篇博文中我们说过了一个线程能创建的线程的数量是受很多因素影响的,并非一定是以这几个宏值为准的。

线程属性使用 pthread_attr_t 类型表示。

 1 #include 
 2 #include 
 3 #include 
 4 #include <string.h>
 5 
 6 static void *func(void *p)
 7 {
 8     puts("Thread is working.");
 9 
10     pthread_exit(NULL);
11 }
12 
13 int main()
14 {
15     pthread_t tid;
16     int err, i;
17     pthread_attr_t attr;
18 
19     pthread_attr_init(&attr);
20     // 修改每个线程的栈大小
21     pthread_attr_setstacksize(&attr,1024*1024);
22 
23     for(i = 0 ; ; i++)
24     {
25         // 测试当前进程能创建多少个线程
26         err = pthread_create(&tid,&attr,func,NULL);
27         if(err)
28         {
29             fprintf(stderr,"pthread_create():%s\n",strerror(err));
30             break;
31         }
32 
33     }
34 
35     printf("i = %d\n",i);
36     
37     pthread_attr_destroy(&attr);
38 
39     exit(0);
40 }

 

上面的栗子就是通过线程的属性修改了为每个线程分配的栈空间大小,这样创建出来的线程数量与默认的就不同了。

线程属性使用 pthread_attr_init(3) 函数初始化,用完之后使用 pthread_attr_destroy(3) 函数销毁。

线程属性不仅可以设定线程的栈空间大小,还可以创建分离的线程等等。

 

2.互斥量属性

互斥量属性使用 pthread_mutexattr_t 类型表示,与线程属性一样,使用之前要初始化,使用完毕要销毁。

pthread_mutexattr_init(3) 函数用于初始化互斥量的属性,用法跟线程的属性很相似。

 

 1 pthread_mutexattr_getpshared, pthread_mutexattr_setpshared  -  get  and
 2        set the process-shared attribute
 3 
 4 #include 
 5 
 6 int pthread_mutexattr_getpshared(const pthread_mutexattr_t *
 7        restrict attr, int *restrict pshared);
 8 
 9 int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr,
10        int pshared);

 

函数名称里面的 p 是指 process,这两个函数的作用是设定线程的属性是否可以跨进程使用。这条有点乱是吧,线程的属性怎么能跨进程使用呢?别急,我们先看看 clone(2) 函数。

1 clone, __clone2 - create a child process
2 
3 #define _GNU_SOURCE
4 #include 
5 
6 int clone(int (*fn)(void *), void *child_stack,
7           int flags, void *arg, ...
8           /* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );

 

clone(2) 进程的 flags 如果设置了 CLONE_FILES 则父子进程共享文件描述符表,正常情况文件描述符表是线程之间共享的,因为多线程是运行在同一个进程的地址空间之内的。

虽然 clone(2) 函数的描述是创建子进程,但实际上如果将 flags 属性设置得极端分离(各种资源都独享),相当于创建了一个子进程;

而如果 flags 属性设置得极端近似(各种资源都共享),则相当于创建了兄弟线程。所以对于内核来讲并没有进程这个概念,只有线程的概念。你创建出来的到底是进程还是线程,并不影响内核进行调度。

如果需要创建一个“东西”与当前的线程既共享一部分资源,又独占一部分资源,就可以使用 clone(2) 函数创建一个既不是线程也不是进程的“东西”,因为对内核来说进程和线程本来就是模糊的概念。

现在能理解为什么上面说 pthread_mutexattr_setpshared(3) 函数的作用是设定线程的属性是否可以跨进程使用了吧?

 

互斥量分为四种,不同的互斥量在遇到不同的情况时效果是不同的,《APUE》第三版 P347 有图12-5 说明了这个现象,LZ 把它照搬到这里。

互斥量类型 没有解锁时重新加锁 不占用时解锁 在已解锁时解锁
PTHREAD_MUTEX_NORMAL(常规) 死锁 未定义 未定义
PTHREAD_MUTEX_ERRORCHECK(检错) 返回错误 返回错误 返回错误
PTHREAD_MUTEX_RECURSIVE(递归) 允许 返回错误 返回错误

PTHREAD_MUTEX_DEFAULT

(默认,我们平时使用的就是这个)

未定义 未定义 未定义

表1 互斥量类型行为

LZ 解释一下表头上的描述是什么意思:

1)没有解锁时重新加锁:当前 mutex 已 lock,再次 lock 的情况;

2)不占用时解锁:他人锁定由你解锁的情况;

3)在已解锁时解锁:当前 mutex 已 unlock,再次 unlock 的情况;

 

3.重入

第一次见到重入是在信号阶段是吧。

如果一个函数在相同的时间点可以被多个线程安全地调用,就称该函数是线程安全的。

POSIX 标准要求,在线程标准制定之后,所有的库必须支持线程安全,如果不支持线程安全需要在函数名添加 _unlocked 后缀,或发布一个支持线程安全的函数,函数名要添加 _r 后缀。

我们在 man 手册中已经见过很多带有 _r 后缀的函数了。

 

4.线程特定数据

就是为了某些数据支持多线程并发而做的改进。最典型的就是 errno,errno 最初是全局变量,现在早已变成宏定义了。

我们把 errno 预编译一下,看看它的庐山真面目。

1 #include 
2 
3 errno;

 

1 >$ gcc -E errno.c
2 # 2 "errno.c" 2
3 
4 (*__errno_location ());
5 >$

 

 

5.线程的取消 

在上一篇博文中我们说过,pthread_cancel(3) 函数只是提出取消请求,并不能强制取消线程。

线程的取消分为两种情况:允许取消 或 不允许取消。

pthread_cancel(3) 提出取消请求后,是否允许取消是由被请求取消的线程自己决定的。

不允许取消没什么好说的,我们说说允许取消。

允许取消分为两种情况:异步 cancel 和 推迟 cancel(默认)

1)异步 cancel:是内核的操作方式,这里不做解释。

2)推迟 cancel:推迟到取消点再响应取消操作。取消点其实就是一个函数,收到取消请求时取消点的代码不会执行。

《APUE》第三版 P362 图12-14 都是可能导致阻塞的系统调用,它们都是 POSIX 定义的一定存在的取消点。P363 图12-15 是 POSIX 定义的可选取消点,这些函数实际是否为取消点要看平台具体的实现。

为什么要采用推迟取消的策略,而不是收到请求在任何地方都立即取消呢?我们先举个栗子说明这个问题,大家请看下面的伪代码:

 1 thr_func()
 2 
 3 {
 4 
 5    p = malloc();
 6 
 7   -------------------------->收到了一个取消请求
 8 
 9   -------------------------->pthread_cleanup_push();->free(p); // 不是取消点,继续执行
10 
11    fd1 = open(); // 是取消点,在取消点执行之前响应取消动作
12 
13   -------------------------->pthread_cleanup_push();->close(fd1);
14 
15    fd2 = open();
16 
17   -------------------------->pthread_cleanup_push();->close(fd2);
18 
19    pthread_exit();
20 
21 }

 

在线程执行函数运行的任何时候都可能收到取消请求,假设上面的函数刚刚使用 malloc(3) 函数动态分配了一段内存,还没来得及挂钩子函数的时候就收到了一个取消请求,如果立即响应这个取消请求就会导致内存泄漏。而挂载钩子函数的宏 pthread_cleanup_push 不是取消点,所以会推迟这个取消请求继续工作。等它把钩子函数挂载完毕之后继续运行来到 open(2) 函数,由于 open(2) 函数是有效的取消点,所以响应了这个取消请求,线程被取消并且通过钩子函数释放了上面 malloc(3) 所申请的空间。这就是推迟取消最明显的作用。

pthread_setcancelstate(3) 函数的作用就是修改线程的可取消状态,可以将线程设置为可取消的或不可取消的。

pthread_setcanceltype(3) 函数用来修改取消类型,也就是可以选择 异步 cancel 和 推迟 cancel。

pthread_testcancel(3) 函数的作用是人为放置取消点。假如某个线程一启动就疯狂的做数学运算10分钟,没有调用任何函数,则这个线程无法响应取消,为了使这个线程可以响应取消就可以通过这个函数人为放置取消点。

 

6.线程和信号

(十) 一起学 Unix 环境高级编程 (APUE) 之 线程控制

图1 线程级别的信号位图

在前面讨论信号的博文中,LZ 给大家画过一张信号处理过程的草图,在那幅图中简单的把一个线程的标准信号画成了两个位图。而实际上每个线程级别都持有一个 mask 位图和一个 padding 位图,每个进程级别持有一个 padding 位图而没有 mask 位图。从内核态回到用户态之前,当前线程先用自己的 mask 位图与进程级别的 padding 做按位与(&)运算,如果有信号就要去处理;然后再用自己的 mask 位图与自己的 padding 位图做按位与运算,再处理相应的信号。

所以其实是哪个线程被调度,就由哪个线程响应进程级别的信号。

由此可见,线程之间也是可以互相发信号的。

 

1 pthread_kill - send a signal to a thread
2 
3 #include 
4 
5 int pthread_kill(pthread_t thread, int sig);
6 
7 Compile and link with -pthread.

pthread_kill(3) 函数的作用就是在线程阶段发信号,thread 表示给哪个线程发送信号,sig 是发送哪个信号。

由于这个函数使用起来很简单,这里 LZ 就不把栗子贴出来了,大家自己动手写写试试吧。

 

pthread_sigmask(3) 函数的作用时人为的干预线程级别的 mask 位图。与 sigsetmask(3) 函数很像,大家自己动手试试吧。

 

7.线程和 fork

这一小节主要说的是 fork(2) 在不同平台上实现有歧义。

在fork的发展过程中主要有两大阵营,一大阵营使用写时拷贝技术,另一大阵营使用类似 vfork(2) 的策略。

这两种策略在前面我们讨论进程关系的博文中都讨论过,感兴趣的童鞋可以自己看看书上的描述,这里就不做太多的介绍了。

 

8.线程和 I/O

这一小节主要就是介绍了下 pread(2) 和 pwrite(2) 函数,这两个函数实际当中用得并不多,感兴趣的童鞋自己看看书上的介绍或者看看 man 手册里的说明吧,这里不做过多的讨论了。如果有什么问题可以在评论中留言。

 

到这里 POSIX 标准的线程就介绍完了。*nix 平台线程的标准不只有 POSIX 一家,还有像 OpenMP 等标准也定义了不同的线程实现方式。

 

9.OpenMP 标准

 我们使用 OpenMP 标准写一个 Hello World 程序。

 1 #include 
 2 #include 
 3 #include 
 4 
 5 int main()
 6 {
 7 #pragma omp parallel sections
 8 {
 9 #pragma omp section
10         printf("[%d]:Hello\n",omp_get_thread_num());
11 #pragma omp section
12         printf("[%d]:World\n",omp_get_thread_num());
13 }
14 
15         exit(0);
16 }

 OpenMP 标准的多线程就是使用 # 这种预处理标签实现的,使用 GCC 编译的时候需要加 -fopenmp 参数。

>$ make hello
cc -fopenmp -Wall    hello.c   -o hello
>$ ./hello
[0]:Hello
[1]:World
>$ ./hello
[1]:World
[0]:Hello
>$

 

从上面的运行结果可以看出来,线程已经创建,并且已经发生了竞争。

GCC 从 4.0 以上的版本开始支持 OpenMP 标准。

由于 OpenMP 标准不是 《APUE》里面介绍的,所以我们这里就不做过多的探讨了,感兴趣的小伙伴们可以去 http://www.openmp.org 了解更多内容。

 


推荐阅读
  • Linux 中使用 clone 函数来创建线程
    2019独角兽企业重金招聘Python工程师标准Linux上创建线程一般使用的是pthread库实际上libc也给我们提供了创建线程的函数那就是cloneintclone(i ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 如何用JNI技术调用Java接口以及提高Java性能的详解
    本文介绍了如何使用JNI技术调用Java接口,并详细解析了如何通过JNI技术提高Java的性能。同时还讨论了JNI调用Java的private方法、Java开发中使用JNI技术的情况以及使用Java的JNI技术调用C++时的运行效率问题。文章还介绍了JNIEnv类型的使用方法,包括创建Java对象、调用Java对象的方法、获取Java对象的属性等操作。 ... [详细]
  • PHP反射API的功能和用途详解
    本文详细介绍了PHP反射API的功能和用途,包括动态获取信息和调用对象方法的功能,以及自动加载插件、生成文档、扩充PHP语言等用途。通过反射API,可以获取类的元数据,创建类的实例,调用方法,传递参数,动态调用类的静态方法等。PHP反射API是一种内建的OOP技术扩展,通过使用Reflection、ReflectionClass和ReflectionMethod等类,可以帮助我们分析其他类、接口、方法、属性和扩展。 ... [详细]
  • 作者一直强调的一个概念叫做oneloopperthread,撇开多线程不谈,本篇博文将学习,怎么将传统的IO复用pollepoll封装到C++类中。1.IO复用复习使用p ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • JDK源码学习之HashTable(附带面试题)的学习笔记
    本文介绍了JDK源码学习之HashTable(附带面试题)的学习笔记,包括HashTable的定义、数据类型、与HashMap的关系和区别。文章提供了干货,并附带了其他相关主题的学习笔记。 ... [详细]
  • 本文介绍了使用readlink命令获取文件的完整路径的简单方法,并提供了一个示例命令来打印文件的完整路径。共有28种解决方案可供选择。 ... [详细]
  • linux进阶50——无锁CAS
    1.概念比较并交换(compareandswap,CAS),是原⼦操作的⼀种,可⽤于在多线程编程中实现不被打断的数据交换操作࿰ ... [详细]
  • UNIX高级环境编程 第11、12章 线程及其属性
    第11章线程11.2线程概念线程资源:线程ID,一组寄存器,栈,调度优先级和策略,信号屏蔽字,e ... [详细]
  • C++程序员视角下的Rust语言
    自上世纪80年代初问世以来,C就是一门非常重要的系统级编程语言。到目前为止,仍然在很多注重性能、实时性、偏硬件等领域发挥着重要的作用。C和C一样&#x ... [详细]
  • 在说Hibernate映射前,我们先来了解下对象关系映射ORM。ORM的实现思想就是将关系数据库中表的数据映射成对象,以对象的形式展现。这样开发人员就可以把对数据库的操作转化为对 ... [详细]
author-avatar
wang小资_588
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有