linux线程互斥量pthread_mutex_t使用简介

为什么使用线程锁

在多线程应用程序中,当多个线程共享相同的内存时,如同时访问一个变量时,需要确保每个线程看到一致的数据视图,即保证所有线程对数据的修改是一致的。

如下两种情况不存在不一致的问题:

  • 每个线程使用的变量都是其他线程不会读取和修改的
  • 变量是只读的

当一个线程在修改变量的值时,其他线程在读取这个变量时可能会得到一个不一致的值。

一个典型的例子是,在一个多线程程序中,两个及以上个线程对同一个变量i执行i++操作,结果得到的值并不如顺序执行所预期的那样。这就是线程间不同步的一个例子。

可以用程序修改变量值时所经历的三个步骤解释这个现象:

  • 从内存单元读入寄存器
  • 在寄存器中对变量操作(加/减1)
  • 把新值写回到内存单元

不能预期以上三步骤在一个总线周期内完成,所以也就不能指望多线程程序如预期那样运行。

开始使用线程锁

互斥量

多线程程序中可能会存在数据不一致的情况,那么如何保证数据一致呢?可以考虑同一时间只有一个线程访问数据。互斥量(mutex)就是一把锁。

多个线程只有一把锁一个钥匙,谁上的锁就只有谁能开锁。当一个线程要访问一个共享变量时,先用锁把变量锁住,然后再操作,操作完了之后再释放掉锁,完成。

当另一个线程也要访问这个变量时,发现这个变量被锁住了,无法访问,它就会一直等待,直到锁没了,它再给这个变量上个锁,然后使用,使用完了释放锁,以此进行。

这个即使有多个线程同时访问这个变量,也好象是对这个变量的操作是顺序进行的。

互斥变量使用特定的数据类型:pthread_mutex_t,使用互斥量前要先初始化,使用的函数如下:

#include 

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);

简单的使用可以使用默认的属性初始化互斥量,函数的后一个参数设置为NULL即可。

对互斥量加锁解锁的函数如下:

#include 

int pthread_mutex_tlock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthreadd_mutex_t *mutex);

函数pthread_mutex_trylock会尝试对互斥量加锁,如果该互斥量已经被锁住,函数调用失败,返回EBUSY,否则加锁成功返回0,线程不会被阻塞。

避免死锁

产生死锁的情况较多,如一个线程对变量a加锁后,试图对变量b加锁,另一个线程对变量b加了锁,试图对a加锁,这时两个线程都不释放锁,加不会加锁成功,造成两个线程处于死锁状态。

可以在设计中避免死锁的发生。如使用 pthread_mutex_timedlock函数,该函数允许线程阻塞特定时间,如果加锁失败就会返回ETIMEDOUT。函数原型如下:

#include 
#include 

int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timesec *restrict tsptr);

读写锁

读写锁与互斥量类似,但它允许更高的并行性。

互斥量只有两种状态:锁住和未锁住,且一次只有一个线程可以对它加锁。

读写锁可以有三种状态:读模式下加锁状态、写模式下加锁状态和不加锁状态。一次只有一个线程可以占有写模式的读写锁,但多个线程可以同时占有读模式的读写锁。

读写锁非常适合于对数据结构读的次数远远大于写的情况。

与互斥量相比,读写锁在使用之前必须初始化,在释放它们底层的内存之前必须销毁。使用读写锁相关的函数如下:

#include 

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

条件变量

条件变量与互斥量一直使用时,允许线程以无竞争的方式等待特定的条件发生。条件变量是线程可用的另一种同步机制。

条件本身是由互斥量保护的。线程在改变条件状态之前必须产生锁住互斥量,其他线程在获得互斥量之前不会到这种改变,因为互斥量必须在锁定以后才能计算条件。

在使用条件变量之前必须先对它初始化。释放条件变量底层的内存空间之前,对条件变量反初始化。使用的相关函数如下:

#include 

int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
int pthreead_cond_destroy(pthread_cond_t *cond);

int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

自旋锁

与互斥量类似,但它不是通过休眠使进程阻塞,而是在获取锁之前一直处于忙等阻塞状态。当锁被持有的时间较短,而且线程不希望在重新调度上花费太多成本的情况下使用自旋锁。

当自旋锁用在非抢占式内核中时是非常有用的,除了提供互斥机制以外,它们会阻塞中断,这样中断处理程序就不会让系统陷入死锁状态,因为它需要获取已被加锁的自旋锁。

在用户层,自旋锁并不非常有用。很多互斥量的实现非常高效,甚至与采用自旋锁是同行效率的。

使用自旋锁的相关函数如下。

#include 

int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_destroy(pthread_spinlock_t *lock);

int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);

总结

在多线程编程中,线程同步是必不可少的考虑环节。线程不同步导致的问题很难排查,甚至bug复现的都很困难。

所以在多线程编程中,应该在设计中考虑到数据同步问题,保证变量视图的一致性,避免出现不一致性情况。


参考资料:
《UNIX环境高级编程》

你可能感兴趣的:(linux线程互斥量pthread_mutex_t使用简介)