线程的互斥与同步
//售票系统
#include <stdio.h>
#include <pthread.h>
int ticket = 10000;
void* buy_ticket(void* arg)//买票
{
const char* id = (const char*)arg;
while(1)
{
if(ticket > 0)
{
printf("%s get a ticket: %d\n", id, ticket);
ticket--;
}
else
{
break;
}
}
}
int main()
{
pthread_t tid1, tid2,tid3, tid4;
pthread_create(&tid1, NULL, buy_ticket, "thread_1");
pthread_create(&tid2, NULL, buy_ticket, "thread_2");
pthread_create(&tid3, NULL, buy_ticket, "thread_3");
pthread_create(&tid4, NULL, buy_ticket, "thread_4");
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_join(tid3, NULL);
pthread_join(tid4, NULL);
return 0;
}
如上所示为售票系统,有四个执行流都对全局变量ticket(票数)进行操作,它们对ticket的同时操作会导致ticket变量出现错误,比如下面:
objdump -d +可执行文件名//反汇编
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//一般用于定义为全局变量的锁
(2)动态分配
int pthread_mutex_init(pthread_mutex_t* restrict mutex, const pthread_mutexattr_t* restrict attr);
//参数mutex:要初始化的互斥量
//参数attr:默认为NULL
int pthread_mutex_destroy(pthread_mutex_t* mutex);
3. 互斥量加锁和解锁
int pthread_mutex_lock(pthread_mutex_t* mutex);//加锁
int pthread_mutex_unlock(pthread_mutex_t* mutex);//解锁
//返回值:成功返回0,失败返回错误号
调用pthread_lock加锁时,可能会遇到以下情况:
//互斥锁:售票系统
#include <stdio.h>
#include <pthread.h>
int ticket = 10000;
pthread_mutex_t lock;//定义一个互斥锁
void* buy_ticket(void* arg)//买票
{
const char* id = (const char*)arg;
while(1)
{
pthread_mutex_lock(&lock);//对临界区加锁
if(ticket > 0)
{
printf("%s get a ticket: %d\n", id, ticket);
ticket--;
pthread_mutex_unlock(&lock);
}
else
{
pthread_mutex_unlock(&lock);
break;
}
//不能在这里解锁,因为break后就执行不到这里了
}
}
int main()
{
//pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_init(&lock, NULL);
pthread_t tid1, tid2,tid3, tid4;
pthread_create(&tid1, NULL, buy_ticket, "thread_1");
pthread_create(&tid2, NULL, buy_ticket, "thread_2");
pthread_create(&tid3, NULL, buy_ticket, "thread_3");
pthread_create(&tid4, NULL, buy_ticket, "thread_4");
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_join(tid3, NULL);
pthread_join(tid4, NULL);
pthread_mutex_destroy(&lock);//释放互斥锁
return 0;
}
此时,每个执行流对临界区的操作是互斥的,就不会产生以上出现的数据产生错误的问题。运行结果为:
二. 条件变量——实现同步
当一个线程互斥的访问某个变量时,它可能发现在其他线程改变状态之前,它什么都做不了。比如,一个线程访问队列时,发现队列为空,它只能等待,直到其他线程将第一个节点添加到队列中。这种情况就需要用到条件变量。下面介绍一些条件变量函数:
int pthread_cond_init(pthread_cond_t* restrict cond, const pthread_condattr_t* restrict attr);
//参数cond:要初始化的条件变量
//参数attr:默认为NULL
2. 销毁条件变量
int pthread_cond_destroy(pthread_cond_t* cond);
3. 等待条件满足
int pthread_cond_wait(pthread_cond_t* restrict cond, pthread_mutex_t* restrict mutex);
//参数cond:要在这个条件变量上等待
//参数mutex:互斥量
注意:
(2)如果发现等待条件满足,则使用wait使线程挂起等待;如果等待条件不满足,就对临界资源进行操作,释放锁。
(3)当对临界资源操作完成后,释放互斥量,退出临界区。
(1)当等待条件满足时,挂起调用它的线程 ;
(2)释放互斥量 ;
(3)当再一次被唤醒并且切换到该线程后,会重新自动获得互斥量,并在等待处继续往下执行。
int pthread_cond_broadcast(pthread_cond_t* cond);//唤醒一群
int pthread_cond_signal(pthread_cond_t* cond);//唤醒某一指定的
当条件满足之后,就要唤醒在条件变量下等待的线程。
因此,该函数在使用时:
(1)进入临界区对临界资源进行操作,所以要先申请互斥量
(2)使等待条件为假,如插入线程2向队列中插入节点
(3)调用signal。如果此时有线程在等待,则唤醒它。若没有等待的线程,则该函数什么也不做
(4)解锁互斥量,退出临界区
//条件变量:每隔一秒打一条消息
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex;//互斥量
pthread_cond_t cond;//条件变量
void* run1(void* arg)//实现打印功能
{
while(1)
{
printf("This is Young May\n");
pthread_cond_wait(&cond, &mutex);
printf("I am thread %d\n", (int)arg);
}
}
void* run2(void* arg)//实现sleep一秒的功能
{
while(1)
{
pthread_cond_signal(&cond);//唤醒指定的条件变量
printf("I am thread %d\n", (int)arg);
sleep(1);
}
}
int main()
{
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond, NULL);
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, run1, (void*)1);
pthread_create(&tid2, NULL, run2, (void*)2);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
在上面两个线程中,sleep线程先运行,所以,会先输出2,然后切换到1线程,输出语句this is YoungMay,接着wait进入等待,此时切换回2线程,sleep1s后,执行signal,线程1被唤醒。但是,signal之后的也会继续运行。所以1s后会输出2,执行sleep时,切换到1线程,接着wait继续运行,输出1,输出语句this is YoungMay。再进入wait,切换到2线程,1s后,signal使wait解锁,但会接着signal运行,输出2,进入sleep后,切换到1线程,输出1和语句this is YoungMay,之后就这样循环运行。
还有关于条件变量的相关实现,见另一篇博客——生产者与消费者模型。
三. POSIX信号量——实现同步
POSIX信号量与SystemV信号量作用相同,均是用于同步操作,本质上均是一个临界资源个数的计数器,为达到无冲突的访问共享资源的目的。但是POSIX信号量可以用于线程同步。下面先介绍POSIX信号量的相关接口函数:
1. 初始化信号量
(1)函数原型
(2)函数功能:初始化信号量
(3)函数参数:
sem:要进行操作的信号量
pshared:0表示线程间共享,非0表示进程间共享
value:信号量的初始值
(4)返回值:成功返回0,失败返回-1
2. 销毁信号量
(1)函数原型
(2)函数功能:销毁信号量
(3)函数参数sem:要销毁的信号量
(4)函数返回值:成功返回0,失败返回-1
3. 等待信号量/申请资源
(1)函数原型:
(2)函数功能:等待信号量,会将信号量值减1
(3)函数参数:我们常用第一个函数,参数即为要等待的信号量
(4)函数返回值:成功返回0,失败返回-1
4. 发布信号量/归还资源
(1)函数原型
(2)函数功能:发布信号量,表示资源已使用完毕,可以归还资源了,会将信号量值加1
(3)函数参数sem:要归还资源的信号量
(4)函数返回值:成功返回0,失败返回-1
5. 编写代码使用测试
见下一篇博客——生产者与消费者模型