1 互斥量简介
互斥量是Linux线程间数据同步最主要和最常用的手段,能够确保同一时间只有一个线程访问数据。互斥量本质上就是一把锁,当线程需要访问数据时就启用互斥量(加锁),访问完成后再释放互斥量(解锁)。对数据加锁之后,其他线程不能访问该数据,当然了也不能对该数据再次加锁,这样就保证任何时刻只有一个线程访问共享数据。
如果线程尝试对一个已经加锁的数据再次加锁,那么将会被阻塞,直到该数据解锁为止。如果在解锁时有一个以上的线程被阻塞,那么所有该锁上的被阻塞线程都会变成可运行状态,第一个变为运行的线程就可以对互斥量加锁,其他的被阻塞线程将会继续被阻塞只能继续等待。这样就保证了多个被阻塞的线程每次只有一个线程继续运行。
只有将所有的线程都设计成按相同的规则加锁解锁才能保证加锁的可靠性。所有的线程在访问数据时都必须得到锁才能访问互斥量。如果允许其中一个线程在没有得到锁的情况下也可以访问共享数据,那么其他的线程在使用共享数据之前都申请锁,也还是会出现数据不一致的问题。
2 互斥量相关API函数简介
下面对POSIX标准下的互斥量的标准API进行简介。
2.1创建互斥量
互斥量是pthread_mutex_t类型的。互斥量既可以像静态变量那样分配,也可以在运行时动态创建(比如通过malloc()在一块内存中分配)。对于静态初始化把常量PTHREAD_MUTEX_INITIALIZER赋值给互斥量。
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
对于动态创建互斥量可以调用函数pthread_mutex_init()来完成。函数格式如下:
#include<pthread.h>
int pthread_mutex_init( pthread_mutex_t *restrict mutex,
const pthread _mutexattr_t *restrict attr);
参数说明:
mutex —— 创建的互斥量
attr —— 互斥量属性,NULL代表互斥量的各种属性取默认值。
返回值:初始化成功返回0;否则返回错误编号。
2.2 销毁互斥量
当不需要自动或者动态分配的互斥量时,必须使用pthread_mutex_destroy()函数将其销毁。函数格式如下:
#include<pthread.h>
int pthread_mutex_destroy( pthread_mutex_t * mutex );
参数说明:
mutex —— 需要销毁的的互斥量
2.3 加锁
采用函数pthread_mutex_lock()来对互斥量加锁。如果互斥量已经加锁,调用线程将阻塞直到互斥量解锁。函数格式如下:
#include<pthread.h>
int pthread_mutex_lock( pthread_mutex_t * mutex );
参数说明:
mutex —— 需要加锁的的互斥量
返回值:加锁成功返回0;否则返回错误编号。
如果线程不希望被阻塞,可以调用pthread_mutex_trylock()来尝试加锁。如果调用pthread_mutex_trylock()函数时,互斥量处于未锁住状态,那么直接对互斥量加锁,如果互斥量已经处于锁住状态,则pthread_mutex_trylock()就会失败,不能对互斥量进行加锁,返回EBUSY。函数具体格式如下:
#include<pthread.h>
int pthread_mutex_trylock( pthread_mutex_t * mutex );
参数说明:
mutex —— 需要尝试加锁的互斥量
返回值:加锁成功返回0;否则返回EBUSY。
当线程试图访问一个已经加锁的互斥量时,函数pthread_mutex_timedlock()允许设定线程阻塞时间。pthread_mutex_timedlock()函数与pthread_mutex_lock()函数基本上是等价的,但是在达到超时时间值时,pthread_mutex_timedlock()不会对互斥量进行加锁,而是返回错误码ETIMEOUT。函数原型如下:
#include<pthread.h>
int pthread_mutex_timedlock( pthread_mutex_t *restrict mutex,
const struct timespec *restrict tsptr );
参数说明:
mutex —— 需要加锁的互斥量
tsptr —— 超时时间,线程愿意等待的绝对时间(与相对时间对比而言,指定在时间X之前可以等待,而不是愿意阻塞Y秒),用timespec结构来表示的(用秒和纳秒来描述时间)。
返回值:加锁成功返回0;否则返回EBUSY。
2.4 解锁
如果线程对互斥量访问结束,需要调用函数pthread_mutex_unlock()对互斥量进行解锁。下列行为均属于错误应该避免:对未加锁的信号量解锁,解锁其他线程锁定的互斥量。函数格式如下:
#include<pthread.h>
int pthread_mutex_unlock( pthread_mutex_t * mutex );
参数说明:
mutex —— 需要解锁的互斥量
返回值:解锁成功返回0;否则返回错误编号。
3 避免死锁
互斥量使用不当就会形成死锁,造成程序运行故障。通常情况下,以下情形会造成死锁:
(1)线程试图对同一个互斥量加锁两次,此线程将会陷入死锁状态;
(2)两个线程都需要先获得对方已经加锁的互斥量才能继续往前运行,那么这两个线程就会互相阻塞,形成死锁。
在设计编写程序时,要避免死锁情况的产生。常用的方法就是定义互斥量的层级关系。当多个线程对一组互斥量操作时,总以相同顺序对该组互斥量进行锁定,也就是仔细控制互斥量加锁顺序可以避免死锁的产生。
有时候由于程序结构的限制,对互斥量进行排序很困难。这种情况下可以尝试先释放占有的锁,然后过一段时间再试。可以使用pthread_mutex_trylock函数。如果占有锁,使用pthread_mutex_trylock尝试加新锁成功,则程序继续进行,如果失败,可以先释放已经占有的锁,然后过一段时间再试。
4 例程
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
//定义互斥量
pthread_mutex_t mtx_counter;
//定义互斥量属性
pthread_mutexattr_t mtx_attr;
//定义全局变量
int counter;
void err_exit(const char *err_msg)
{
printf("\r\n error:%s! exit!\r\n",err_msg);
exit(1);
}
//创建线程
void *thread_fun(void *arg)
{
while(1)
{
//加锁
pthread_mutex_lock( &mtx_counter );
printf(" Child thread , enter critical, counter = %d \n", counter);
if(counter == 10)
{
counter = 0;
printf("\r\n Operate is done, counter = %d \r\n",counter);
}
//解锁
pthread_mutex_unlock( &mtx_counter );
sleep(1);
}
return NULL;
}
int main(void)
{
pthread_t tid;
//初始化互斥量
if(pthread_mutex_init(&mtx_counter, NULL) == -1)
{
err_exit("pthread_mutex_init()");
}
//创建一个线程
if(pthread_create(&tid, NULL, thread_fun, NULL) == -1)
{
err_exit("pthread_create()");
}
counter = 0;
while(1)
{
//加锁
pthread_mutex_lock( &mtx_counter );
if(counter < 10)
{
counter = counter + 1;
printf(" mainthread, counter = %d \n",counter);
}
//解锁
pthread_mutex_unlock( &mtx_counter );
sleep(1);
}
return 0;
}
程序有两个线程。主线程对全局变量counter加1,子线程检查counter是否加到10,如果加到10则把counter赋值为0。两个线程在访问全局变量counter时都加了锁对临界区进行了保护。程序运行结果如图所示。