线程同步之互斥量Mutex

前面的文章介绍了线程的创建、终止、连接和分离。本篇介绍线程的同步。

多线程的难点是对共享资源的访问,如何保证多个线程能够“同时”访问同一个共享资源而又不引起冲突,就是线程同步的本质。

在线程中,访问某一共享资源的代码片段称为“临界区”,该片段应为 原子(atomic)操作,即同时访问同一共享资源的其他线程不应中断该片段的执行。那么如何做到这一点呢?这就是本篇所要介绍的机制:互斥量(Mutex)。

互斥量用来确保共享资源同时只被一个线程访问。互斥量有两种状态:已锁定(locked)和未锁定(unlocked)。在任何时候,最多只能有一个线程可以锁定该互斥量。一旦某个线程锁定互斥量,即成为该互斥量的所有者,只有所有者才能给互斥量解锁。

互斥量分为“静态分配互斥量”和“动态分配互斥量”两种类型。

1. 静态分配的互斥量

静态分配的互斥量,在初始化时可将其定义为如下形式,也是我个人最常使用的一种方式:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

2. 动态分配的互斥量

静态初始值PTHREAD_MUTEX_INITIALIZER只能对经由静态分配且携带默认属性的互斥量进行初始化,其他情况下,必须调用pthread_mutex_init()对互斥量进行动态初始化。

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);  // Return 0 on success, or a positive error number on error

参数mutex是初始化操作的目标互斥量;参数attr是指向pthread_mutexattr_t类型对象的指针,该对象在函数调用之前已经过了初始化处理,用于定义互斥量的属性。若将attr参数置为NULL,则该互斥量的各种属性会取默认值。

那么,在哪些情况下,需要调用pthread_mutex_init()来初始化互斥量呢?
(1) 动态分配在堆中的互斥量。例如,动态创建针对某一结构的链表,表中每个结构都包含一个pthread_mutex_t类型的字段来存放互斥量,用以保护对该结构的访问。
(2) 互斥量是在栈中分配的自动变量。
(3) 初始化由静态分配,且不使用默认属性的互斥量。

3. 互斥量的销毁

当不再需要由自动或动态分配的互斥量时,应使用pthread_mutex_destroy()将其销毁。对于由PTHREAD_MUTEX_INITIALIZER静态初始化的互斥量,则无需调用pthread_mutex_destroy()。

#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex); // Return 0 on success, or a positive error number on error

销毁互斥量需要注意的几点:
(1) 互斥量处于未锁定状态,且后续也无任何线程企图锁定它时,才可以销毁;
(2) 若互斥量处于动态分配的一块内存区域内,应在释放此内存前将互斥量销毁;
(3) 对于自动分配的互斥量,应在宿主函数返回前将其销毁。

经由pthread_mutex_destroy()销毁的互斥量,可调用pthread_mutex_init()对其重新初始化。

4. 互斥量的类型

互斥量的类型属于互斥量的属性之一。SUSv3定义了3种互斥量类型:

  • PTHREAD_MUTEX_NORMAL: 无死锁检测功能。
  • PTHREAD_MUTEX_ERRORCHECK:对此类互斥量的所有操作都会执行互斥检查,耗时较长,可用于调试。
  • PTHREAD_MUTEX_RECURSIVE:递归互斥量,维护一个锁计数器,线程获取一次锁,计数器加1,释放一次锁,计数器减1。当锁计数器为0时,互斥量才可被其他线程使用。

下里演示了如何设置互斥量类型、互斥量的动态初始化等操作:

pthread_mutex_t mutex;
pthread_mutexattr_t mutex_attr;
int ret, type;

ret = pthread_mutexattr_init(&mutex_attr);
if(ret != 0)
{
	printf("Error pthread_mutexattr_init! \n");
	exit(EXIT_FAILURE);
}

ret = pthread_mutexattr_settype(&mutex_attr, PTHREAD_MUTEX_ERRORCHECK);
if(ret != 0)
{
	printf("Error pthread_mutexattr_settype! \n");
	exit(EXIT_FAILURE);
}

ret = pthread_mutex_init(&mutex, &mutex_attr);
if(ret != 0)
{
	printf("Error pthread_mutex_init! \n");
	exit(EXIT_FAILURE);
}

ret = pthread_mutexattr_destroy(&mutex_attr);
if(ret != 0)
{
	printf("Error pthread_mutexattr_destroy! \n");
	exit(EXIT_FAILURE);
}

5. pthread_mutex_lock()与pthread_mutex_unlock()

互斥量在被初始化之后,处于未锁定状态。可分别通过pthread_mutex_lock()和pthread_mutex_unlock()将其加锁和解锁。

