最近研究mysql源码,各种锁,各种互斥,好在我去年认真学了《unix环境高级编程》, 虽然已经忘得差不多了,但是学过始终是学过,拿起来也快。写这篇文章的目的就是总结linux 下多线程编程,作为日后的参考资料。
本文将介绍linux系统下多线程编程中,线程同步的各种方法。包括:
在介绍不同的线程同步的方法之前,先简单的介绍一下进程和线程的概念, 它们的优缺点,线程相关的API,读者——写者问题和哲学家就餐问题。
进程(process)是指在系统中正在运行的一个应用程序,是系统资源分配的基本单位,在内存 中有其完备的数据空间和代码空间,拥有完整的虚拟空间地址。一个进程所拥有的数据和变量只 属于它自己。
线程(thread)是进程内相对独立的可执行单元,所以也被称为轻量进程(lightweight processes );是操作系统进行任务调度的基本单元。它与父进程的其它线程共享该进程所拥有的全部代码 空间和全局变量,但拥有独立的堆栈(即局部变量对于线程来说是私有的)。
更加详细的介绍,请参考这里。
由于以下原因,行业内广泛地在编程库和操作系统中实现线程:
尽管线程在现代计算机中极具重要性,它们却有很多缺点:
线程的缺点与使用多进程的优点相对应:
以上关于进程和线程的优缺点,都来自《深入理解mysql核心技术》
进程原语和线程原语的比较:
进程原语 | 线程原语 | 描述 |
fork | pthread_create | 创建新的控制流 |
exit | pthread_exit | 从现有的控制流退出 |
waitpid | pthread_join | 从控制流中得到退出状态 |
atexit | pthread_cancle_push | 注册在退出控制流时调用的函数 |
getpid | pthread_self | 获取控制流的ID |
abort | pthread_cancle | 请求控制流的非正常退出 |
读者————写者问题是一个用信号量实现的经典进程同步问题。在系统中,一个数据集(如文件或 记录) 被几个并发进程共享,这些线程分两类,一部分只要求进行读操作,称之为“读者”; 另一类要求写或修改操作,我们称之为“写者“。一般而言,对一个数据集,为了保证数据的完整 性、正确性,允许多个读者进程同时访问,但是不允许一个写者进程同其它任何一个进程(读者 或者写者)同时访问,而这类问题就称之为"读者-写者"问题。
读者优先的算法在操作系统相关的书籍中都有介绍,这是一种最简单的解决办法: 当没有写进 程正在访问共享数据集时,读进程可以进入访问,否则必须等待。而读者优先的算法存在"饿死 写者"线程的问题:只要有读者不断到来,写者就要持久地等待,直到所有的读者都读完且没有 新的读者到来时写者才能写数据集。而在很多情况下我们需要避免"饿死写者",故而采用写者优 先算法:
在写者优先算法中,我们要实现的目标是:
1.要让读者与写者之间、以及写者与写者之问要互斥地访同数据集; 2.在无写进程到来时各读者可同时访问数据集; 3.在读者和写者都等待时访问时写者优先.
在实现写者优先时,增加一个互斥量,用于写者优先。当有写者来时,就不在允许读者去读取数据, 等待正在读数据的读者完成以后开始写数据,以此实现写者优先。
哲学家就餐问题可以这样表述,假设有五位哲学家围坐在一张圆形餐桌旁,做以下两件事 情之一:吃饭,或者思考。吃东西的时候,他们就停止思考,思考的时候也停止吃东西。餐桌中 间有一大碗意大利面,每两个哲学家之间有一只餐叉。因为用一只餐叉很难吃到意大利面,所以 假设哲学家必须用两只餐叉吃东西。他们只能使用自己左右手边的那两只餐叉。哲学家就餐问题 有时也用米饭和筷子而不是意大利面和餐叉来描述,因为很明显,吃米饭必须用两根筷子。
互斥锁创建
有两种方法创建互斥锁,静态方式和动态方式。POSIX定义了一个宏PTHREAD_MUTEX_INITIALIZER 来静态初始化互斥锁,方法如下:
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
在LinuxThreads实现中, pthread_mutex_t是一个结构,而PTHREAD_MUTEX_INITIALIZER则是一个结构常量。
动态方式是采用pthread_mutex_init()函数来初始化互斥锁,API定义如下:
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)
其中mutexattr用于指定互斥锁属性(见下),如果为NULL则使用缺省属性。 pthread_mutex_destroy ()用于注销一个互斥锁,API定义如下:
int pthread_mutex_destroy(pthread_mutex_t *mutex)
锁操作
锁操作主要包括加锁pthread_mutex_lock()、解锁pthread_mutex_unlock()和测试加锁 pthread_mutex_trylock()三个,不论哪种类型的锁,都不可能被两个不同的线程同时得到, 而必须等待解锁。对于普通锁和适应锁类型,解锁者可以是同进程内任何线程; 而检错锁则必须由加锁者解锁才有效,否则返回EPERM;对于嵌套锁,文档和实现要求必须由 加锁者解锁,但实验结果表明并没有这种限制,这个不同目前还没有得到解释。在同一进程中 的线程,如果加锁后没有解锁,则任何其他线程都无法再获得锁。
int pthread_mutex_lock(pthread_mutex_t *mutex) int pthread_mutex_unlock(pthread_mutex_t *mutex) int pthread_mutex_trylock(pthread_mutex_t *mutex)
pthread_mutex_trylock() 语义与pthread_mutex_lock()类似,不同的是在锁已经被占据时返回 EBUSY而不是挂起等待。
互斥量实现读者写者问题
#include <pthread.h> #include <signal.h> #include "apue.h" #define N 5 //No. of reader #define M 5 //No. of reading and writing pthread_mutex_t rd = PTHREAD_MUTEX_INITIALIZER; // it's mean reader can reading pthread_mutex_t wr = PTHREAD_MUTEX_INITIALIZER; //it's mean writer can writing int readCount = 0; void* reader(void *arg) { int n = M; int id = (int)arg; while (n--) { sleep( rand() % 3); pthread_mutex_lock(&rd); readCount++; if( readCount == 1) { pthread_mutex_lock(&wr); } pthread_mutex_unlock(&rd); printf("reader %d is reading\n", id); sleep( rand() % 3); pthread_mutex_lock(&rd); readCount--; if (readCount == 0) { pthread_mutex_unlock(&wr); } pthread_mutex_unlock(&rd); printf("reader %d is leaving\n", id); } printf("----reader %d has done----\n", (int)arg); } void* writer(void *arg) { int n = M; while (n--) { sleep( rand() % 3); pthread_mutex_lock(&wr); printf("\twriter is writing\n"); sleep( rand() % 3); pthread_mutex_unlock(&wr); printf("\twriter is leaving\n"); } printf("----writer has done----\n");