目录
尝试加锁操作 pthread_mutex_trylock函数
销毁读写锁 pthread_rwlock_destroy函数
尝试请求读锁 pthread_rwlock_rdlock函数
尝试请求写锁 pthread_rwlock_wrlock函数
唤醒阻塞在条件变量的线程 pthread_cond_signal函数
广播唤醒阻塞在条件变量的线程 pthread_cond_broadcast函数
线程同步
线程同步,指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其它线程为保证数据一致性,不能调用该功能。
作用:协同步调,对公共资源(临界资源)【按序】访问。防止数据混乱,产生与时间有关的错误
数据混乱的原因
<1> 资源共享(独享资源则不会)
<2> 调度随机(意味着数据访问会出现竞争)
<3> 线程间缺乏必要同步机制
互斥锁 mutex
Linux 中提供一把互斥锁 mutex(也称之为互斥量)。
每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。
同一时刻,只能有一个线程持有该锁。
锁本质为建议锁!对公共数据进行保护。所有线程【应该】在访问公共数据前先拿锁再访问。但,锁本身不具备强制性。
创建互斥锁 pthread_mutex_init函数
初始化一个互斥锁(互斥量) ---> 初值可看作 1
#include <pthread.h>
pthread_mutex_t mutex;
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
参1:传出参数,调用时应传&mutex ---> pthread_mutex_t mutex
参 2:互斥量属性。是一个传入参数,通常传 NULL,选用默认属性(线程间共享)
返回值:成功:0
失败:错误号
restrict 关键字:只用于限制指针,告诉编译器,所有修改该指针指向内存中内容的操作,
只能通过本指针完成。不能通过除本指针以外的其他变量或指针修改
静态初始化:如果互斥锁 mutex 是静态分配的(定义在全局,或加了 static 关键字修饰),
可以直接使用宏进行初始化如下:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
销毁互斥锁 pthread_mutex_destroy函数
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
返回值:成功:0
失败:错误号
加锁操作 pthread_mutex_lock函数
加锁。 可理解为将 mutex--(或 -1),操作后 mutex 的值为 0
lock 尝试加锁,如果加锁不成功,线程阻塞,阻塞到持有该互斥量的其他线程解锁为止。
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
返回值:成功:0
失败:错误号
加锁操作 pthread_mutex_unlock函数
解锁。 可理解为将 mutex ++(或 +1),操作后 mutex 的值为 1
unlock 主动解锁函数, 同时将阻塞在该锁上的所有线程全部唤醒,至于哪个线程先被唤醒,取决于优先级、调度。默认:先阻塞、先唤醒。
#include <pthread.h>
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功:0
失败:错误号
尝试加锁操作 pthread_mutex_trylock函数
trylock 加锁失败直接返回错误号(如: EBUSY),不阻塞。
#include <pthread.h>
int pthread_mutex_trylock(pthread_mutex_t *mutex);
返回值: 成功:0
失败:错误号
互斥锁使用技巧
注意事项:
尽量保证锁的粒度, 越小越好。(访问共享数据前,加锁。访问结束【立即】解锁。)
互斥锁,本质是结构体。 方便理解可以看成整数。 初值为 1。(pthread_mutex_init() 函数调用成功。)
练习:利用互斥锁访问临界资源
使用mutex(互斥量、互斥锁)一般步骤:
1. pthread_mutex_t lock; 创建锁
2 pthread_mutex_init; 初始化 1
3. pthread_mutex_lock;加锁 1-- --> 0
4. 访问共享数据(stdout)
5. pthrad_mutext_unlock();解锁 0++ --> 1
6. pthead_mutex_destroy;销毁锁
pthread_mutex_t mutex; // 定义一把互斥锁
void *tfn(void *arg) {
srand(time(NULL));
while (1) {
pthread_mutex_lock(&mutex); // 加锁
printf("hello ");
sleep(rand() % 3); // 模拟长时间操作共享资源,导致cpu易主,产生与时间有关的错误
printf("world\n");
pthread_mutex_unlock(&mutex); // 解锁
sleep(rand() % 3);
}
return NULL;
}
int main(void) {
pthread_t tid;
srand(time(NULL));
int ret = pthread_mutex_init(&mutex, NULL); // 初始化互斥锁
if(ret != 0){
fprintf(stderr, "mutex init error:%s\n", strerror(ret));
exit(1);
}
pthread_create(&tid, NULL, tfn, NULL);
while (1) {
pthread_mutex_lock(&mutex); // 加锁
printf("HELLO ");
sleep(rand() % 3);
printf("WORLD\n");
pthread_mutex_unlock(&mutex); // 解锁
sleep(rand() % 3);
}
pthread_join(tid, NULL);
pthread_mutex_destory(&mutex); // 销毁互斥锁
return 0;
}
多线程经典问题:死锁
死锁:在并发环境下,各进程因竞争资源而造成的一种互相等待对方手里的资源,导致各进程都阻塞,都无法向前推进的现象,就是"死锁"。
发生死锁后若无外力干涉,这些进程都将无法向前推进。
容易发生死锁的情况
1. 线程试图对同一个互斥量 A 加锁两次。
2. 线程 1 拥有 A 锁,请求获得 B 锁;线程 2 拥有 B 锁,请求获得 A 锁
避免方法
<1> 保证资源的获取顺序,要求每个线程获取资源的顺序一致
<2> 当得不到所有所需资源时,放弃已经获得的资源,等待。
读写锁
与互斥量类似,但读写锁允许更高的并行性。其特性为: 写独占,读共享。但相较于互斥锁而言,当读线程多的时候,提高访问效率
特别强调: 读写锁只有一把, 但其具备两种状态:
1. 线程以读模式请求加锁状态 (读锁)
2. 线程以写模式请求加锁状态 (写锁)
读锁、写锁并行阻塞,写锁优先级高
创建读写锁 pthread_rwlock_init函数
pthread_rwlock_t rwlock
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
参 2: attr 表读写锁属性,通常使用默认属性, 传 NULL 即可。
返回值:成功返回0,失败返回错误号
销毁读写锁 pthread_rwlock_destroy函数
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
返回值:成功返回0,失败返回错误号
请求读锁 pthread_rwlock_rdlock函数
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
返回值:成功返回0,失败返回错误号
请求写锁 pthread_rwlock_wrlock函数
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
返回值:成功返回0,失败返回错误号
尝试请求读锁 pthread_rwlock_rdlock函数
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
返回值:成功返回0,失败返回错误号
尝试请求写锁 pthread_rwlock_wrlock函数
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
返回值:成功返回0,失败返回错误号
解锁 pthread_rwlock_unlock 函数
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
返回值:成功返回0,失败返回错误号
练习:利用读写锁对全局变量进行互斥访问
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
int counter;
pthread_rwlock_t rwlock;
void *th_write(void *arg){
int i = (int) arg;
int t;
while(1){
pthread_rwlock_wrlock(&rwlock);
t = counter;
usleep(1000);
printf("=========write %d: %lu: counter=%d ++counter=%d\n", i, pthread_self(), t,++counter);
pthread_rwlock_unlock(&rwlock);
usleep(1000);
}
return NULL;
}
void *th_read(void *arg){
int i = (int)arg;
while(1){
pthread_rwlock_rdlock(&rwlock);
printf("--------------read %d: %lu: %d\n", i, pthread_self(), counter);
pthread_rwlock_unlock(&rwlock);
usleep(2000);
}
return NULL;
}
int main(int argc,char*argv[]){
pthread_t tid[8];
int i, ret;
pthread_rwlock_init(&rwlock, NULL);
for(i = 0;i<3;i++){
ret = pthread_create(&tid[i],NULL,th_write ,(void *)i);
if(ret != 0){
fprintf(stderr, "pthread_create error:%s\n", strerror(ret));
exit(1);
}
}
for(i = 0; i < 5; i++){
ret = pthread_create(&tid[i+3], NULL, th_read, (void *)i);
if(ret != 0){
fprintf(stderr, "pthread_create error:%s\n", strerror(ret));
exit(1);
}
}
for(i = 0; i < 8; i++){
pthread_join(tid[i],NULL);
}
pthread_rwlock_destroy(&rwlock);
return 0;
}
条件变量
条件变量本身不是锁!但它也可以造成线程阻塞。通常与互斥锁配合使用。
创建条件变量 pthread_cond_init函数
#include <pthread.h>
pthread_cond_t cond; //用于定义条件变量
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
参 2: attr 表条件变量属性,通常为默认值,传 NULL 即可
返回值:成功:0
失败: 错误号
可以使用静态初始化的方法, 初始化条件变量:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
销毁条件变量 pthread_cond_destroy函数
int pthread_cond_destroy(pthread_cond_t *cond);
返回值:成功:0
失败: 错误号
阻塞等待 pthread_cond_wait函数
1) 阻塞等待条件变量满足
2) 解锁已经加锁成功的信号量 (相当于 pthread_mutex_unlock(&mutex)),1、2两步为一个原子操作
3) 当条件满足,函数返回时,解除阻塞并重新申请获取互斥锁。重新加锁信号量 (相当于, pthread_mutex_lock(&mutex);)
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
返回值:成功:0
失败: 错误号
限时等待 pthread_cond_timedwait函数
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
参三: 参看man sem_timedwait 函数,查看 struct timespec 结构体。
struct timespec {
time_t tv_sec; /* seconds */ 秒
long tv_nsec; /* nanosecondes*/ 纳秒
}
形参abstime:绝对时间。相对于 1970 年 1 月 1 日 00:00:00 秒的时间
正确传参:
time_t cur = time(NULL); 获取当前时间。
struct timespec t; 定义 timespec 结构体变量 t
t.tv_sec = cur+1; 定时 1 秒
pthread_cond_timedwait (&cond, &mutex, &t); 传参
返回值:成功:0
失败: 错误号
唤醒阻塞在条件变量的线程 pthread_cond_signal函数
唤醒至少一个阻塞在条件变量上的线程
int pthread_cond_signal(pthread_cond_t *cond);
返回值:成功:0
失败:错误号
广播唤醒阻塞在条件变量的线程 pthread_cond_broadcast函数
唤醒全部阻塞在条件变量上的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
返回值:成功:0
失败:错误号
线程同步经典模型 - 生产者消费者模型
线程同步典型的案例即为生产者消费者模型,而借助条件变量来实现这一模型,是比较常见的一种方法。假定有两个线程,一个模拟生产者行为,一个模拟消费者行为。两个线程同时操作一个共享资源(一般称之为汇聚),生产向其中添加产品,消费者从中消费掉产品。
条件变量的优点
相较于 mutex 而言,条件变量可以减少竞争。
如直接使用 mutex,除了生产者、消费者之间要竞争互斥量以外,消费者之间也需要竞争互斥量,但如果汇聚(链表)中没有数据,消费者之间竞争互斥锁是无意义的。有了条件变量机制以后,只有生产者完成生产,才会引起消费者之间的竞争。提高了程序效率。
练习:生产者消费者条件变量模型
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <errno.h>
struct tmp{
struct tmp *next;
int val;
};
struct tmp *head;
pthread_cond_t tcond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t tmutex = PTHREAD_MUTEX_INITIALIZER;
void err_thread(const char* str, int ret){
if(ret != 0){
fprintf(stderr, "%s: %s\n", str, strerror(ret));
pthread_exit(NULL);
}
}
void *product(void *arg){
for(;;){
struct tmp *ptmp = malloc(sizeof(struct tmp));
ptmp->val = rand() %1000 + 1;
printf("------prosuct %d\n", ptmp->val);
pthread_mutex_lock(&tmutex);
ptmp->next = head;
head = ptmp;
pthread_mutex_unlock(&tmutex);
pthread_cond_signal(&tcond);
sleep(rand()%3);
}
return NULL;
}
void *consumer(void *arg){
for(;;){
struct tmp *ctmp;
pthread_mutex_lock(&tmutex);
while(head == NULL){
pthread_cond_wait(&tcond, &tmutex);
}
ctmp = head;
head = ctmp->next;
pthread_mutex_unlock(&tmutex);
printf("---------------consumer%lu : %d\n",pthread_self(), ctmp->val);
free(ctmp);
sleep(rand()%3);
}
return NULL;
}
int main(int argc, char* argv[]){
pthread_t pid, cid[3];
int ret, i;
srand(time(NULL));
ret = pthread_create(&pid, NULL, product, NULL);
err_thread("pthread_create error", ret);
for(i = 0;i < 3; i++){
ret = pthread_create(&cid[i], NULL, consumer, NULL);
err_thread("pthread_create error", ret);
}
ret = pthread_join(pid, NULL);
err_thread("pthread_join error", ret);
for(i = 0; i< 3;i++){
ret = pthread_join(cid[i],NULL);
err_thread("pthread_join error", ret);
}
return 0;
}
信号量
应用于线程、进程间同步。
相当于 初始化值为 N 的互斥量。 N值,表示可以同时访问共享数据区的线程数。
创建信号量 sem_init函数
#include <semaphore.h>
sem_t sem; 规定信号量 sem 不能 < 0
int sem_init(sem_t *sem, int pshared, unsigned int value);
参 1: sem 信号量
参 2: pshared 取 0 用于线程间;取非 0(一般为 1)用于进程间
参 3: value 指定信号量初值
返回值:成功返回 0,
失败返回-1,同时设置 errno
销毁信号量 sem_destroy函数
#include <semaphore.h>
int sem_destroy(sem_t *sem);
返回值:成功返回 0,
失败返回-1,同时设置 errno
加锁信号量 sem_wait函数
一次调用,做一次-- 操作, 当信号量的值为 0 时,再次 -- 就会阻塞。
int sem_wait(sem_t *sem);
返回值:成功返回 0,
失败返回-1,同时设置 errno
解锁信号量 sem_post函数
一次调用,做一次++ 操作. 当信号量的值为 N 时, 再次 ++ 就会阻塞。
int sem_post(sem_t *sem);
返回值:成功返回 0,
失败返回-1,同时设置 errno
尝试加锁信号量 sem_trywait函数
尝试对信号量加锁 -- (与 sem_wait 的区别类比 lock 和 trylock)
int sem_trywait(sem_t *sem);
返回值:成功返回 0,
失败返回-1,同时设置 errno
限时尝试加锁信号量 sem_timedwait 函数
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
参 2: abs_timeout 采用的是绝对时间。
返回值:成功返回 0,
失败返回-1,同时设置 errno
定时 1 秒:
time_t cur = time(NULL); 获取当前时间。
struct timespec t; 定义 timespec 结构体变量 t
t.tv_sec = cur+1; 定时 1 秒
t.tv_nsec = t.tv_sec +100;
sem_timedwait(&sem, &t); 传参
练习:信号量实现的生产者消费者
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <semaphore.h>
#include <pthread.h>
#define NUM 5
int queue[NUM];
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
sem_t blank_number, product_number;
void err_printf(const char* str,int ret){
if(ret != 0){
fprintf(stderr, "%s:%s\n", str, strerror(ret));
pthread_exit(NULL);
}
}
void *product(void *arg){
int i = 0;
while(1){
sem_wait(&blank_number);
queue[i] = rand()% 1000+1;
printf("======product=====%d\n", queue[i]);
sem_post(&product_number);
i =(i+1)%NUM;
sleep(rand()%1);
}
return NULL;
}
void *consumer(void *arg){
int i =0;
while(1){
sem_wait(&product_number);
printf("-----------consumer------%d\n", queue[i]);
queue[i] = 0;
sem_post(&blank_number);
i = (i+1)%NUM;
sleep(rand() % 1);
}
return NULL;
}
int main(int argc,char *argv[]){
pthread_t pid, cid;
int ret;
sem_init(&blank_number, 0 , NUM);
sem_init(&product_number, 0, 0);
ret = pthread_create(&pid, NULL, product, NULL);
err_printf("pthread_create error", ret);
ret = pthread_create(&cid, NULL, consumer, NULL);
err_printf("pthread_create error", ret);
pthread_join(pid, NULL);
pthread_join(cid, NULL);
sem_destroy(&blank_number);
sem_destroy(&product_number);
return 0;
}