#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);   //Return 0 on success, or a positive error number on error
int pthread_mutex_unlock(pthread_mutex_t *mutex);  //Return 0 on success, or a positive error number on error

在调用pthread_mutex_lock()时需要传入要锁定的互斥量。如果该互斥量当前处于未锁定状态,则将其锁定并立即返回;如果该互斥量当前被其他线程锁定,那么pthread_mutex_lock()将会一直被阻塞直到互斥量被其他线程解锁,此时当前调用会锁定互斥量并返回。

与pthread_mutex_lock()类似的函数还有pthread_mutex_trylock()和pthread_mutex_timedlock()。

#include <pthread.h>

int pthread_mutex_trylock(pthread_mutex_t *mutex);
#include <pthread.h>
#include <time.h>

int pthread_mutex_timedlock(pthread_mutex_t *mutex, const struct timespec *abs_timeout);

这两个函数是Pthreads API中基于pthread_mutex_lock()函数的两个变体,与pthread_mutex_lock()用法基本相同,不同之处在于:

pthread_mutex_trylock()不会阻塞当前线程,调用后直接返回一个值来描述互斥锁的状态:

0:成功获取到锁;
EBUSY:互斥量已经被锁定,返回EBUSY;
EUBVAL:互斥量未被初始化;
EAGAIN:互斥量的lock count已经超过递归锁的最大值,无法再获取该互斥量。

pthread_mutex_timedlock()指定了一个附加参数abs_timeout,用于设置等待获取互斥量时的休眠时间限制。如果abs_timeout指定的时间间隔超时,而调用线程又没有获得对互斥量的所有权,那么pthread_mutex_timedlock()返回ETIMEDOUT错误。

pthread_mutex_trylock()与pthread_mutex_timedlock()在实际使用中频率较低,不多赘述。

6. 互斥量的性能

如下示例中测试了两个加法线程,各执行40万次加法,在线程func1中统计了该线程的总耗时(单位为us)。

#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/time.h>

static int cnt = 0;
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
struct timeval t_start, t_end;
time_t time_intval;

void *func1(void *arg)
{
	gettimeofday(&t_start, NULL);
	
	for(int i = 0; i < 400000; i++)
	{
		pthread_mutex_lock(&mutex);
		cnt++;
		pthread_mutex_unlock(&mutex);
	}
	
	gettimeofday(&t_end, NULL);
	time_intval = (t_end.tv_sec * 1000000 + t_end.tv_usec) - (t_start.tv_sec * 1000000 + t_start.tv_usec);
	
	printf("Time without mutex: %ld \n", time_intval);
	
	return NULL;
}

void *func2(void *arg)
{
	for(int i = 0; i < 400000; i++)
	{
		pthread_mutex_lock(&mutex);
		cnt++;
		pthread_mutex_unlock(&mutex);
	}
	
	return NULL;
}


int main()
{
	pthread_t myThread_1, myThread_2;
	int ret;
	
	ret = pthread_create(&myThread_1, NULL, func1, NULL);
	if(ret != 0)
	{
		printf("Error create myThread_1! \n");
		exit(1);
	}
	
	ret = pthread_create(&myThread_2, NULL, func2, NULL);
	if(ret != 0)
	{
		printf("Error create myThread_2! \n");
		exit(1);
	}
	
	ret = pthread_join(myThread_1, NULL);
	if(ret != 0)
	{
		printf("Error join myThread_1! \n");
		exit(1);
	}
	ret = pthread_join(myThread_2, NULL);
	if(ret != 0)
	{
		printf("Error join myThread_2! \n");
		exit(1);
	}
	
	printf("The final number is: %d \n", cnt);
	
	return 0;
}

在使用pthread_mutex_lock()与pthread_mutex_unlock()时,耗时如下:

在以上程序示例中,去掉对pthread_mutex_lock()与pthread_mutex_unlock()的使用,执行结果如下:

不加锁的情况下,一个线程执行所需要的时间约为2.765ms,但计算出了错误的结果;加锁的执行时间约为24.006ms,耗时增加了一个数量级。但在通常情况下,线程会花费更多时间去处理非临界区,对临界区的访问只占少量时间,对互斥量的操作也相对较少,因此,使用互斥量对大部分应用程序的性能并无显著影响,并且,相对于给出错误的结果,加锁所付出的这点性能代价仍然是应用程序开发者可以接受的。
 

发布了57 篇原创文章 · 获赞 58 · 访问量 13万+

猜你喜欢

转载自blog.csdn.net/DeliaPu/article/details/95889474