POSIX多线程
Content
1. 基础概念
2. 线程安全
3. 可重入
4. 并发系统基本功能
1. 基础概念
线程
- 进程里执行代码的部分;
- 包含一系列机器指令所必须的机器状态,包括当前指令位置(一般为PC寄存器)、栈顶指针SP、通用寄存器、地址和数据寄存器等。
- 线程不包括进程中的其他数据,如地址空间和文件描述符;
进程
- 线程加上地址空间、文件描述符和其他数据。
- 一个进程中的所有线程共享文件和地址空间,包括程序段、数据段和堆栈;
既然线程和进程都需要地址空间、文件描述符等,那么区别何在?
- 多个线程可以共享一个地址空间,而做不同的事情。
- 在多处理器系统中,一个进程中的多个线程可以同时做不同的工作。
- 系统在线程间切换比在进程间切换快得多;
- 每一个进程有独立的虚拟地址空间,但同一个进程中的线程共享相同的地址空间和其他进程数据;
pthreads可以以一种优雅、高效、可移植的方式完成工作。
此处指是简单一句话总结线程和进程,实际上其定义此处完整。
异步(asynchronous):事情相互独立地发生,除非有强加的依赖性。任何两个彼此独立运行的操作都是异步的。
异步的复杂性
- 如果没有同时执行多个活动,那么异步就没有优势;
- 如果开始了一个异步活动,然后什么也不做就等待他结束,则并没有从异步获得好处。
并发(concurrency):事情同时发生。
- 实际上可能是串行发生的事情好像同时发生一样;
- 并发描述的是单处理器系统中线程或进程的行为;
- 在POSIX中,并发的定义要求“延迟调用线程的函数不应该导致其他线程的无限期延迟”;
- 并发操作之间可能任意交错,导致程序相互独立的运行(一个程序不必等到另一个程序结束后才开始运行),但并发并不代表操作同时进行。
并行(parallelism):指并发序列同时执行。指事情在相同的方向上独立进行(没有交错);
并发与并行
- 真正的并行只能在多处理器系统中存在;
- 但并发可以在单处理器系统和多处理器系统中都存在;
- 并发能在单处理器系统中存在是因为并发实际上是并行的假象;
- 并行则要求程序能够同时执行多个操作;
- 而并发只要求程序能够假装同时执行多个操作;
2. 线程安全
什么是线程安全?
- 定义:指代码能够被多个线程调用而不会产生灾难性后果;
- 特点:不要求代码在多个线程中高效的运行,只要求能够安全地运行;
实现线程安全的工具
- pthreads互斥量、条件变量、线程私有数据;
如何实现线程安全?
- 一般方法
- 对不需要保存永久状态的函数,通过整个函数调用的串行化实现;
- 比如,进入函数时加锁,退出函数时解锁;
- => 函数可以被多个线程调用,但一次只能有一个线程调用该函数;
- 更有效的方式
- 将线程安全函数分为多个小的临界区;
- => 允许多个线程进入该函数,但不能同时进入一个临界区;
- 更好的方式
- 将代码改造为对临界对象(数据)的保护而非对临界代码的保护;
- => 可使不同时访问相同临界数据的线程完全并行的执行;
3. 可重入
可重入
- 有时用来表示有效的线程安全,即通过采用比将函数或库转换成一系列区域更为复杂的方式使代码成为线程安全的;
- 可重入的函数应该避免依赖任何静态数据,最好避免依赖线程间任何形式的同步;
- 互斥量和线程私有数据可以实现线程安全,但通常需要改变接口来使函数可重入;
举例
- pthreads为了使readdir()函数可重入,增加readdir_r()函数,并在该函数内避免任何锁操作;
- 让调用者在搜索目录时分配一个数据结构来保存readdir_r()的环境;
特点:这种方式只有调用者才知道数据如何使用;
4. 并发系统基本功能
基本功能
- 执行环境:是并发实体的状态;提供建立、删除、维护环境的方式;
- 调度:决定在某个给定时刻该执行哪个环境,并在不同的环境中切换;
- 同步:为并发执行的环境提供协调访问共享资源的机制;
什么是同步?——让线程协调地完成工作的机制;
同步的实现方式
- 互斥量
- 条件变量
- 信号量
- 事件
- 消息机制:管道/Socket/Posix消息队列
线程、互斥量、条件变量关系
- 线程是计算机中的可执行单元,是CPU调度单位;
- 互斥量和条件变量都是线程同步的手段;
- 互斥量阻止线程间发生不可预期的冲突;
- 一旦避免了冲突,条件变量让线程等待直到可以安全地执行;
Reference
http://www.cnblogs.com/NickyYe/archive/2008/12/01/1344802.html
Content
0. 序
1. 基本的同步版本
2. 多进程版本
3. 多线程版本
4. 小结
0. 序
本节通过一个简单的闹钟实例演示异步编程方法。
该程序循环接受用户输入信息,直到出错或者输入完毕。用户输入的每行信息有两部分:闹钟等待的时间(秒)和闹钟时间到达时显示的文本信息。
1. 基本的同步版本
代码如下。
- /*
- * alarm.c
- *
- * Simple synchronous alarm program. This is used as a
- * reference for progressive examples of asynchronous
- * alarm programs.
- */
- #include "errors.h"
- int main (int argc, char **argv)
- {
- int seconds;
- char line[128];
- char message[64];
- while (1)
- {
- printf("Alarm> ");
- if (fgets(line, sizeof(line), stdin) == NULL)
- exit(0);
- if (strlen(line) <= 1)
- continue;
- /**
- Parse input line into seconds (%d) and a message (%64[^\n]),
- consisting of up to 64 characters separated from the seconds
- by whitespace.
- */
- if (sscanf(line, "%d %64[^\n]", &seconds, message) < 2)
- {
- fprintf(stderr, "Bad command\n");
- }
- else
- {
- sleep(seconds);
- printf("(%d) %s\n", seconds, message);
- }
- }
- }
特点:同步实现异步,闹钟请求后,程序睡眠等待闹钟时间到;
问题:用同步方式来实现异步,致一次只能处理一个闹钟请求;即程序睡眠时间到后才能进行下一次闹钟请求;
运行结果。
- # ./alarm
- Alarm> 5
- Bad command
- Alarm> 5 this is a test with 5 seconds
- (5) this is a test with 5 seconds //等待5秒并打印该消息
- Alarm> 10 the seconds with 10 seconds
- (10) the seconds with 10 seconds //等待10秒并打印该消息
- Alarm>
2. 多进程版本
代码如下。
- /*
- * alarm_fork.c
- *
- * This version of alarm.c uses fork to create a new process to
- * wait for each alarm to expire.
- */
- #include <sys/types.h>
- #include <wait.h>
- #include "errors.h"
- int main(int argc, char **argv)
- {
- int status;
- char line[128];
- int seconds;
- pid_t pid;
- char message[64];
- while (1)
- {
- printf ("Alarm> ");
- if (fgets(line, sizeof(line), stdin) == NULL) exit(0);
- if (strlen(line) <= 1) continue;
- /**
- Parse input line into seconds (%d) and a message (%64[^\n]),
- consisting of up to 64 characters separated from the seconds
- by whitespace.
- */
- if (sscanf(line, "%d %64[^\n]", &seconds, message) < 2)
- {
- fprintf(stderr, "Bad command\n");
- }
- else
- {
- pid = fork();
- if (pid == (pid_t)-1)
- errno_abort("Fork");
- if (pid == (pid_t)0)
- {
- /*
- * If we're in the child, wait and then print a message
- */
- sleep(seconds);
- printf("pid = %d, (%d) %s\n", pid, seconds, message);
- exit(0);
- }
- else
- {
- /*
- * In the parent, call waitpid() to collect any children that
- * have already terminated.
- */
- do
- {
- printf("line = %d, pid = %d\n", __LINE__, pid);
- pid = waitpid((pid_t)-1, NULL, WNOHANG);
- printf("line = %d, pid = %d\n", __LINE__, pid);
- if (pid == (pid_t)-1)
- errno_abort("Wait for child");
- } while (pid != (pid_t)0);
- }
- }
- }
- }
特点:在子进程中异步地调用sleep函数,而父进程继续运行;
问题:对每次闹钟请求,使用fork创建子进程,程序开销过大;
waitpid()函数
- WNOHANG:父进程不必挂起等待子进程的结束;
- 如果有子进程终止,该函数回收该子进程的资源;
- 如果没有子进程终止,该函数立即返回pid=0;父进程继续回收终止的子进程直到没有子进程终止;
- 在每个命令之后循环调用waitpid,来回收所有结束的子进程;
运行结果。
- Alarm> 3 test 1 //输入命令
- Alarm> 5 test 2 //输入命令
- Alarm> (3) test 1 //等待3秒并打印该消息
- 10 test 3 //输入命令
- Alarm> (5) test 2 //等待5秒并打印该消息
- Alarm> (10) test 3 //等待10秒并打印该消息
- Alarm>
- # ./alarm_fork
- Alarm> 5 test 1
- line = 56, pid = 7901
- line = 58, pid = 0
- Alarm> 10 test 2
- line = 56, pid = 7902
- line = 58, pid = 0
- Alarm> pid = 0, (5) test 1
- pid = 0, (10) test 2
- Alarm> 15 test 3
- line = 56, pid = 7904
- line = 58, pid = 7901
- line = 56, pid = 7901
- line = 58, pid = 7902
- line = 56, pid = 7902
- line = 58, pid = 0
- Alarm> pid = 0, (15) test 3
- Alarm>
3. 多线程版本
代码如下。
- /*
- * alarm_fork.c
- *
- * This version of alarm.c uses pthread_create to create a
- * separate thread to wait for each alarm to expire.
- */
- #include <pthread.h>
- #include "errors.h"
- typedef struct alarm_tag {
- int seconds;
- char message[64];
- } alarm_t;
- void *alarm_thread (void *arg)
- {
- alarm_t *alarm = (alarm_t*)arg;
- int status;
- status = pthread_detach (pthread_self ());
- if (status != 0)
- err_abort (status, "Detach thread");
- sleep (alarm->seconds);
- printf ("(%d) %s\n", alarm->seconds, alarm->message);
- free (alarm);
- return NULL;
- }
- int main (int argc, char *argv[])
- {
- int status;
- char line[128];
- alarm_t *alarm;
- pthread_t thread;
- while (1) {
- printf ("Alarm> ");
- if (fgets (line, sizeof (line), stdin) == NULL) exit (0);
- if (strlen (line) <= 1) continue;
- alarm = (alarm_t*)malloc (sizeof (alarm_t));
- if (alarm == NULL)
- errno_abort ("Allocate alarm");
- /*
- * Parse input line into seconds (%d) and a message
- * (%64[^\n]), consisting of up to 64 characters
- * separated from the seconds by whitespace.
- */
- if (sscanf (line, "%d %64[^\n]", &alarm->seconds, alarm->message) < 2) {
- fprintf (stderr, "Bad command\n");
- free (alarm);
- } else {
- status = pthread_create(&thread, NULL, alarm_thread, alarm);
- if (status != 0)
- err_abort (status, "Create alarm thread");
- }
- }
- }
特点:在该例子中,alarm线程调用pthread_detach()函数来分离自己,通知pthreads不必关心它的终止时间和退出状态;因此不需要等待线程结束,除非希望获得它的返回值。alarm线程的资源在它终止后立即回收。
一般地,pthreads会保存线程的资源以使其他线程了解它已经终止并获得其最终结果。在本例中,alarm线程负责自己分离自己。
该版本用到以下函数:
- pthread_create():创建线程,运行代码由第三个参数指定,运行代码需要的参数由第四个参数传入;返回线程标志符;
- Pthread_detach():当线程终止时允许pthreads立即回收线程资源;
- pthread_exit():终止调用线程;
运行结果。
- # ./alarm_thread
- Alarm> 5 atest
- Alarm> 10 btest
- Alarm> (5) atest
- Alarm> 15 ctest
- Alarm> (10) btest
- (15) ctest
- Alarm>
4. 小结
多进程版本
- 每个闹钟有一个从主进程拷贝的独立地址空间;
- 主进程调用waitpid来告诉系统释放其创建的子进程资源;
多线程版本
- 所有线程共享同一个地址空间;
- 不需要等待线程结束,除非希望获得它的返回值;
- 线程间传递消息更快
- 不需要映射共享内存;
- 不需要通过管道读写;
- 不需要担心进程间传送的地址指针是否一致;
- 线程间共享一切:一个线程内有效的地址指针在所有线程内同样有效;
多线程编程优点
- 在多处理器系统中开发程序的并行性(仅并行性需要特殊硬件支持);
- 在等待慢速外设I/O操作结束的同时,程序可以执行其他计算,为程序的并发提供更有效、更自然的开发方式;
- 一种模块化编程模型,能清晰地表达程序中独立事件间的相互关系;
多线程编程缺点
- 计算负荷,如线程间同步需要的时间代价;
- 需要设立较好的编程规则,如要避免死锁、竞争和优先级倒置等;
- 更难以调试;
何种应用适合使用多线程?
- 计算密集型应用,为了能在多处理器系统上运行,将这些计算分解到多个线程中实现;
- IO密集型应用,为提高性能,将IO操作重叠;如分布式服务器;
Reference
# man waitpid
# man pthread_create
# man pthread_detach
content
1. 线程建立与使用
- 创建线程
- 初始线程
- 线程分离
2. 线程生命周期
- 就绪态
- 被阻塞
- 线程终止
- 线程回收
1. 线程建立与使用
创建线程
- 通过pthread_create()函数创建线程;
- 向该函数传递线程函数地址和线程函数参数;
- 线程函数只有一个void*参数;
- 该函数返回pthread_t类型的线程ID;
- 一般调用该函数创建线程,然后调用pthread_join()函数等待线程结束;
- 在当前线程从函数pthread_create()中返回以及新线程被调度执行之间不存在同步关系;
- 新线程可能在当前线程从pthread_create()返回值前就运行了;
- 或在当前线程从pthread_create()返回之前,新线程就可能已经运行完毕了;
pthread_join()
- 阻塞其调用者直到指定线程终止,然后可以选择地保存线程的返回值;
- 当pthread_join()调用返回时,被连接线程就已经被分离(detached),再也不能连接该线程了;
- 如果连接(joining)线程不关心返回值,或者它知道被连接(joined)的线程根本不返回任何值,则可向pthread_join()的&retval参数传递NULL,此时,被连接线程的返回值将被忽略;
如何使用可参考pthread_create()、pthread_join()手册;
初始线程
- C程序运行时,首先运行main()函数,main()函数所在线程称为初始线程或主线程;
- 初始线程可调用pthread_self()获得其ID,也可调用pthread_exit()来终止自己;
- 从main()返回将导致进程终止,也将使进程内所用线程终止;
- 在main()中调用pthread_exit(),这样进程就必须等待所有线程结束后才能终止;
- 若初始线程将其ID保存在一个其他线程可以访问的空间,则其他线程就可以等待初始线程的终止或者分离初始线程;
线程分离
- 分离一个正在运行的线程不会对线程带来任何影响,仅仅是通知系统当该线程结束时,其所属资源可以被回收;
- 分离线程意味着通知系统不再需要此线程,允许系统将分配给它的资源回收;
- 一个没有被分离的线程终止时会保留其虚拟内存,包括堆栈和其他系统资源。
2. 线程生命周期
在任意时刻,线程处于下表的四个基本状态之一。
状态 |
说明 |
就绪ready |
线程能够运行,但在等待可用的处理器;
|
运行running |
线程正在运行;在多处理器系统中,可能有多个线程处于运行状态; |
阻塞blocked |
线程由于等待处理器外的其他条件无法运行,如条件变量的改变、加锁互斥量或IO操作结束; |
终止terminated |
不是被分离,也不是被连接,一旦线程被分离或者连接,它就可以被回收;
|
线程状态转换如下图。
说明
- 线程开始处于就绪状态;
- 当线程运行时,它调用特定的起始函数;
- 它可能被其他线程抢占,或者因等待外来事情而阻塞自己;
- 最终线程完成工作,或者从起始函数返回,或者调用pthread_exit函数,即进入终止状态;
- 如果线程已被分离,则它立刻被回收重用;否则,线程停留在终止状态直到被分离或被连接;
就绪态
- 线程刚被创建时;
- 线程被解除阻塞再次可以运行时;
- 运行线程被抢占时,如时间片到;
被阻塞
- 试图加锁一个已经被锁住的互斥量;
- 等待某个条件变量;
- 调用singwait等待信号;
- 执行无法立即完成的IO操作;
- 内存页错误之类的系统操作;
初始线程(main()函数所在线程)与普通线程区别
- 初始线程的启动函数main()是从程序外部调用的;如crt0.o文件复制初始化进程并调用main()函数;而普通线程的启动函数及其运行参数均由pthread_create()函数创建线程时传入,且由CPU调度的;
- main()函数的参数是argc和argv;普通线程的参数是void*,且由pthread_create()函数传入;
- 若普通线程从启动函数中返回,则线程终止,而其他线程依然可以运行;但初始线程从main()返回时,进程终止,进程内所有线程也被终止;
- 若希望在初始线程终止时,进程中的其他线程继续执行,则需要在初始线程调中调用pthread_exit()而非从main()返回;
- 大多数系统,初始线程运行在默认进程堆栈上,该堆栈可以增长到足够尺寸;而某些实现中,普通线程的堆栈空间是受限的;
- 如果线程堆栈溢出,则程序会出现段错误;
线程睡眠原因
- 被阻塞,需要的某个资源不可用;
- 被抢占,即系统将处理器分配给其他线程;
pthread_join()的详细解释
- 用来等待一个线程的结束;
- 是一个线程阻塞函数,调用它的函数将一直等待到被等待的线程结束为止;
- 如,主线程调用pthread_join()等待它创建的线程运行结束,即主线程调用该函数后会被阻塞;
- 当函数返回时,被等待的线程的资源被回收;
- 若此时新线程没有运行,则它将在主线程被阻塞后从就绪态进入运行态;当新线程运行完毕并返回时,主线程才会被解除阻塞,返回就绪态;当处理器可用时,主线程或立即执行或等到创建的线程终止后重新运行直到结束;
线程终止
- 一般地,线程从启动函数返回来终止自己;
- 当调用pthread_exit()退出线程或者调用pthread_cancel()取消线程时,线程在调用每个清理过程后也进入终止状态;
- 清理过程又线程通过pthread_cleanup_push()注册,且尚未通过pthread_cleanup_poo()删除;
Linux系统僵尸线程
- 如果线程已经被分离,则会被回收;否则,线程处于终止状态,仍然可以被其他线程调用pthread_join()连接;
- 这种线程被称为僵尸线程,像Unix系统中的进程已经结束但还没有被一个wait/waitpid调用回收一样,即使已经死了但还存在;
- 僵尸线程可能会保留其运行时的大部分甚至所有资源,因此不应该让线程长时间处于这种状态;当创建不需要连接的线程时,应该使用detachstate属性建立线程使其自动分离;
线程回收
- 如果使用detachstate属性(即设置属性为PTHREAD_CREATE_DETACH)建立线程,或者调用pthread_detach()分离线程,则当线程结束时将被立刻回收;
- 如果终止线程没有被分离,则它将一直处于终止状态直到被分离(通过pthread_detach)或者被连接(通过pthread_join);
- 线程一旦被分离,就不能再访问它;
- 回收将释放所有在线程终止时未释放的系统和进程资源,包括
- 保存线程返回值的内存空间、堆栈;
- 保存寄存器状态的内存空间;
- 实际上线程终止时上述资源就不能被访问了;
- 一旦线程被回收,线程ID就无效了,不能再连接、取消或者执行其他任何操作;
- 终止线程ID可能被分给新线程;
NPTL (Native POSIX Threads Library)
This is the modern Pthreads implementation. By comparison with LinuxThreads, NPTL provides closer conformance to therequirements of the POSIX.1 specification and better performance when creating largenumbers of threads. NPTL is available since glibc 2.3.2, and requires features that are present in theLinux 2.6 kernel.
Thread-safe functions
A thread-safe function is one that can be safely (i.e., it will deliver the same results regardless of whetherit is) called from multiplethreads at the same time.
Reference
# man pthreads
# man pthread_create
# man pthread_join
# man pthread_exit
# man pthread_cancel
Content
0. 序
1. 基本概念
2. 互斥量的例子
3. 互斥量定义
3.1 64位系统
3.2 32位系统
3.3 pthread_mutex_t结构的内容
4. 互斥量初始化与销毁
4.1 初始化
(1) 静态初始化
(2) 动态初始化
4.2 销毁互斥量
5. 小结
0. 序
本文涉及到的glibc版本为2.11,若无特别说明,.表示glibc-2.11源代码目录,本文为/usr/src/glibc-2.11。
1. 基本概念
临界区:一个存取共享资源的代码段,而这些共享资源无法同时被多个线程访问;即影响共享数据的代码段。
线程同步方法
- 确保对相同/相关数据的内存访问互斥地进行,即一次只能允许一个线程写数据,其他线程必须等待;
- Pthreads使用特殊形式的Edsger Dijkstra信号灯——互斥量;
- mutex: mutual(相互),exclusion(排斥);
2. 互斥量的例子
下图显示了共享互斥量的三个线程的时序图。
说明
- 处于标圆形框之上的线段表示相关的线程没有拥有互斥量;
- 处于圆形框中心线之上的线段表示相关的线程等待互斥量;
- 处于圆形框中心线之下的线段表示相关的线程拥有互斥量;
过程描述
- 最初,互斥量没有被加锁;
- 当线程1试图加锁该互斥量时,因为没有竞争,线程1立即加锁成功,对应线段也移到中心线之下;
- 然后线程2试图加锁互斥量,由于互斥量已经被加锁,所以线程2被阻塞,对应线段在中心线之上;
- 接着,线程1解锁互斥量,于是线程2解除阻塞,并对互斥量加锁成功;
- 然后,线程3试图加锁互斥量,同样被阻塞;
- 此时,线程1调用函数pthread_mutext_trylock试图加锁互斥量,而立即返回EBUSY;
- 然后,线程2解锁互斥量,解除线程3的阻塞,线程3加锁成功;
- 最后,线程3完成工作,解锁互斥量;
3. 互斥量定义
3.1 64位系统
file: /usr/include/bits/pthreadtypes.h
- /* Data structures for mutex handling. The structure of the attribute
- type is not exposed on purpose. */
- typedef union
- {
- struct __pthread_mutex_s
- {
- int __lock;
- unsigned int __count;
- int __owner;
- #if __WORDSIZE == 64
- unsigned int __nusers;
- #endif
- /* KIND must stay at this position in the structure to maintain
- binary compatibility. */
- int __kind;
- #if __WORDSIZE == 64
- int __spins;
- __pthread_list_t __list;
- # define __PTHREAD_MUTEX_HAVE_PREV 1
- #else
- unsigned int __nusers;
- __extension__ union
- {
- int __spins;
- __pthread_slist_t __list;
- };
- #endif
- } __data;
- char __size[__SIZEOF_PTHREAD_MUTEX_T];
- long int __align;
- } pthread_mutex_t;
- typedef union
- {
- char __size[__SIZEOF_PTHREAD_MUTEXATTR_T];
- long int __align;
- } pthread_mutexattr_t;
该定义来自glibc,其在glibc代码中的位置为./nptl/sysdeps/unix/sysv/linux/x86_64/bits/pthreadtypes.h。64位系统在安装glibc的时候会自动拷贝该文件(x86_64版本)到/usr/include/bits目录。
其中,
- # define __SIZEOF_PTHREAD_MUTEX_T 40
- # define __SIZEOF_PTHREAD_MUTEXATTR_T 4
关于__pthread_list_t(双向链表)和__pthread_slist_t(单向链表)的定义可参考源代码。
3.2 32位系统
file: /usr/include/bits/pthreadtypes.h
- /* Data structures for mutex handling. The structure of the attribute
- type is not exposed on purpose. */
- typedef union
- {
- struct __pthread_mutex_s
- {
- int __lock;
- unsigned int __count;
- int __owner;
- /* KIND must stay at this position in the structure to maintain
- binary compatibility. */
- int __kind;
- unsigned int __nusers;
- __extension__ union
- {
- int __spins;
- __pthread_slist_t __list;
- };
- } __data;
- char __size[__SIZEOF_PTHREAD_MUTEX_T];
- long int __align;
- } pthread_mutex_t;
- typedef union
- {
- char __size[__SIZEOF_PTHREAD_MUTEXATTR_T];
- long int __align;
- } pthread_mutexattr_t;
该定义来自glibc,其在glibc代码中的位置为./nptl/sysdeps/unix/sysv/linux/i386/bits/pthreadtypes.h。32位系统在安装glibc的时候会自动拷贝该文件(i386版本)到/usr/include/bits目录。
其中,
- #define __SIZEOF_PTHREAD_MUTEX_T 24
- #define __SIZEOF_PTHREAD_MUTEXATTR_T 4
3.3 pthread_mutex_t结构的内容
如下是在64位系统的实验结果。
- (gdb) p data.mutex
- $1 = {
- __data = {
- __lock = 0,
- __count = 0,
- __owner = 0,
- __nusers = 0,
- __kind = 0,
- __spins = 0,
- __list = {
- __prev = 0x0,
- __next = 0x0
- }
- },
- __size = '\000' <repeats 39 times>,
- __align = 0
- }
4. 互斥量初始化与销毁
4.1 初始化
互斥量使用原则:使用前必须初始化,而且只被初始化一次;
(1) 静态初始化
- 使用宏PTHREAD_MUTEX_INITIALIZER声明具有默认属性的静态互斥量;
file: /usr/include/pthread.h
- /* Mutex initializers. */
- #if __WORDSIZE == 64
- # define PTHREAD_MUTEX_INITIALIZER \
- { { 0, 0, 0, 0, 0, 0, { 0, 0 } } }
- # ifdef __USE_GNU
- # define PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP \
- { { 0, 0, 0, 0, PTHREAD_MUTEX_RECURSIVE_NP, 0, { 0, 0 } } }
- # define PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP \
- { { 0, 0, 0, 0, PTHREAD_MUTEX_ERRORCHECK_NP, 0, { 0, 0 } } }
- # define PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP \
- { { 0, 0, 0, 0, PTHREAD_MUTEX_ADAPTIVE_NP, 0, { 0, 0 } } }
- # endif
- #else
- # define PTHREAD_MUTEX_INITIALIZER \
- { { 0, 0, 0, 0, 0, { 0 } } }
- # ifdef __USE_GNU
- # define PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP \
- { { 0, 0, 0, PTHREAD_MUTEX_RECURSIVE_NP, 0, { 0 } } }
- # define PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP \
- { { 0, 0, 0, PTHREAD_MUTEX_ERRORCHECK_NP, 0, { 0 } } }
- # define PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP \
- { { 0, 0, 0, PTHREAD_MUTEX_ADAPTIVE_NP, 0, { 0 } } }
- # endif
- #endif
该文件在glibc代码中的位置为./nptl/sysdeps/pthread/pthread.h。
(2) 动态初始化
- 通过pthread_mutex_init()调用动态初始化互斥量;
- 使用场合
- 当使用malloc动态分配一个包含互斥量的数据结构时,应使用动态初始化;
- 若要初始化一个非缺省属性的互斥量,必须使用动态初始化;
- 也可动态初始化静态声明的互斥量,但必须保证每个互斥量在使用前被初始化,而且只能被初始化一次;
动态初始化代码可参考./nptl/pthread_mutex_init.c文件。其中__pthread_mutex_init()函数即对mutex的各个feild进行初始化。
4.2 销毁互斥量
使用pthread_mutex_destroy()释放互斥量。
注意
- 当确信没有线程在互斥量上阻塞,且互斥量没有被锁住时,可以立即释放;
- 不需要销毁一个使用PTHREAD_MUTEX_INITIALIZER宏静态初始化的互斥量;
销毁互斥量代码可参考./nptl/pthread_mutex_destroy.c文件。其中__pthread_mutex_destroy()函数设置mutex的相应字段使其不可用。代码如下。
- int
- __pthread_mutex_destroy (mutex)
- pthread_mutex_t *mutex;
- {
- if ((mutex->__data.__kind & PTHREAD_MUTEX_ROBUST_NORMAL_NP) == 0
- && mutex->__data.__nusers != 0)
- return EBUSY;
- /* Set to an invalid value. */
- mutex->__data.__kind = -1;
- return 0;
- }
5 小结
本文简单介绍互斥量的基本概念,如何初始化和销毁及其注意问题。后文分析其加锁、解锁原理。
Reference
<Programming with POSIX Threads>