Linux下线程的同步与互斥
线程间通信
线程共享同一进程的地址空间
优点:
- 通过全局变量交换数据
缺点:
- 多个线程访问共享全局数据时需要同步或者互斥
同步
同步指的是多个任务按照约定的先后次序互相配合完成一件事情。
1968年,Edsgar Dijkstra基于信号量的概念提出了一种同步机制。由信号量来决定线程是继续运行还是阻塞等待。
信号量
- 信号量代表某一类资源,其值表示系统中该资源的数量。
- 信号量是一个受保护的变量,只能通过三种操作来访问。
> 初始化
> P操作(申请资源)
> V操作(释放资源)
P操作(P(S)):
if(信号量的值大于0){
申请资源的任务继续运行;
信号量的值减一;
}
else{
申请资源的任务阻塞;
}
V操作(V(S)):
信号量的值加一;
if(有任务在等待资源){
唤醒等待的任务,让其继续运行;
}
Posix信号量
-
Posix中定义了两种信号量:
>无名信号量(基于内存的信号量)
>有名信号量
下面将介绍无名信号量。
pthread库常用的函数
sem_init函数
#include <semaphore.h>
int sem_init(sem_t* sem,int pshared,unsigned int val);
- 函数调用成功时返回0,失败时返回EOF
- 第一个参数(sem):指向要初始化的信号量对象
- 第二个参数(pshared): 0——线程间 1——进程间
- 第三个参数(val):信号量初值
P操作和V操作的函数
#include <semaphore.h>
int sem_wait(sem_t* sem); //P操作
int sem_post(sem_t* sem); //V操作
- 函数调用成功时返回0,失败时返回EOF
- 参数(sem):指向要操作的信号量对象
线程同步的示例
示例(生产者/消费者问题)
两个线程同步读写缓冲区
这个问题需要两个信号量实现读写的同步,一个信号量来描述可读缓冲区的数量,一个来描述可写缓冲区的数量。
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <semaphore.h>
char buf[32];
sem_t r;
sem_t w;
void* function(void* arg);
int main(void)
{
pthread_t a_thread;
if(sem_init(&r,0,0)<0) //创建可读缓冲区信号量,初始为0
{
perror("sem_init");
exit(-1);
}
if(sem_init(&w,0,1)<0) //创建可写缓冲区信号量
{
perror("sem_init");
exit(-1);
}
if(pthread_create(&a_thread,NULL,function,NULL)!=0)
{
printf("fail to pthread_create");
exit(-1);
}
printf("input 'quit' to exit\n");
do
{
sem_wait(&w);
fgets(buf,32,stdin);
sem_post(&r);
}while(strncmp(buf,"quit",4)!=0);
return 0;
}
void* function(void* arg)
{
while(1)
{
sem_wait(&r);
printf("you enter %d characters\n",strlen(buf));
sem_post(&w);
}
}
结果为
在写这段代码时,首先要主要到,创建信号量应在创建线程之前完成。防止出现还未创建信号量主线程的CPU时间片用完导致执行了新的线程,发生访问冲突。
互斥
临界资源
- 一次只允许一个任务(进程、线程)访问的共享资源
临界区
- 访问临界区的代码
互斥机制
- mutex互斥锁
- 任务访问临界资源之前申请锁,访问完后释放锁
pthread提供的函数
互斥锁初始化函数——pthread_mutex_init
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t* mutex,const pthread_mutexattr_t* attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//POSIX定义了一个宏PTHREAD_MUTEX_INITIALIZER来静态初始化互斥锁
- 函数调用成功时返回0,失败时返回错误码
- 第一个参数(mutex):指向要初始化的互斥锁对象
- 第二个参数(attr):互斥锁属性,NULL表示默认属性
或者你可以使用宏来静态初始化锁变量。
互斥锁的属性在创建锁的时候指定,在LinuxThreads实现中仅有一个锁类型属性,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同。当前有四个值可供选择:
enum
{
PTHREAD_MUTEX_TIMED_NP,
PTHREAD_MUTEX_RECURSIVE_NP,
PTHREAD_MUTEX_ERRORCHECK_NP,
PTHREAD_MUTEX_ADAPTIVE_NP
};
- PTHREAD_MUTEX_TIMED_NP,这是默认值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。
- PTHREAD_MUTEX_RECURSIVE_NP,嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。
- PTHREAD_MUTEX_ERRORCHECK_NP,检错锁,如果同一个线程请求同一个锁,则返回EDEADLK(死锁),否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样保证当不允许多次加锁时不出现最简单情况下的死锁。
- PTHREAD_MUTEX_ADAPTIVE_NP,适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。
通常我们使用默认的普通锁
申请锁函数——pthread_mutex_lock
int pthread_mutex_lock(pthread_mutex_t *mutex);
- 函数调用成功时返回0,失败时返回错误码
- mutex参数:指向要初始化的互斥锁对象
- 如果无法获得锁,任务阻塞
释放锁函数——pthread_mutex_unlock
int
__pthread_mutex_unlock (pthread_mutex_t *mutex)
- 函数调用成功时返回0,失败时返回错误码
- mutex参数:指向要初始化的互斥锁对象
- 执行完临界区要及时释放锁
线程互斥示例
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <semaphore.h>
unsigned int count,value1,value2;
pthread_mutex_t lock;
void* function(void* arg);
int main(void)
{
pthread_t a_thread;
if(pthread_mutex_init(&lock,NULL)!=0)
{
printf("fail to pthread_mutex_init\n");
exit(-1);
}
if(pthread_create(&a_thread,NULL,function,NULL)!=0)
{
printf("fail to pthread_create\n");
exit(-1);
}
while(1)
{
count++;
#ifdef _LOCK_
pthread_mutex_lock(&lock);
#endif
value1 = count;
value2 = count;
#ifdef _LOCK_
pthread_mutex_unlock(&lock);
#endif
}
return 0;
}
void* function(void* arg)
{
while(1)
{
#ifdef _LOCK_
pthread_mutex_lock(&lock);
#endif
if(value1 != value2)
{
printf("value1 = %u, value2 = %u\n",value1,value2);
usleep(100000);
}
#ifdef _LOCK_
pthread_mutex_unlock(&lock);
#endif
}
return NULL;
}
首先我们看不加锁的执行情况,我们可以发现会出现value1与value2不同情况。
枷锁之后的结果
我们可以分析出如果不加锁,那么当一个进程在执行value1 = count;
就可能发生时间片到执子线程,而此时可能value2还未被赋值,所以value1!=value2
。
这时我们使用互斥锁便能很好的解决同时访问临界区的问题。
如果你看到这觉得写得不错,那么可以帮我点一个赞再走吧^.^