2 线程同步
- 线程同步中的“同步”与生活中大家认知的“同步”略有不同,“同”不指同时,其主旨在于协同步调,按预定的先后次序执行线程;
- 之所以需要实现线程同步,是因为若不对线程的执行次序加以控制,可能会出现数据混乱。
出现与事件有关的错误的原因有三个:
- (1)资源共享;
- (2)调度随机;
- (3)线程间缺乏必要的同步机制。
Linux系统实现线程同步的方式常用的有三种:
- 互斥锁;
- 条件变量;
- 信号量。
2.1 互斥锁
使用互斥锁的实现线程同步时主要操作分为四步:
①初始化互斥锁:pthread_mutex_init
②加锁:pthread_mutex_lock
③解锁:pthread_mutex_unlock
④销毁锁:pthread_mutxe_destroy
2.1.1 初始化互斥锁
#include <pthread.h>int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
功能:初始化互斥锁。
参数说明:
- mutex:一个传入传出参数:
– ①pthread_mutext_t的本质是结构体,为简化理解,读者可将其视为整型;
– ②变量mutex只有两种取值:0和1,加锁:mutex-1;解锁:mutex+1;
– ③参数mutex之前的restrict是一个关键字,该关键字用于限制指针,功能是告诉编译器,所有修改该指针指向内容的操作,只能通过本指针完成。 - attr:一个传入传出参数,代表互斥量的属性,通常传NULL,表示使用默认属性。
返回值说明:
- 成功:返回0;
- 不成功:返回errno,errno的常见取值为EAGAIN和EDEADLK,其中EAGAIN表示超出互斥锁递归锁定的最大次数,因此无法获取该互斥锁;EDEADLK表示当前线程已有互斥锁,二次加锁失败。
2.1.2 加锁
#include <pthread.h>int pthread_mutex_lock(pthread_mutex_t *mutex);
功能:锁定指定互斥量。
参数说明:
- mutex:待锁定的互斥量。
返回值说明:
- 成功:返回0;
- 不成功:返回errno。
#include <pthread.h>int pthread_mutex_trylock(pthread_mutex_t *mutex);
功能:尝试加锁,若锁正在被使用,不阻塞等待,而是直接返回并返回错误号。
参数说明:
- mutex:待锁定的互斥量。
返回值说明:
- 成功:返回0;
- 不成功:返回errno,其中常见的errno有两个,分别为EBUSY和EAGAIN,它们代表的含义如下:
– EBUSY:参数mutex指向的互斥锁已锁定;
– EAGAIN:超过互斥锁递归锁定的最大次数。
2.1.3 解锁
#include <pthread.h>int pthread_mutex_unlock(pthread_mutex_t *mutex);
功能:为指定互斥量解锁。
参数说明:
- mutex:待解锁的互斥量。
返回值说明:
- 成功:返回0;
- 不成功:返回errno。
2.1.4 销毁锁
#include <pthread.h>int pthread_mutex_destroy(pthread_mutex_t *mutex);
功能:为指定互斥量销毁。
参数说明:
- mutex:待销毁的互斥量。
返回值说明:
- 成功:返回0;
- 不成功:返回errno。
【案例 1】在原线程和新线程中分别进行打印操作,使原线程分别打印“HELLO”、“ WORLD”,新线程分别打印“hello”、“world”。
//未添加mutex
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void *tfn(void *paraArg) {srand(time(NULL));while (1) {printf("hello ");//模拟长时间操作共享资源,导致cpu易主,产生与时间有关的错误sleep(rand() % 3);printf("world\n");sleep(rand() % 3);}//of whilereturn NULL;
}//of tfn
int main(void) {pthread_t tempTid;srand(time(NULL));pthread_create(&tempTid, NULL, tfn, NULL);while (1) {printf("HELLO ");sleep(rand() % 3);printf("WORLD\n");sleep(rand() % 3);}//of whilepthread_join(tempTid, NULL);return 0;
}//of main
未添加互斥量,会导致打印乱序。
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
pthread_mutex_t globMutux; //定义互斥锁
void err_thread(int paraRet, char *paraStr) {if (paraRet != 0) {fprintf(stderr, "%s:%s\n", paraStr, strerror(paraRet));pthread_exit(NULL);}//of if
}//of err_thread
void *tfn(void *paraArg) {srand(time(NULL));while (1) {pthread_mutex_lock(&globMutux); //加锁:m--printf("hello ");//模拟长时间操作共享资源,导致cpu易主,产生与时间有关的错误sleep(rand() % 3);printf("world\n");pthread_mutex_unlock(&globMutux); //解锁:m++sleep(rand() % 3);}//of whilereturn NULL;
}//of tfn
int main(void) {pthread_t tempTid;srand(time(NULL));int tempFlag = 5;pthread_mutex_init(&globMutux, NULL); //初始化mutex:m=1int tempRet = pthread_create(&tempTid, NULL, tfn, NULL);err_thread(tempRet, "pthread_create error");while (tempFlag--) {pthread_mutex_lock(&globMutux); //加锁:m--printf("HELLO ");sleep(rand() % 3);printf("WORLD\n");pthread_mutex_unlock(&globMutux); //解锁:m--sleep(rand() % 3);}//of whilepthread_cancel(tempTid);pthread_join(tempTid, NULL);pthread_mutex_destroy(&globMutux);return 0;
}//of main
2.2 条件变量
使用条件变量控制线程同步时,线程访问共享资源的前提,是程序中设置的条件变量得到满足。条件变量不会对共享资源加锁,但也会使线程阻塞,若线程不满足条件变量规定的条件,就会进入阻塞状态直到条件满足。
条件变量的使用分为以下四个步骤:
- (1)初始化条件变量:pthread_cond_init();
- (2)等待条件变量满足:pthread_cond_wait();
- (3)唤醒阻塞线程:pthread_cond_signal()、pthread_cond_broadcast();
- (4)释放条件变量:pthread_cond_destroy()。
2.2.1 初始化条件变量
#include <pthread.h>int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
功能:初始化条件变量。
参数说明:
- cond:代表条件变量,一个指向pthread_cond_t的结构体指针,pthread_cond_t是Linux系统定义的条件变量类型;
- attr:代表条件变量的属性:
– NULL:表示使用默认属性初始化条件变量;
– PTHREAD_PROCESS_PRIVATE:表示当前进程中的线程共用此条件变量;
– PTHREAD_PROCESS_SHARED:表示多个进程间的线程共用条件变量。
返回值说明:
- 成功:返回0;
- 不成功:返回-1并设置errno。
2.2.2 等待条件变量满足
#include <pthread.h>int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
功能:使线程进入阻塞状态,等待一个条件变量后继续执行。pthread_cond_wait类似于互斥锁的pthread_mutex_lock函数,但其功能更为丰富,它的工作机制如下:
- (1)阻塞等待条件变量cond满足;
- (2)解除已绑定的互斥锁(类似于pthread_mutex_unlock);
- (3)当线程被唤醒,pthread_cond_wait函数返回,pthread_cond_wait函数同时会解除线程阻塞,并使线程重新申请绑定互斥锁。
条件变量控制流程示意图
参数说明:
- cond:代表条件变量,一个指向pthread_cond_t的结构体指针,pthread_cond_t是Linux系统定义的条件变量类型;
- mutex:代表与当前线程绑定的互斥锁。
返回值说明:
- 成功:返回0;
- 不成功:返回-1并设置errno。
#include <pthread.h>int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex,const struct timespec *restrict abstime);
struct timespec {time_t tv_sec; //秒long tv_nsec; //纳秒
};
功能:使线程阻塞等待条件变量,不同的是,该函数可以指定线程的阻塞时长,若等待超时,该函数便会返回。
参数说明:
- cond:代表条件变量,一个指向pthread_cond_t的结构体指针,pthread_cond_t是Linux系统定义的条件变量类型;
- mutex:代表与当前线程绑定的互斥锁;
- abstime:等待时长。
返回值说明:
- 成功:返回0;
- 不成功:返回-1并设置errno。
2.2.3 唤醒条件变量
#include <pthread.h>int pthread_cond_signal(pthread_cond_t *cond);
功能:在条件变量满足之后,以信号的形式唤醒阻塞在该条件变量的一个线程。处于阻塞状态中的线程的唤醒顺序由调度策略决定。
参数说明:
- cond:代表条件变量,一个指向pthread_cond_t的结构体指针,pthread_cond_t是Linux系统定义的条件变量类型。
返回值说明:
- 成功:返回0;
- 不成功:返回-1并设置errno。
#include <pthread.h>int pthread_cond_broadcast(pthread_cond_t *cond);
功能:唤醒阻塞在指定条件变量的线程,不同的是,该函数会以广播的形式,唤醒阻塞在该条件变量上的所有线程。
参数说明:
- cond:代表条件变量,一个指向pthread_cond_t的结构体指针,pthread_cond_t是Linux系统定义的条件变量类型。
返回值说明:
- 成功:返回0;
- 不成功:返回-1并设置errno。
2.2.4 销毁条件变量
#include <pthread.h>int pthread_cond_destory(pthread_cond_t *cond);
功能:当没有线程在等待参数cond指定的条件变量时,才可以销毁条件变量。
参数说明:
- cond:代表条件变量,一个指向pthread_cond_t的结构体指针,pthread_cond_t是Linux系统定义的条件变量类型。
返回值说明:
- 成功:返回0;
- 不成功:返回EBUSY。
【案例2】生产者-消费者模型是线程同步中的一个经典案例。假设有两个线程,这两个线程同时操作一个共享资源(一般称为汇聚),其中一个模拟生产者行为,生产共享资源,当容器存满时,生产者无法向其中放入产品;另一个线程模拟消费者行为,消费共享资源,当产品数量为0时,消费者无法获取产品,应阻塞等待。显然,为防止数据混乱,每次只能由生产者、消费者中的一个,操作共享资源。本案例要求使用程序实现简单的生产者-消费者模型(可假设容器无限大)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
struct msg {struct msg *next;int num;
};
struct msg *globHead;
pthread_cond_t globHasProduct = PTHREAD_COND_INITIALIZER; //初始化条件变量
pthread_mutex_t globLock = PTHREAD_MUTEX_INITIALIZER; //初始化互斥锁
//消费者
void *consumer(void *paraP) {struct msg *tempMsgP;for (;;) {pthread_mutex_lock(&globLock); //加锁//若头结点为空,表明产品数量为0,消费者无法消费产品while (globHead == NULL) {pthread_cond_wait(&globHasProduct, &globLock); //阻塞等待并解锁}//of whiletempMsgP = globHead;globHead = tempMsgP->next; //模拟消费一个产品pthread_mutex_unlock(&globLock);printf("-Consume ---%d\n", tempMsgP->num);free(tempMsgP);sleep(rand() % 5);}//of for
}//of consumer
//生产者
void *producer(void *paraP) {struct msg *tempMsgP;while (1) {tempMsgP = malloc(sizeof(struct msg));tempMsgP->num = rand() % 1000 + 1; //模拟生产一个产品printf("-Produce ---%d\n", tempMsgP->num);pthread_mutex_lock(&globLock); //加锁tempMsgP->next = globHead; //插入结点(添加产品)globHead = tempMsgP;pthread_mutex_unlock(&globLock); //解锁pthread_cond_signal(&globHasProduct); //唤醒等待在该条件变量上的一个线程sleep(rand() % 5);}//of while
}//of producer
int main(int argc, char *argv[]) {pthread_t tempPid, tempCid;srand(time(NULL));//创建生产者、消费者线程pthread_create(&tempPid, NULL, producer, NULL);pthread_create(&tempCid, NULL, consumer, NULL);//回收线程pthread_join(tempPid, NULL);pthread_join(tempCid, NULL);return 0;
}//of main
2.3 信号量
使用信号量实现线程同步时,线程在访问共享资源时会根据操作类型执行如下操作:
- 若有线程申请访问共享资源,系统会执行P操作使共享资源计数减一;
- 若有线程释放共享资源,系统会执行V操作使共享资源计数加一。
信号量的使用也分为四个步骤:
- (1)初始化信号量:sem_init();
- (2)阻塞等待信号量:sem_wait();
- (3)唤醒阻塞线程:sem_post();
- (4)释放信号量:sem_destroy()。
2.3.1 初始化信号量
#include <pthread.h>int sem_init(sem_t *sem, int pshared, unsigned int value);
功能:初始化信号量。
参数说明:
- sem:指向信号量变量的指针;
- pshared:用于控制信号量的作用范围,其取值通常为0与非0:
– 当pshared被设置为0时,信号量将会被放在进程中所有线程可见的地址内,由进程中的线程共享;
– 当pshared被设置为非0值时,信号量将会被放置在共享内存区域,由所有进程共享。 - value:设置信号量sem的初值。
返回值说明:
- 成功:返回0;
- 不成功:返回-1,并设置errno。
2.3.2 阻塞等待信号量
#include <pthread.h>int sem_wait(sem_t *sem);
功能:阻塞等待信号量,sem_wait函数对应P操作,若调用成功,则会使信号量sem的值减一。
参数说明:
- sem:指向信号量变量的指针。
返回值说明:
- 成功:返回0;
- 不成功:返回-1,并设置errno。
2.3.3 唤醒阻塞线程
#include <pthread.h>int sem_post(sem_t *sem);
功能:唤醒阻塞线程,sem_post函数对应V操作,若调用成功,则会使信号量sem的值加一。
参数说明:
- sem:指向信号量变量的指针。
返回值说明:
- 成功:返回0;
- 不成功:返回-1,并设置errno。
2.3.4 释放信号量
#include <pthread.h>int sem_destroy(sem_t *sem);
功能:与互斥锁类似,信号量也是一种系统资源,使用完毕之后应主动回收,函数调用成功,则会使信号量sem的值加一。
参数说明:
- sem:指向信号量变量的指针。
返回值说明:
- 成功:返回0;
- 不成功:返回-1,并设置errno。
2.3.5 获取信号量
#include <pthread.h>int sem_getvalue(sem_t *sem, int *sval);
功能:获取系统中当前信号量的值。
参数说明:
- sem:指向信号量变量的指针;
- sval:个传入指针,用于获取信号量的值,信号量sem的值会被存储在参数sval中。
返回值说明:
- 成功:返回0;
- 不成功:返回-1,并设置errno。
【案例 3】实现一个模拟生产者-消费者模型,但对生产者进行限制:若容器已满,生产者不能生产,需等待消费者消费。
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <stdio.h>
#include <semaphore.h>
#define NUM 5
int globQueue[NUM]; //全局数组实现环形队列
sem_t globBlankNum, globProductNum; //空格子信号量, 产品信号量
void *producer(void *paraArg) {int i = 0;while (1) {sem_wait(&globBlankNum); //生产者将空格子数--,为0则阻塞等待globQueue[i] = rand() % 1000 + 1; //生产一个产品printf("----Produce---%d\n", globQueue[i]);sem_post(&globProductNum); //将产品数++i = (i + 1) % NUM; //借助下标实现环形sleep(rand() % 1);}//of while
}//of producer
void *consumer(void *paraArg) {int i = 0;while (1) {sem_wait(&globProductNum); //消费者将产品数--,为0则阻塞等待printf("-Consume---%d\t%lu\n", globQueue[i], pthread_self());globQueue[i] = 0; //消费一个产品 sem_post(&globBlankNum); //消费掉以后,将空格子数++i = (i + 1) % NUM;sleep(rand() % 1);}//of while
}//of consumer
int main(int paraArgc, char *paraArgv[]) {pthread_t tempPid, tempCid;sem_init(&globBlankNum, 0, NUM); //初始化空格子信号量为5sem_init(&globProductNum, 0, 0); //初始化产品数信号量为0pthread_create(&tempPid, NULL, producer, NULL);pthread_create(&tempCid, NULL, consumer, NULL);pthread_create(&tempCid, NULL, consumer, NULL);pthread_join(tempPid, NULL);pthread_join(tempCid, NULL);sem_destroy(&globBlankNum);sem_destroy(&globProductNum);return 0;
}//of main
2.4 小结
本部分主要讲解了Linux系统中与线程相关的知识,包括线程相关操作及线程同步,其中线程操作包括创建线程、退出线程、终止线程、回收线程等;线程同步包括互斥锁、条件变量、信号量这线程同步的三种方式。线程是Linux编程基础中非常重要的一项内容。
2.5 编程题
【1】编写一个程序,开启三个线程,第一个线程向终端输出A,第二个线程向终端输出B,第三个线程向终端输出C,每个线程打印10遍,要求输出必须按照ABC的顺序显示,如:ABCABCABC…
【2】利用线程的信号量实现互斥功能,模拟打印机。