Linux 系统编程
一、线程的基本概念
线程是应用程序并发执行多种任务的一种机制,在一个进程中可以创建多个线程执行多个任务。每个进程都可以创建多个线程,创建的多个线程共享进程的地址空间,即线程被创建在进程使用的地址空间上。相较于进程来说,线程的创建不需要对地址空间进行复制,因此创建子线程比创建子进程要快得多。同一个进程中的多个线程可以并发的执行。在多处理器环境下,多个进程可以同时并行。如果一个线程因等待 I / O 操作而阻塞,那么其他线程仍然可以继续运行。同时,同一个进程创建的线程之间进行通信相对于进程来说,要方便、快速很多。原因在于线程之间是共享进程的数据段的,因此,线程间通信是通过共享数据来实现的。而进程则不同,每个进程所操作的地址空间是独立的,要实现通信、进行数据的传递需要引入进程间的通信机制来实现。下面我们将在Linux系统里面通过查看man手册来对相关线程函数进行讲解。
二、线程的相关函数
1.创建线程
在头文件#include<pthread.h>
中函数pthread_create()
用于在一个进程中创建一个线程
其中有三个参数:
thread
:传出参数,是无符号长整形,线程创建成功,会将线程ID写入到这个指针指向的内存中arrt
:线程的属性,一个pthread_attr_t类型的结构体,用以指定新创建线程的属性(如线程栈的位置和大小、线程调度策略和优先级以及线程的状态),一般默认为空 NULLstart_routine
:函数指针,创建出子线程的执行函数arg
:用于向第三个参数start_routine所指向的函数传参
- 返回值:线程成功创建返回0,否则返回对应的错误号
函数pthread_self(void)
用于返回子线程的ID。
2.线程退出
线程退出的方式很多,如下几种情况都可使线程退出:
(1)执行函数return
语句并返回指定值;
(2)线程调用pthread_exit()
函数;
(3)调用pthread_cancel()
函数取消线程;
(4)任意线程调用exit()
函数,或者main()
函数中执行了return
语句,都会照成进程中的所有线程终止。
但是如果想让线程退出,同时不会导致虚拟地址空间的释放(针对主线程),我们就可以使用线程退出函数pthread_exit()
,不管在主线程还是在子线程调用,都不影响其他线程的正常运行。
- 参数
retval
:子线程退出时携带的数据,当前子线程退出后,主线程能够通过它得到子线程的数据,可置为NULL
3.线程回收
一般来说,在子线程退出时,其内核资源主要由主线程来回收,函数pthread_join()
用于等待指定thread标识的线程终止,如果子线程在运行,调用该函数时就会阻塞,等到子线程执行完后,对子线程进程资源的回收,每次只能回收一个子线程。如果未能进行回收,线程终止时可能产生僵尸进程。
参数:
thread
:要被回收的子线程IDretval
:二级指针,指向一个存储pthread_exit()传出数据的地址,可置为NULL- 返回值:线程成功回收返回0,否则返回对应的错误号
4.简单示例:多线程的创建
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#define errlog(errmsg) do{perror("errmsg"); return -1;}while(0)
void* thread1_handle(void*arg) //子线程1
{
int cnt = *((int*)arg);
while(cnt > 0){
printf("thread1...\n");
sleep(1);
cnt --;
}
pthread_exit("thread1...exit");
}
void* thread2_handle(void*arg) //子线程2
{
int cnt = *((int*)arg);
while(cnt > 0){
printf("thread2...\n");
sleep(1);
cnt --;
}
pthread_exit("thread2...exit");
}
int main()
{
pthread_t thread1, thread2;
int arg1 = 2, arg2 = 5;
void* retval;
if(pthread_create(&thread1, NULL, thread1_handle, (void*)&arg1) != 0){ //线程的创建
errlog("pthread_create1 error");
}
if(pthread_create(&thread2, NULL, thread2_handle, (void*)&arg2) != 0){
errlog("pthread_create2 error");
}
pthread_join(thread1, &retval); //回收子线程的数据
printf("thread1: %s\n", (char*)retval);
pthread_join(thread2, &retval);
printf("thread2: %s\n", (char*)retval);
printf("------ End of file ------\n");
return 0;
}
上述代码,thread1_handle、thread2_handle分别为两个子线程的执行代码,结束后进行资源回收并输出显示。
注意:
- 编译时,编译器需要链接到线程库文件(动态库),需要通过参数指定出来,即
gcc create_p.c -g -lpthread
- 虚拟地址的生命周期和主线程是一样的,与子线程无关,要是在子线程没抢到CPU时间片的同时,主线程退出了,虚拟地址空间也被释放,子线程就会一并被销毁
- 每个线程在栈区都有一块属于自己的内存,当线程退出时,写入到栈区的数据也会被释放,即位于同一虚拟地址空间中的线程,不能共享栈区数据,但可以共享全局数据和堆区数据,也可以相互访问对方的栈空间上的数据,如子线程返回的数据可以保持到主线程的栈区内存里
5.线程分离
默认情况下,线程是可连接的(也可称为结合态),即当线程退出时,其他线程可以通过调用pthread_join()
函数来获取其返回状态。但有时候,程序中的主线程有自己的业务处理流程,若让主线程负责子线程的资源回收,调用phtread_join()
函数会直接阻塞主线程,使之不能继续执行,所以我们可以调用pthread_detach()
函数并向thread参数传入指定的标识符,将该线程标记为分离状态(分离态),就能不再阻塞主线程的执行。
一旦子线程进入分离状态,就不能用pthread_join()
函数进行获取其状态,也无法返回 “可连接” 状态。
实现线程分离
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#define errlog(errmsg) do{perror("errmsg"); return -1;}while(0)
void* thread_handler(void*arg)
{
pthread_detach(pthread_self());
int count = *((int*)arg);
while(count > 0){
printf("thread...\n");
sleep(1);
count --;
}
return NULL;
}
int main()
{
pthread_t thread;
int arg = 3;
if(pthread_create(&thread, NULL, thread_handler, (void*)&arg) != 0){
errlog("pthread_create thread error");
}
sleep(2);
if(pthread_join(thread, NULL) == 0){
printf("pthread wait success\n");
}
else{
printf("pthread wait failed\n");
}
return 0;
}
此时返回子线程回收失败
对于上述设置线程分离的方法,也可以在线程刚创建时即进行分离(而非之后才调用pthread_detach()函数)
int pthread_attr_init(pthread_atttr_t * attr);
int pthread_attr_destroy(pthread_attr_t * attr);
int pthread_attr_setdetachstate(pthread_attr_t * attr, int detachstate);
......
这里就不写了,本人比较懒 详细信息请在Linux环境下使用man手册自查
6.线程的取消
一般情况下,多个线程会并发的执行,各自处理各自的任务,但有时候也需要用到线程的取消,请求立即退出。例如,某个线程检测到错误发生时。
pthread_cancel()
函数向由thread指定的线程发送一个取消请求,发送请求后立即返回。
pthread_setcancelstate()函数设置线程的取消状态,有两个参数:
- state:PTHREAD_CANCEL_ENABLE(可取消)、PTHREAD_CANCEL_DISABLE(不可取消),默认为前者
- oldstate:用于保存前一次状态,默认为NULL
pthread_setcanceltype()函数设置可以取消的类型,有两个参数:
- type:PTHREAD_CANCEL_DEFERRED(设置可取消点)、PTHREAD_CANCEL_ASYNCHRONOUS(立即取消)
- oldtype:保存前一次的取消类型,默认为NULL
示例:设置线程不可取消
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#define errlog(errmsg) do{perror(errmsg); return -1;} while(0)
void* thread_handler(void* arg)
{
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); //不可取消
while(1){
printf("thread...\n");
sleep(1);
}
pthread_exit(0);
}
int main()
{
pthread_t thread;
if(pthread_create(&thread, NULL, thread_handler, NULL) != 0){
errlog("pthread_create error");
}
sleep(3);
pthread_cancel(thread);
pthread_join(thread, NULL);
return 0;
}
线程进入死循环,不会被取消请求退出,^C为将整个进程退出
示例:设置线程取消状态
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#define errlog(errmsg) do{perror(errmsg); return -1;} while(0)
void* thread_handler(void* arg)
{
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL);//运行到可取消点
while(1){
//pthread_testcancel(pthread_self());
}
return NULL;
}
int main()
{
pthread_t thread;
for(int i = 1;i <= 3; i ++){
printf("processing...\n");
sleep(1);
}
if(pthread_create(&thread, NULL, thread_handler, NULL) != 0){
errlog("pthread_create error");
}
pthread_cancel(thread);
pthread_join(thread, NULL);
return 0;
}
显然,其结果显然不是我们想要的,子线程会一直执行 while(1) ,而在主线程中并未执行pthread_cancel()
函数
为什么呢 要想取消线程,必须进入内核,到达一个取消点,即进行系统调用,但子线程中在设置了可取消点之外,就只有while语句,其不需要进入内核,也就不会进行系统调用,也就到不了可取消点。若要解决这个问题,此时我们可以直接给当前线程设置一个“可取消点”,即把 while(1) 中的注释去掉,再运行一遍,这时线程就在取消点被取消结束了。
关于C++线程类的使用,可以点击这个链接 爱编程的大丙 查看
三、线程通信
前面已经介绍了线程的基本编程,然而没有对线程进行实质性的信息传递,即多线程的通信。线程不同于进程的是多线程间共享进程的虚拟地址空间,而线程通信虽然很容易,但也有其弊端,正是因为并发的线程访问了相同的资源,所有造成了数据的不确定性。因此,线程间的通信需要结合一些同步互斥机制使用。
下面我们先看一段代码:
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#define errlog(errmsg) do{perror(errmsg); return -1;} while(0)
int v1, v2;
int cnt = 0;
pthread_mutex_t mutex;
void* thread1_handler(void*arg)
{
while(1){
//pthread_mutex_lock(&mutex);
v1 = cnt;
v2 = cnt;
cnt ++;
//pthread_mutex_unlock(&mutex);
}
pthread_exit("thread1...exit\n");
}
void* thread2_handler(void*arg)
{
while(1){
//pthread_mutex_lock(&mutex);
if(v1 != v2){
sleep(1);
printf("v1 = %d, v2 = %d\n", v1, v2);
}
//pthread_mutex_unlock(&mutex);
}
pthread_exit("thread2...exit\n");
}
int main()
{
pthread_t thread1, thread2;
void* retval;
if(pthread_mutex_init(&mutex, NULL) != 0){
errlog("mutex_init error");
}
if(pthread_create(&thread1, NULL, thread1_handler, NULL) != 0){
errlog("create thread1 error");
}
if(pthread_create(&thread2, NULL, thread2_handler, NULL) != 0){
errlog("create thread2 error");
}
pthread_join(thread1, &retval);
printf("%s\n", (char*)retval);
pthread_join(thread2, &retval);
printf("%s\n", (char*)retval);
pthread_mutex_destroy(&mutex);
return 0;
}
对其分析,从单独的线程角度看,线程1将全局变量 cnt 的值赋给 v1 和 v2 的值都是相等的。而线程2则判断 v1 和 v2 是否相等,如果不相等则输出二者的值。要是线程1是完整执行的话,那么value 和 value2 的值一定会是相等的,即线程2执行时,应该不会在终端上输出任何信息,然而结果却并非如此
可以看出程序运行时相等或者不相等的情况都出现了,这就是一种典型的竞态的产生,线程并没有按照预想的结果有秩序的范围共享资源。
四、互斥锁的使用
上述代码的运行并未达到我们想要的效果,其原因就是线程在访问共享资源时被其他线程打断,其他线程也开始访问共享资源导致了数据的不确定性。这里将介绍一种互斥机制,用于保护对共享资源的操作,即当一个线程在访问共享资源的同时,其他线程要想访问,必须等该线程操作完毕后才能进行。我们通常把涉及到共享资源的操作的代码段,称为临界区,其共享资源也可以被称为临界资源。互斥锁的原理就是对临界区进行加锁,使处于临界区的线程不被其他线程打断,保证临界区运行的完整。
互斥锁的使用包括初始化、上锁、解锁、释放
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
pthread_mutex_init()
函数用来实现互斥锁的初始化:
- mutex:用来指定互斥锁的标识符,类似ID;
- attr:互斥锁属性,一般设置为NULL
pthread_mutex_destroy()
函数为释放互斥锁,参数为指定互斥锁的标识符
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
初始化互斥锁之后,互斥锁处于未锁定的状态,pthread_mutex_lock()
函数为上锁处理,如果该锁资源处于持有状态,那么函数直接导致线程阻塞,直到其他线程使用pthread_mutex_unlock()
函数进行解锁。
对于上述的代码,只需将那两行注释去掉,即可实现共享资源的正常访问。我们还需要注意的是如果多线程同时对一个共享资源进行访问,其中一个线程采用了互斥锁的机制,其他线程也必须遵循该规则。 Pthread API 还提供了pthread_mutex_trylock()
、pthread_mutex_timelock()
两个函数,具体的这里就不讲了,参考man手册。
五、死锁的产生
互斥锁在默认的情况下使用,一般需要关注死锁的问题。所谓死锁,即互斥锁无法解除同时也无法加持,导致程序可能无限阻塞的情况。有时,一个线程可能会同时访问多个不同的共享资源,而每个共享资源都需要有不同的互斥锁来管理。那么很容易就好造成死锁的情况,情况如下:
- 在互斥锁默认属性的情况下,在同一个线程中不允许对统一互斥锁进行连续加锁操作。因为之前锁处于未解除状态,如果再次对同一个互斥锁进行加锁,那么必然会导致程序无限期等待
- 多个线程对多个互斥锁交叉使用,每一个线程都试图对其他线程所持有的互斥锁进行加锁,线程分别持有了对方需要的锁资源,并相互影响,可能会导致程序无限期等待。 注意 :在一个线程中操作多把锁时,加锁与解锁的顺序一定是相反的,否则也会导致错误
- 一个持有互斥锁的线程如果被其他线程取消,其他线程将无法获得该锁,则会造成死锁,如下代码所示:
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#define errlog(msg) do{perror(msg); return -1;}while(0)
int v1, v2;
int cnt = 2;
pthread_mutex_t mutex;
void cleanup_handler(void* arg)
{
pthread_mutex_unlock(&mutex);
}
void* thread1_handler(void*arg)
{
while(1){
pthread_mutex_lock(&mutex);
// pthread_cleanup_push(cleanup_handler, NULL);
v1 = cnt;
v2 = cnt;
cnt ++;
sleep(2);
//pthread_cleanup_pop(0);
pthread_mutex_unlock(&mutex);
}
pthread_exit(0);
}
void* thread2_handler(void*arg)
{
while(1){
sleep(1);
pthread_mutex_lock(&mutex);
if(v1 == v2){
sleep(1);
printf("v1 = %d, v2 = %d\n", v1, v2);
}
pthread_mutex_unlock(&mutex);
}
pthread_exit(0);
}
int main()
{
pthread_t thread1, thread2;
void* retval;
if(pthread_mutex_init(&mutex, NULL) != 0){
errlog("init error");
}
if(pthread_create(&thread1, NULL, thread1_handler, NULL) != 0){
errlog("create thread1 errror");
}
if(pthread_create(&thread2, NULL, thread2_handler, NULL) != 0){
errlog("create thread2 error");
}
printf("ready to cancel thread1...\n");
sleep(2);
pthread_cancel(thread1);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
pthread_mutex_destroy(&mutex);
return 0;
}
如上,对线程进行延迟处理,确保线程1先获得互斥锁,并在线程1持有互斥锁期间被取消,此时线程1使用的互斥锁将无法被线程2获取,造成死锁。为了规避这些问题,线程可以设置一个或者多个清理函数,当线程遭到取消是会自动运行这些函数。取消时,会沿该栈自顶向下依次执行清理函数。当执行完所有的清理函数后,线程终止。pthread_cleanup_push()
函数和pthread_cleanup_pop()
函数分别负责向调用线程的清理函数栈添加和移除清理函数。
执行
pthread_cleanup_push()
函数会将参数 routine 所含的函数地址添加到调用线程的清理函数栈顶, arg 作为调用函数的参数传递给routine。
自动调用清理函数的条件
- 线程调用 pthread_exit() 函数
- 线程被 pthread_cancel() 函数取消
- 线程调用 pthread_cleanup_pop() 且参数 execute 为 0 时
所有只要将上述代码中的两行注释删去,就能实现自动清理函数栈,进而解除死锁的情况,此时的输出结果如下:
对于互斥锁的属性,还有几个函数pthread_mutexattr_init()
、pthread_mutexattr_settype()
、pthread_mutexattr_gettype()
这里就不讲了
六、信号量的使用
信号量本身代表一种资源,其本质是一个非负的整数计算器,用来控制对公共资源的访问。即信号量的核心内容是信号量的值。其工作原理为:所有对共享资源操作的线程,在访问共享资源以前,都需要先操作信号量的值。操作信号量的值又可称为 PV 操作, P 操作为申请信号量,V 操作为释放信号量。当申请信号量成功是,信号量的值减一,而释放信号量成功时,信号量加一,但是当信号量的值为0时,申请信号量会发生阻塞,其值不能减为负数,合理的利用这一特性,即可以实现对共享资源访问的控制。
信号量作为一种同步互斥机制:
- 若用于实现互斥,多线程只需要设置一个信号量
- 若用于实现同步时,则需要设置多个信号量,并通过设置不同的信号量的初始值来实现线程的执行顺序
下面将介绍基于POSIX的无名信号量,与互斥锁类似:
sen_init()
函数用来信号量的初始化,参数如下
- sem:信号量的标识符
- pshared:设置信号量的使用环境,0 表示用于同一个进程间的多线程使用,非 0 表示进程间使用
- value:信号量的初始值
int sem_destroy(sem_t *sem) //表示用来摧毁信号量
sem_wait()
函数用来执行申请信号量操作,当申请成功时,信号量的值减一,当信号量的值为0时,将会阻塞,直到其他线程执行释放信号量。sem_trywait()
函数与sem_wait()
函数类似,唯一的区别在于sem_trywait()
函数不会阻塞,当信号量为0时,函数直接返回错误码 EAGAIN 。sem_timewait()
函数同样,多了参数 abs_timeout ,用来设置时间限制,在该时间内若仍然不能申请,那么函数不会一直阻塞,而是返回错误码 ETIMEOUT。
int sem_post(sem_t *sem); //用于信号量的释放,成功时信号量加 1
int sem_getvalue(sem_t *sem, int *sval); //用于获取信号量的值,并保存到参数2中
下面我们来看个示例:
#include <stdio.h>
#include <semaphore.h>
#include <pthread.h>
#include <stdlib.h>
#define N 32
#define errlog(msg) do{perror(msg); return -1;}while(0)
char buf[N];
//sem_t sem1, sem2;
void* thread1_handler(void*arg)
{
while(1){
//sem_wait(&sem2);
fgets(buf, N, stdin);
buf[sizeof(buf) - 1] = '\0';
//sem_post(&sem1);
}
pthread_exit("thread1....eixt\n");
}
void* thread2_handler(void*arg)
{
while(1){
//sem_wait(&sem1);
printf("buf: %s\n", buf);
sleep(1);
//sem_post(&sem2);
}
pthread_exit("thread2....eixt\n");
}
int main()
{
// if(sem_init(&sem1, 0, 0) < 0)
// errlog("sem1_init error");
// if(sem_init(&sem2, 0, 1) < 0)
// errlog("sem2_init error");
pthread_t thread1, thread2;
if(pthread_create(&thread1, NULL, thread1_handler, NULL) != 0)
errlog("pthread_create_thread1 error");
if(pthread_create(&thread2, NULL, thread2_handler, NULL) != 0)
errlog("pthread_create_thread2 error");
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
return 0;
}
依据线程的基本知识,我们可以知道该代码的执行结果:两个线程会交替进行操作,当终端输入内容后(线程1读取输入内容),线程2则读取输入的内容并一直打印输出,显然不是我们的本意。我们的目的是让线程1进行写数据,线程2进行读数据,并且保证数据的实时、有效,因此我们可以在这引入两个信号量,实现同步的操作,使线程按照一定的顺序实现写入和读取,即将上述代码中的注释去掉,其结果如下
七、条件变量的使用
条件变量的使用很简单,即让当前不需要访问共享资源的线程进行阻塞等待(睡眠),如果某一时刻就共享资源的状态改变需要某一个线程来处理,那么则可以通知该线程进行处理(唤醒)。条件变量可以看出是互斥锁的补充,因为条件变量需要结合互斥锁一起使用,之所以这样,是因为互斥锁的状态只有锁定和非锁定两种,无法决定线程执行先后,有一点的局限。而条件变量可以通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足。
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_cond_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *restrict cond);
pthread_cond_init()
函数为初始化条件变量,参数:
- cond:条件变量的标识符
- attr:设置条件变量的属性,通常为NULL,执行默认属性
pthread_cond_destroy()
函数为摧毁一个条件变量
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
pthread_cond_signal()
函数的功能为发送至少一个处于阻塞等待的线程,使其脱离阻塞的状态,继续执行。如果没有线程处于阻塞状态,pthread_cond_signal()
函数也会成功返回。pthread_cond_broadcast()
函数能够唤醒当前条件变量所指定的所有阻塞等待的线程,前者用的频率更高。
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
int pthread_cond_timewait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, conse struct timespc *restrict abstime);
pthread_cond_wait()
函数用于使线程进入睡眠状态,当使用它时,线程进入阻塞状态,必须先进行加锁操作,之和再进行解锁。即pthread_cond_wait()
函数必须放在pthread_mutex_lock()
函数和pthread_mutex_unlock()
函数之间。注意: 一旦函数pthread_cond_wait()
实现阻塞使线程进入睡眠后,函数自身会自动释放之前持有的互斥锁。它不等同与唤醒操作,睡眠操作必须要想进行加锁。下面给出示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <semaphore.h>
#include <pthread.h>
#define N 128
#define errlog(msg) do{perror(msg); return -1;}while(0)
pthread_mutex_t mutex;
pthread_cond_t cond;
char buf[N];
void* thread1_handler(void*arg)
{
while(1)
{
fgets(buf, N, stdin);
buf[sizeof(buf) - 1] = '\0';
pthread_cond_signal(&cond);
}
pthread_exit(0);
}
void* thread2_handler(void*arg)
{
while(1)
{
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond, &mutex);
printf("thread2 buf: %s\n", buf);
sleep(1);
pthread_mutex_unlock(&mutex);
}
pthread_exit(0);
}
void* thread3_handler(void*arg)
{
while(1)
{
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond, &mutex);
printf("thread3 buf: %s\n", buf);
sleep(1);
pthread_mutex_unlock(&mutex);
}
pthread_exit(0);
}
int main()
{
pthread_t thread1, thread2, thread3;
if(pthread_mutex_init(&mutex, NULL) != 0)
errlog("mutex error");
if(pthread_cond_init(&cond, NULL) != 0)
errlog("cond error");
if(pthread_create(&thread1, NULL, thread1_handler, NULL) != 0)
errlog("thread1_create error");
if(pthread_create(&thread2, NULL, thread2_handler, NULL) != 0)
errlog("thread2_create error");
if(pthread_create(&thread3, NULL, thread3_handler, NULL) != 0)
errlog("thread3_create error");
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
pthread_join(thread3, NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
上述代码的目的是使线程1写数据,线程2和线程3进行读数据,其读数据的情况取决于当时执行的情况
pthread_mutex_lock(&mutex); //执行加锁操作
pthread_cond_wait(&cond, &mutex); //线程阻塞,此时自动执行解锁
//当线程收到唤醒信号,函数立刻返回此时进入临界区前,再次自动加锁
… //临界区
pthread_mutex_unlock(&mutex); //解除互斥锁
线程在睡眠之前进行加锁操作,这是必须的,此时互斥锁是对 pthread_cond_wait 函数的保护,保证在线程睡眠的过程中是不会被打断的,一旦线程睡眠成功,那么此时 phtread_cond_wait 函数除了阻塞外,还将之前持有的互斥锁解除,供其他线程加锁用,并进行睡眠,避免了死锁的产生。当线程被唤醒时,pthread_cond_wait 函数立刻返回,再次对该线程进行加锁,访问临界区的资源,保证了共享资源的完整,即让任意时刻只有一个线程在访问共享资源。当访问完共享资源后,再进行解锁操作。因此,上述线程的睡眠操作涉及了两次加锁、两次解锁的处理,线程1中的 fgets()
函数本身就是读取终端输入,直接就能阻塞,因此不需要引入互斥锁。
互斥锁与条件变量的配合使用
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <semaphore.h>
#define N 128
#define errlog(msg) do{perror(msg) ;return -1;}while(0)
char buf[N];
pthread_mutex_t mutex;
pthread_cond_t cond;
int count = 0;
void* thread1_handler(void*arg)
{
while(1)
{
printf("thread1_count = %d\n", ++count);
sleep(1);
pthread_mutex_lock(&mutex);
strcpy(buf, "hello");
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond);
}
pthread_exit(0);
}
void* thread2_handler(void*arg)
{
while(1)
{
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond, &mutex);
printf("thread2_buf: %s\n", buf);
sleep(1);
pthread_mutex_unlock(&mutex);
}
pthread_exit(0);
}
void* thread3_handler(void*arg)
{
while(1)
{
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond, &mutex);
printf("thread3_buf: %s\n", buf);
sleep(1);
pthread_mutex_unlock(&mutex);
}
pthread_exit(0);
}
int main()
{
pthread_t thread1, thread2, thread3;
if(pthread_mutex_init(&mutex, NULL) != 0)
errlog("mutex_init error");
if(pthread_cond_init(&cond, NULL) != 0)
errlog("cond_init error");
if(pthread_create(&thread1, NULL, thread1_handler, NULL) != 0)
errlog("pthread1_create errro");
if(pthread_create(&thread2, NULL, thread2_handler, NULL) != 0)
errlog("pthread2_create errro");
if(pthread_create(&thread3, NULL, thread3_handler, NULL) != 0)
errlog("pthread3_create errro");
pthread_join(thread1, NULL);
pthread_join(thread1, NULL);
pthread_join(thread1, NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
以下结果请读者自己分析研究。需要注意的是,唤醒操作一定是要在睡眠之后。
八、线程池(待更)
参考资料:《Linux系统编程》、爱编程的大丙