前面的文章介绍了线程的创建、终止、连接和分离。本篇介绍线程的同步。
多线程的难点是对共享资源的访问,如何保证多个线程能够“同时”访问同一个共享资源而又不引起冲突,就是线程同步的本质。
在线程中,访问某一共享资源的代码片段称为“临界区”,该片段应为 原子(atomic)操作,即同时访问同一共享资源的其他线程不应中断该片段的执行。那么如何做到这一点呢?这就是本篇所要介绍的机制:互斥量(Mutex)。
互斥量用来确保共享资源同时只被一个线程访问。互斥量有两种状态:已锁定(locked)和未锁定(unlocked)。在任何时候,最多只能有一个线程可以锁定该互斥量。一旦某个线程锁定互斥量,即成为该互斥量的所有者,只有所有者才能给互斥量解锁。
互斥量分为“静态分配互斥量”和“动态分配互斥量”两种类型。
静态分配的互斥量,在初始化时可将其定义为如下形式,也是我个人最常使用的一种方式:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
静态初始值PTHREAD_MUTEX_INITIALIZER只能对经由静态分配且携带默认属性的互斥量进行初始化,其他情况下,必须调用pthread_mutex_init()对互斥量进行动态初始化。
#include
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) 初始化由静态分配,且不使用默认属性的互斥量。
当不再需要由自动或动态分配的互斥量时,应使用pthread_mutex_destroy()将其销毁。对于由PTHREAD_MUTEX_INITIALIZER静态初始化的互斥量,则无需调用pthread_mutex_destroy()。
#include
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()对其重新初始化。
互斥量的类型属于互斥量的属性之一。SUSv3定义了3种互斥量类型:
下里演示了如何设置互斥量类型、互斥量的动态初始化等操作:
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);
}
互斥量在被初始化之后,处于未锁定状态。可分别通过pthread_mutex_lock()和pthread_mutex_unlock()将其加锁和解锁。
#include
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
int pthread_mutex_trylock(pthread_mutex_t *mutex);
#include
#include
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()在实际使用中频率较低,不多赘述。
如下示例中测试了两个加法线程,各执行40万次加法,在线程func1中统计了该线程的总耗时(单位为us)。
#include
#include
#include
#include
#include
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,耗时增加了一个数量级。但在通常情况下,线程会花费更多时间去处理非临界区,对临界区的访问只占少量时间,对互斥量的操作也相对较少,因此,使用互斥量对大部分应用程序的性能并无显著影响,并且,相对于给出错误的结果,加锁所付出的这点性能代价仍然是应用程序开发者可以接受的。