在线程通信笔记中所展示的代码中,线程在访问共享的全局变量时,没有按照一定的规则顺序进行访问造成了不可预计的后果。针对代码的运行结果分析,其原因就是线程在访问共享资源的过程中被其他线程打断,其他线程也开始访问共享资源导致了数据的不确定性。对于上述情况而言,最好的解决办法是当一个线程在进行共享资源的访问时,其他线程不能访问,保证对于共享资源操作的完整性。
本笔记介绍一种互斥机制,用以保护对共享资源的操作,即保护线程对共享资源的操作代码可以完整执行,而不会在访问的中途被其他线程介入对共享资源访问,造成错误。在这里,通常把对共享资源操作的代码段,称之为临界区,其共享资源也可以称为临界资源。于是这种机制——互斥锁的工作原理就是对临界区进行加锁,保证处于临界区的线程不被其他线程打断,确保其临界区运行完整。
互斥锁
是一种互斥机制。互斥锁作为一种资源,在使用之前需要先初始化一个互斥锁。每一个线程在访问共享资源时,都需要进行加锁操作,如果线程加锁成功,则可以访问共享资源,期间不会被打断,在访问结束之后解锁。如果线程在进行上锁时,其锁资源被其他线程持有,那么该线程则会执行阻塞等待,等待锁资源被解除之后,才可以进行加锁。对于多线程而言,在同等条件下,对互斥锁的持有是不确定的,先持有锁的线程先访问,其他线程只能阻塞等待。也就是说,互斥锁并不能保证线程的执行先后,但却可以保证对共享资源操作的完整性。如下图所示:
互斥锁的使用包括初始化互斥锁、互斥锁上锁、互斥锁解锁、互斥锁释放。
#include <pthread.h> int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); int pthread_mutex_destroy(pthread_mutex_t *mutex);点击复制复制失败已复制
pthread_mutex_init()
函数用来实现互斥锁的初始化。参数 mutex
用来指定互斥锁的标识符,类似于 ID
;参数 attr
为互斥锁的属性,一般设置为 NULL
,即默认属性。与之相反 pthread_mutex_destroy()
函数为释放互斥锁,参数 mutex
用来指定互斥锁的标识符。只有当互斥锁未处于锁定状态,且后续也无任何线程企图锁定它时,将其摧毁才是安全的。
#include <pthread.h> int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex);点击复制复制失败已复制
初始化之后,互斥锁处于未锁定状态, pthread_mutex_lock()
函数为上锁处理,如果该锁资源处于持有状态,那么函数将直接导致线程阻塞。直到其他线程使用 pthread_mutex_unlock()
函数进行解锁,参数 mutex
为互斥锁的标识符。
注意
不可对处于未锁定状态的互斥量进行解锁,或者解锁由其他线程锁定的互斥锁。
之前的线程通信笔记中所展示的代码就可以使用互斥锁来实现互斥操作,避免竞态,只需要使用互斥锁将线程的临界区锁住即可,具体代码如下所示:
#include <pthread.h> #include <stdio.h> #define errlog(errmsg) \ do { \ perror(errmsg); \ printf("--%s--%s--%d--\n", __FILE__, __FUNCTION__, __LINE__); \ return -1; \ } while (0) int value1 = 0; int value2 = 0; int count = 0; pthread_mutex_t lock; void *thread1_handler(void *arg) { while (1) { pthread_mutex_lock(&lock); value1 = count; value2 = count; count++; pthread_mutex_unlock(&lock); } pthread_exit("thread1...exit"); } void *thread2_handler(void *arg) { while (1) { pthread_mutex_lock(&lock); if (value1 != value2) { sleep(1); printf("value1 = %d value2 = %d\n", value1, value2); } pthread_mutex_unlock(&lock); } pthread_exit("thread2...exit"); } int main(int argc, const char *argv[]) { pthread_t thread1, thread2; void *retval; if(pthread_mutex_init(&lock,NULL)!=0){ errlog("pthread_mutex_init error"); } if (pthread_create(&thread1, NULL, thread1_handler, NULL) != 0) { errlog("pthread_create1 error"); } if (pthread_create(&thread2, NULL, thread2_handler, NULL) != 0) { errlog("pthread_create2 error"); } pthread_join(thread1, &retval); printf("%s\n", (char *)retval); pthread_join(thread2, &retval); printf("%s\n", (char *)retval); pthread_mutex_destroy(&lock); return 0; }点击复制复制失败已复制
编译并运行,可以看出,程序并不会执行任何输出,因为不论哪一个线程得到互斥锁,进入自己的临界区,另外一个线程只能阻塞。因此判定条件永远不会成立。这是互斥锁介入之后代码的正确执行结果。
同时,在上述代码中还需要注意的是,如果多线程同时对一个共享资源进行访问,其中一个线程采用了互斥锁的机制,其他线程则必须也遵循该规则,即使用互斥锁机制;如果有任何一个线程在访问共享资源的时候违背了规则,那么结果将会是不可预计的。
Pthread API
还提供了 pthread_mutex_lock()
函数的两个版本:
pthread_mutex_trylock()
和 pthread_mutex_timedlock()
。
#include <pthread.h> #include <time.h> int pthread_mutex_trylock(pthread_mutex_t *mutex); int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abs_timeout);点击复制复制失败已复制
如果互斥锁被持有(占用),对其执行函数 pthread_mutex_trylock()
会失败并返回错误码EBUSY
,而不会执行睡眠等待,除此之外与 pthread_mutex_lock()
函数一致。 pthread_mutex_timedlock()
函数可以指定一个附件的参数 abs_timeout
,用以设置线程等待的时间期限。如果该线程等待的期限时间已到,然而互斥锁仍然处于被持有状态,那么 pthread_mutex_timedlock()
函数返回错误码 ETIMEOUT
。除此之外,其功能与 pthread_mutex_lock()
函数一致。
pthread_mutex_trylock()
函数和 pthread_mutex_timedlock()
函数使用的频率相对于 pthread_mutex_lock()
函数要少。在大多数程序中,线程对互斥锁的持有时间应尽可能短,以避免其他线程等待时间太久,保证其他线程可以尽快获得互斥锁。如果某一线程使用 pthread_mutex_trylock()
函数周期性的轮询是否可以占有互斥锁,则增加了系统消耗。