以上3点中,前两点不能改变,欲提高效率,传递数据,资源必须共享。只要共享资源,就一定会出现竞争。只要存在竞争关系,数据就很容易出现混乱。所以只能从第三点着手解决。使多个线程在访问共享资源的时候,出现互斥。
主要应用函数:
pthread_mutex_init
pthread_mutex_destroy
pthread_mutex_lock
pthread_mutex_trylock
pthread_mutex_unlock
//以上5个函数的返回值都是:成功返回0, 失败返回错误号。
//pthread_mutex_t 类型,其本质是一个结构体。为简化理解,应用时可忽略其实现细节,简单当成整数看待。
//pthread_mutex_t mutex; 变量mutex只有两种取值1、0。
pthread_mutex_init 初始化一个互斥锁(互斥量) ---> 初值可看作1
pthread_mutex_destroy 销毁一个互斥锁
pthread_mutex_lock 加锁。可理解为将mutex--(或-1)
pthread_mutex_unlock 解锁。可理解为将mutex ++(或+1)
pthread_mutex_trylock 尝试加锁
加锁与解锁
lock与unlock:
lock与trylock:
看如下程序:该程序是非常典型的,由于共享、竞争而没有加任何同步机制,导致产生于时间有关的错误,造成数据混乱:
#include
#include
#include
void *tfn(void *arg)
{
srand(time(NULL));
while (1)
{
printf("hello ");
sleep(rand() % 3); /*模拟长时间操作共享资源,导致cpu易主,产生与时间有关的错误*/
printf("world\n");
sleep(rand() % 3);
}
return NULL;
}
int main(void)
{
pthread_t tid;
srand(time(NULL));
pthread_create(&tid, NULL, tfn, NULL);
while (1)
{
printf("HELLO ");
sleep(rand() % 3);
printf("WORLD\n");
sleep(rand() % 3);
}
pthread_join(tid, NULL);
return 0;
}
修改该程序,使用mutex互斥锁进行同步
#include
#include
#include
#include
#include
pthread_mutex_t mutex; //定义锁
void *tfn(void *arg)
{
srand(time(NULL));
while (1)
{
pthread_mutex_lock(&mutex); // mutex--
printf("hello ");
sleep(rand() % 3); /* 模拟长时间操作共享资源,导致cpu易主,产生与时间有关的错误 */
printf("world\n");
pthread_mutex_unlock(&mutex); // mutex++
sleep(rand() % 3);
}
return NULL;
}
int main(void)
{
int flg = 5;
pthread_t tid;
srand(time(NULL));
pthread_mutex_init(&mutex, NULL); // mutex==1
pthread_create(&tid, NULL, tfn, NULL);
while (flg--)
{
pthread_mutex_lock(&mutex); // mutex--
printf("HELLO ");
sleep(rand() % 3);
printf("WORLD\n");
pthread_mutex_unlock(&mutex); // mutex++
sleep(rand() % 3);
}
pthread_cancel(tid);
pthread_join(tid, NULL);
pthread_mutex_destroy(&mutex);
return 0;
}
/* 线程之间共享资源stdout */
运行结果
将unlock挪至第二个sleep后,发现交替现象很难出现。
读写锁的行为
读写锁状态:
一把读写锁具备三种状态:
读写锁特性
主要应用函数
pthread_rwlock_init函数
pthread_rwlock_destroy函数
pthread_rwlock_rdlock函数
pthread_rwlock_wrlock函数
pthread_rwlock_tryrdlock函数
pthread_rwlock_trywrlock函数
pthread_rwlock_unlock函数
以上7 个函数的返回值都是:成功返回0, 失败直接返回错误号。
pthread_rwlock_t类型 用于定义一个读写锁变量。
pthread_rwlock_t rwlock;
pthread_rwlock_init 初始化一把读写锁
pthread_rwlock_destroy 销毁一把读写锁
pthread_rwlock_rdlock 以读方式请求读写锁。(常简称为:请求读锁)
pthread_rwlock_wrlock 以写方式请求读写锁。(常简称为:请求写锁)
pthread_rwlock_unlock 解锁
pthread_rwlock_tryrdlock 非阻塞以读方式请求读写锁(非阻塞请求读锁)
pthread_rwlock_trywrlock 非阻塞以写方式请求读写锁(非阻塞请求写锁)
/* 3个线程不定时 "写" 全局资源,5个线程不定时 "读" 同一全局资源 */
#include
#include
#include
int counter; // 全局资源
pthread_rwlock_t rwlock;
void *th_write(void *arg)
{
int t;
int i = (int)arg;
while (1)
{
t = counter;
usleep(1000);
pthread_rwlock_wrlock(&rwlock); // 请求写锁
printf("=======write %d: %lu: counter = %d ++counter = %d\n", i, pthread_self(), t, ++counter);
pthread_rwlock_unlock(&rwlock);
sleep(1);
}
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);
sleep(1);
}
return NULL;
}
int main(void)
{
int i;
pthread_t tid[8];
pthread_rwlock_init(&rwlock, NULL);
for (i = 0; i < 3; i++)
pthread_create(&tid[i], NULL, th_write, (void *)i);
for (i = 0; i < 5; i++)
pthread_create(&tid[i+3], NULL, th_read, (void *)i);
for (i = 0; i < 8; i++)
pthread_join(tid[i], NULL);
pthread_rwlock_destroy(&rwlock); // 释放读写琐
return 0;
}
运行结果
条件变量分为两部分: 条件和变量。
主要应用函数
pthread_cond_init函数
pthread_cond_destroy函数
pthread_cond_wait函数
pthread_cond_timedwait函数
pthread_cond_signal函数
pthread_cond_broadcast函数
以上6 个函数的返回值都是:成功返回0, 失败直接返回错误号。
pthread_cond_t类型 用于定义条件变量
pthread_cond_t cond;
pthread_cond_init 初始化一个条件变量
pthread_cond_destroy 销毁一个条件变量
pthread_cond_wait 阻塞等待一个条件变量
函数作用:
pthread_cond_timedwait 限时等待一个条件变量
struct timespec
{
time_t tv_sec; /* seconds */ 秒
long tv_nsec; /* nanosecondes*/ 纳秒
}
正确用法:
在讲解setitimer函数时我们还提到另外一种时间类型:
struct timeval
{
time_t tv_sec; /* seconds */ 秒
suseconds_t tv_usec; /* microseconds */ 微秒
};
pthread_cond_signal 唤醒至少一个阻塞在条件变量上的线程
pthread_cond_broadcast 唤醒全部阻塞在条件变量上的线程
生产者消费者条件变量模型
/*借助条件变量模拟 生产者-消费者 问题*/
#include
#include
#include
#include
/*链表作为公享数据,需被互斥量保护*/
struct msg
{
struct msg *next;
int num;
};
struct msg *head;
struct msg *mp;
/* 静态初始化 一个条件变量和一个互斥量*/
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void *consumer1(void *p)
{
for (;;)
{
pthread_mutex_lock(&lock);
while (head == NULL)
{ //头指针为空,说明没有节点
pthread_cond_wait(&has_product, &lock);
}
mp = head;
head = head->next; // 模拟消费掉一个产品
pthread_mutex_unlock(&lock);
printf("-Consume1 ---%d\n", mp->num);
free(mp);
mp = NULL;
sleep(rand() % 5);
}
}
void *consumer2(void *p)
{
for (;;)
{
pthread_mutex_lock(&lock);
while (head == NULL)
{ //头指针为空,说明没有节点
pthread_cond_wait(&has_product, &lock);
}
mp = head;
head = head->next; // 模拟消费掉一个产品
pthread_mutex_unlock(&lock);
printf("-Consume2 ---%d\n", mp->num);
free(mp);
mp = NULL;
sleep(rand() % 5);
}
}
void *producer(void *p)
{
for (;;)
{
mp = malloc(sizeof(struct msg));
mp->num = rand() % 1000 + 1; //模拟生产一个产品
printf("-Produce ---%d\n", mp->num);
pthread_mutex_lock(&lock);
mp->next = head; // 头插法
head = mp;
pthread_mutex_unlock(&lock);
pthread_cond_signal(&has_product); // 将等待在该条件变量上的一个线程唤醒
sleep(rand() % 5);
}
}
int main(int argc, char *argv[])
{
pthread_t pid, cid[2];
srand(time(NULL));
pthread_create(&pid, NULL, producer, NULL);
pthread_create(&cid[0], NULL, consumer1, NULL);
pthread_create(&cid[1], NULL, consumer2, NULL);
pthread_join(pid, NULL);
pthread_join(cid[0], NULL);
pthread_join(cid[1], NULL);
return 0;
}
运行结果
条件变量的优点
主要应用函数
sem_init函数
sem_destroy函数
sem_wait函数
sem_trywait函数
sem_timedwait函数
sem_post函数
以上6 个函数的返回值都是:成功返回0, 失败返回-1,同时设置errno。(注意,它们没有pthread前缀)
sem_t类型,本质仍是结构体。但应用期间可简单看作为整数,忽略实现细节(类似于使用文件描述符)。
sem_t sem; 规定信号量sem不能 < 0。头文件
信号量基本操作:
sem_wait: 1. 信号量大于0,则信号量-- (类比pthread_mutex_lock)
| 2. 信号量等于0,造成线程阻塞
对应
|
sem_post: 将信号量++,同时唤醒阻塞在信号量上的线程 (类比pthread_mutex_unlock)
但,由于sem_t的实现对用户隐藏,所以所谓的++、--操作只能通过函数来实现,而不能直接++、--符号。
信号量的初值,决定了占用信号量的线程的个数。
sem_init 初始化一个信号量
sem_destroy 销毁一个信号量
sem_wait 给信号量加锁 --
sem_post 给信号量解锁 ++
sem_trywait 尝试对信号量加锁 -- (与sem_wait的区别类比lock和trylock)
sem_timedwait 限时尝试对信号量加锁
/*信号量实现 生产者 消费者问题*/
#include
#include
#include
#include
#include
#define NUM 5
int queue[NUM]; // 全局数组实现环形队列
sem_t blank_number, product_number; // 空格子信号量, 产品信号量
void *producer(void *arg)
{
int i = 0;
while (1)
{
sem_wait(&blank_number); // 生产者将空格子数--,为0则阻塞等待
queue[i] = rand() % 1000 + 1; // 生产一个产品
printf("----Produce---%d\n", queue[i]);
sem_post(&product_number); // 将产品数++
i = (i+1) % NUM; // 借助下标实现环形
sleep(rand()%3);
}
}
void *consumer1(void *arg)
{
int i = 0;
while (1)
{
sem_wait(&product_number); // 消费者将产品数--,为0则阻塞等待
printf("-Consume1---%d\n", queue[i]);
queue[i] = 0; // 消费一个产品
sem_post(&blank_number); // 消费掉以后,将空格子数++
i = (i+1) % NUM;
sleep(rand()%3);
}
}
void *consumer2(void *arg)
{
int i = 0;
while (1)
{
sem_wait(&product_number); // 消费者将产品数--,为0则阻塞等待
printf("-Consume2---%d\n", queue[i]);
queue[i] = 0; // 消费一个产品
sem_post(&blank_number); // 消费掉以后,将空格子数++
i = (i + 1) % NUM;
sleep(rand() % 3);
}
}
int main(int argc, char *argv[])
{
pthread_t pid, cid;
sem_init(&blank_number, 0, NUM); // 初始化空格子信号量为5
sem_init(&product_number, 0, 0); // 产品数为0
pthread_create(&pid, NULL, producer, NULL);
pthread_create(&cid, NULL, consumer1, NULL);
pthread_create(&cid, NULL, consumer2, NULL);
pthread_join(pid, NULL);
pthread_join(cid, NULL);
sem_destroy(&blank_number);
sem_destroy(&product_number);
return 0;
}
条件变量与互斥锁、信号量的区别
多线程版:
选用互斥锁mutex,如创建5个, pthread_mutex_t m[5];
模型抽象:
A B C D E
5支筷子,在逻辑上形成环: 0 1 2 3 4 分别对应5个哲学家:
所以有:
if(i == 4)
left = i, right = 0;
else
left = i, right = i+1;
振荡:如果每个人都攥着自己左手的锁,尝试去拿右手锁,拿不到则将锁释放。过会儿五个人又同时再攥着左手锁尝试拿右手锁,依 然拿不到。如此往复形成另外一种极端死锁的现象——振荡。
避免振荡现象:只需5个人中,任意一个人,拿锁的方向与其他人相逆即可(如:E,原来:左:4,右:0 现在:左:0, 右:4)。
所以以上if else语句应改为:
if(i == 4)
left = 0, right = i;
else
left = i, right = i+1;
而后, 首先应让哲学家尝试加左手锁:
while {
pthread_mutex_lock(&m[left]); 如果加锁成功,函数返回再加右手锁,
如果失败,应立即释放左手锁,等待。
若,左右手都加锁成功 --> 吃 --> 吃完 --> 释放锁(应先释放右手、再释放左手,是加锁顺序的逆序)
}
主线程(main)中,初始化5把锁,销毁5把锁,创建5个线程(并将i传递给线程主函数),回收5个线程。
#include
#include
#include
#include
pthread_mutex_t m[5];
void *tfn(void *arg)
{
int i, l, r;
srand(time(NULL));
i = (int)arg;
if (i == 4)
l = 0, r = i;
else
l = i; r = i+1;
while (1)
{
pthread_mutex_lock(&m[l]);
if (pthread_mutex_trylock(&m[r]) == 0)
{
printf("\t%c is eating \n", 'A'+i);
pthread_mutex_unlock(&m[r]);
}
pthread_mutex_unlock(&m[l]);
sleep(rand() % 5);
}
return NULL;
}
int main(void)
{
int i;
pthread_t tid[5];
for (i = 0; i < 5; i++)
pthread_mutex_init(&m[i], NULL);
for (i = 0; i < 5; i++)
pthread_create(&tid[i], NULL, tfn, (void *)i);
for (i = 0; i < 5; i++)
pthread_join(tid[i], NULL);
for (i = 0; i < 5; i++)
pthread_mutex_destroy(&m[i]);
return 0;
}
避免死锁的方法:
多进程版
相较于多线程需注意问题:
实现:
main函数中:
循环 sem_init(&s[i], 0, 1); 将信号量初值设为1,信号量变为互斥锁。
循环 sem_destroy(&s[i]);
循环创建 5 个子进程。 if(i < 5) 中完成子进程的代码逻辑。
循环回收 5 个子进程。
子进程中:
if(i == 4)
left = 0, right == 4;
else
left = i, right = i+1;
while (1) {
使用 sem_wait(&s[left]) 锁左手,尝试锁右手,若成功 --> 吃; 若不成功 --> 将左手锁释放。
吃完后, 先释放右手锁,再释放左手锁。
}
【重点注意】:
直接将sem_t s[5]放在全局位置,试图用于子进程间共享是错误的!应将其定义放置与mmap共享映射区中。main中:
sem_t *s = mmap(NULL, sizeof(sem_t) * 5, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, -1, 0);
使用方式:将s当成数组首地址看待,与使用数组s[5]没有差异。
#include
#include
#include
#include
#include
#include
#include
int main(void)
{
int i;
pid_t pid;
sem_t *s;
s = mmap(NULL, sizeof(sem_t)*5, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, -1, 0);
if (s == MAP_FAILED)
{
perror("fail to mmap");
exit(1);
}
for (i = 0; i < 5; i++)
sem_init(&s[i], 0, 1); // 信号量初值制定为1,信号量,变成了互斥锁
for (i = 0; i < 5; i++)
if ((pid = fork()) == 0)
break;
if (i < 5)
{ // 子进程
int l, r;
srand(time(NULL));
if (i == 4)
l = 0, r = 4;
else
l = i, r = i+1;
while (1)
{
sem_wait(&s[l]);
if (sem_trywait(&s[r]) == 0)
{
printf(" %c is eating\n", 'A'+i);
sem_post(&s[r]);
}
sem_post(&s[l]);
sleep(rand() % 5);
}
exit(0);
}
for (i = 0; i < 5; i++)
wait(NULL);
for (i = 0; i < 5; i++)
sem_destroy(&s[i]);
munmap(s, sizeof(sem_t)*5);
return 0;
}