Linux之线程同步与互斥,模拟抢票系统
线程的同步与互斥
相关背景概念
- 临界区 :每个线程内部访问自己资源的代码叫做临界区:
- 临界资源 :多个执行流共享的资源叫做临界资源
- 互斥 :在任何时刻,只能有且仅有一个执行流访问临界区的临界资源
- 原子性 :一个操作不会被任何调度机制打断,要么完成,要么没完成,只有两种状态,常见的++,–都不是原子操作,因为汇编代码不止一条
互斥量mutex
- 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
- 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
- 多个线程并发的操作共享变量,会带来一些问题。
我们模拟实现抢票系统说明:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
using namespace std;
int tickets = 100; //临界资源
void* route(void* arg)
{
char* id = (char*)arg;
while(1)
{
if(tickets>0)
{
usleep(10000);
fflush(stdout);
cout<< id <<" get a ticket,now, tickets is: "<< tickets <<endl;
tickets--;
}
else{
cout<<"tickets is full"<<endl;
break;
}
}
return (void*)0;
}
int main()
{
pthread_t t1, t2, t3, t4;
pthread_create(&t1, NULL, route, "thread 1");
pthread_create(&t2, NULL, route, "thread 2");
pthread_create(&t3, NULL, route, "thread 3");
pthread_create(&t4, NULL, route, "thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
return 0;
}
很显然,这个代码出问题了,当我们的票数到了 0 时就应该退出停止抢票,结果却不是这样。
为什么会这样呢?
- if 语句判断条件为真以后,代码可以并发的切换到其他线程
- usleep 这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段
- ticket-- 操作本身就不是一个原子操作
那么,我们要怎么样改正呢?做到下面三点
- 代码必须有互斥行为,当代码进入临界区获取资源时,不允许其他线程进入临界区
- 当有多个线程同时来执行临界区代码时,并且此时临界区没有线程正在执行,那么允许且仅允许一个线程进入临界区
- 如果该线程不在临界区执行,那么不能阻塞其他线程进入临界区
其实本质上就是加一把锁,Linux下叫做 互斥锁。
互斥量接口
初始化互斥量
有两种方式
- 方法1,静态分配:
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
销毁互斥量
注意事项
- 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
- 不要销毁一个已经加锁的互斥量
- 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
函数原型
int pthread_mutex_destroy(pthread_mutex_t *mutex);
互斥量的加锁和解锁:
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
成功返回 0,失败返回错误码。
使用pthread_lock注意:
- 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
- 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。
说到这里,我们将上面的抢票在稍加修改:
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <vector>
using namespace std;
volatile int tickets = 100; //临界资源,需要加锁,否则多个线程访问临界资源会发生问题
pthread_mutex_t lock; //互斥锁
void* BuyTickets(void* arg)
{
int* id=(int*)arg;
int number = *id;
while(1)
{
usleep(100);
pthread_mutex_lock(&lock);
if(tickets > 0)
{
usleep(10000);
cout<<" thread "<< number << " get a ticket: "<< tickets <<endl;
tickets--;
pthread_mutex_unlock(&lock);
}
else{
pthread_mutex_unlock(&lock);
cout<<"tickets is full..."<<endl;
return (void*)0;
}
}
return (void*)0;
}
struct attr{
int _num;
pthread_t tid;
};
int main()
{
int num = 5;
vector<attr> thread_list(num);
pthread_mutex_init(&lock,NULL);
for(int i=0;i<num;++i)
{
pthread_create(&(thread_list[i].tid),NULL,BuyTickets,(void*)&i);
thread_list[i]._num = i;
}
for(int i=0;i<num;++i)
{
pthread_join(thread_list[i].tid,NULL);
}
pthread_mutex_destroy(&lock);
return 0;
}
大功告成。
这里我说几个我在写代码中遇到的问题:
pthread_create(&(thread_list[i].tid),NULL,BuyTickets,(void*)&i);
-
在写这个线程创建函数的时候,最后一个参数一定要传地址,否则会有警告,因为这里我们的 i 是一个常数,但函数原型传的是 (void *)类型的
-
注意for(int i=0;i<num;++i) 这里的 i 是根据我们定义的抢票人数来创建线程的,如果写的不对会造成 core dump。我就在这里卡了很久