深入浅出Mutex(二)

来源:互联网 发布:win访问mac共享文件夹 编辑:程序博客网 时间:2024/05/21 09:18

NOTE:上篇请戳这里。 ^_^


一:Sequential Consistency


  首先应该稍微了解下这个概念:大神已经铺好路了,还是要看看SC作者lamport给的定义:

… the result of any execution is the same as if the operations of all the processors were executed in some sequential order, and the operations of each individual processor appear in this sequence in the order specified by its program.”

  any exceution代表了任意的可能的执行顺序,all the processors were executed in some sequential order意在指,从整体程序来讲,所有processor分担的程序执行流都以某种顺序结构执行。而单一的processor的上的执行流应该是由其program所决定的顺序结构,如同大神提到的两点

  • 每个线程的执行流都是顺序执行的。
  • 整个程序的执行顺序对于每个线程都是相同的。


  如果都能按照理想的状态,那么多线程编程就不会如此困难,实际上,如果我们的很多操作都是原子操作的话,按照SC的模型的论述,多线程应该配合的很不错。但是一条命令需要多个时钟周期是再正常不过的

In modern computer systems, memory accesses take multiple bus cycles, and multiprocessors generally interleave bus cycles among multiple processors, so we aren’t guaranteed that our data is sequentially consistent.


二:Mutex


#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);Both return: 0 if OK, error number on failureint pthread_mutex_lock(pthread_mutex_t *mutex);int pthread_mutex_trylock(pthread_mutex_t *mutex);int pthread_mutex_unlock(pthread_mutex_t *mutex);All return: 0 if OK, error number on failure

  言简意赅的总结下Mutex的使用就是:

  1. 初始化Mutex,PTHREAD_MUTEX_INITIALIZER以宏定义的方式初始化一个static mutex,或者以pthread_mutex_init函数调用的方式初始化一个Mutex,其中mutex参数可能是由malloc动态分配的,那么在在free相对应的区域前,需要先调用pthread_mutex_destroy
  2. pthread_mutex_trylock如同其名字一样,当Lock没有成功时,不会使当前线程block,返回EBUSY信息。
  3. 如果使用多个Mutex一定遵循同样的顺序,只要是同时使用多个锁的场合一定注意上锁的顺序,解锁的顺序可能对性能会有影响,但是只是可能。


  多线程的编程的难点,就是没有统一的标准。复杂的或者简单的锁的层次机制可能并不能带来原本使用多线程应有的性能提升,只能针对问题进行不断的思考尝试,优化。具体请看实验一部分,该部分进行详细分析。

#include <pthread.h>#include <time.h>int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict tsptr);Returns: 0 if OK, error number on failure

  使用pthread_mutex_timedlock去获得Mutex,当线程被阻塞的时间到达tsptr所指定的时间(绝对时间),该函数返回ETIMEDOUT

2.1 :Mutex Attributes

#include <pthread.h>int pthread_mutexattr_init(pthread_mutexattr_t *attr);int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);Both return: 0 if OK, error number on failure

  pthread_mutexattr_init 将会初始化attr所指的pthread_mutexattr_t结构体,其中有三个比较重要的属性:process-shared,robust,type.

2.1.1 process-shared
int pthread_mutexattr_getpshared(const pthread_mutexattr_t * restrict attr, int *restrict pshared);int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);Both return: 0 if OK, error number on failure

  process-shared可以被设置为PTHREAD_PROCESS_PRIVATE,此时多个process将不能访问同一个mutex,而当其被设置为PTHREAD_PROCESS_SHARED,此时该mutex可以被多个process访问,意味着可以用该锁来同步多个process。回想之前做signal的实验的时候,多个进程同时打开一个文件的场景 signal实验篇。pthread_mutexattr_getpshared获取当前process-shared的值并保存在pshared所指的变量中,pthread_mutexattr_setpshared使用参数pshared来设置process-shared的值。这里都是很容易理解的。

2.1.2 robust

  process-shared被设置为PTHREAD_PROCESS_SHARED时,robust用来处理一种场景:当一个进程持有该锁的情况下意外终止,对于该锁的状态该如何恢复。

int pthread_mutexattr_getrobust(const pthread_mutexattr_t * restrict attr, int *restrict robust);int pthread_mutexattr_setrobust(pthread_mutexattr_t *attr, int robust);Both return: 0 if OK, error number on failure

  这两个函数的使用方式和上面process-shared的两个函数类似,不再赘述。robust具有两个可能的值:PTHREAD_MUTEX_STALLED代表了不作为,也是默认的属性。毕竟上述场景并非常见的状态;另一个值为PTHREAD_MUTEX_ROBUST,该值使调用pthread_mutex_lock获得mutex的线程立即获得锁,但是其函数返回值为EOWNERDEAD来表示上述场景的发生。

If the application state can’t be recovered, the mutex will be in a permanently unusable state after the thread unlocks the mutex.

  如何理解上面的话呢?我们假设如果我们得到了EOWNERDEAD,那么意味着我们必须进行一些检查,如果我们没有办法恢复上一个被意外终止的进程或者线程所对该Mutex保护内容的修改,那么意味着我们之后再使用这个锁是徒劳的!

#include <pthread.h>int pthread_mutex_consistent(pthread_mutex_t * mutex);Returns: 0 if OK, error number on failure

  以下引用来自MAN pthread_mutex_consistent:

The pthread_mutex_consistent() function is only responsible for notifying the implementation that the state protected by the mutex has been recovered and that normal operations with the mutex can be resumed. It is the responsibility of the application to recover the state so it can be reused. If the application is not able to perform the recovery, it can notify the implementation that the situation is unrecoverable by a call to pthread_mutex_unlock() without a prior call to pthread_mutex_consistent(), in which case subsequent threads that attempt to lock the mutex will fail to acquire the lock and be returned [ENOTRECOVERABLE].

  这样比喻下,如果robust mutex遇到我们上面提到的场景,那么此时该mutex所保护的内容处于一种inconsistent状态,此时由我们编程人员负责去恢复mutex保护的内容,而当我们做好这一切时,应该先调用pthread_mutex_consistent,再调用pthread_mutex_unlock,通知别的要获得该锁的线程该锁可以正常使用了;当我们人为无法恢复时,我们应直接调用pthread_mutex_unlock,此时试图获得该锁的线程将加锁失败,并返回ENOTRECOVERABLE

2.1.3 type
#include <pthread.h>int pthread_mutexattr_gettype(const pthread_mutexattr_t * restrict attr, int *restrict type);int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);Both return: 0 if OK, error number on failure
Mutex type Relock without unlock? Unlock when not owned? Unlock when unlocked? PTHREAD_MUTEX_NORMAL deadlock undefined undefined PTHREAD_MUTEX_ERRORCHECK returns error returns error returns error PTHREAD_MUTEX_RECURSIVE allowed returns error returns error
#include <pthread.h>int pthread_mutexattr_gettype(const pthread_mutexattr_t * restrict attr, int *restrict type);int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);Both return: 0 if OK, error number on failure

  这两个函数不再赘述,上面的表格我们重点关注PTHREAD_MUTEX_RECURSIVE,当tpye为这个值时,允许relock的发生!也就是一个锁的重复加锁,那么相应解锁的时候要和加锁次数相等。但是要注意重复加锁是发生在一个线程内,如果多个线程都可以重复加同一个锁,那么这个锁也没有意义了!
  对于使用Condition Variables的场合,PTHREAD_MUTEX_RECURSIVE性质的Mutex是没有意义的!可以思考为什么?
  那么到底应该在什么时候使用PTHREAD_MUTEX_RECURSIVE性质的Mutex?当多个function操作同一个数据,且该多个functions之间会相互调用,那么此时多个线程中调用这些functions就应该考虑使用RECURSIVE性质的Mutex。本部分的实验作为实验二,自己构造一个使用RECURSIVE的Mutex的场合。


三 :Condition Variables


#include <pthread.h>int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);int pthread_cond_destroy(pthread_cond_t *cond);Both return: 0 if OK, error number on failureint pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict tsptr);Both return: 0 if OK, error number on failureint pthread_cond_signal(pthread_cond_t *cond);int pthread_cond_broadcast(pthread_cond_t *cond);Both return: 0 if OK, error number on failure

  假设一种场景,我们使用一个变量去指示一种状态的改变,如果用过单片机,应该很快就明白,这相当于一种flag,那么按照同样的思想,使用一个Variables来代表一种状态的改变,而这种状态本身可能意味着一系列的操作。
  Condition Variables是用Mutex实现,但是却封装了一些操作使其成为原子操作,避免竞态的发生。比如当pthread_cond_wait中对于当前Condition Variables的检查,当其状态未发生变化时使调用该函数的线程的休眠,如果这两个步骤不是原子操作的话,可能有潜在的竞态发生。
  Condition Variables的使用方式有并不复杂,遵循以下步骤:

  • 使用pthread_cond_init或者 PTHREAD_COND_INITIALIZER去初始化一个Condition Variables。同时也要初始化一个Mutex
  • 在需要等待状态改变的地方调用pthread_cond_wait并将上一步的加锁的MutexCondition Variables作为参数传递给该函数。注意在pthread_cond_wait 之后要解锁。
  • 在需要改变状态的地方使用Mutex加锁,进行一系列的操作之后,调用pthread_cond_signal或者pthread_cond_broadcast来唤醒其他被阻塞在该Condition Variables的线程,最后进行Mutex解锁。

注意以下两点:

  • The caller passes it locked to the function, which then atomically places the calling thread on the list of threads waiting for the condition and unlocks the mutex.
  • When pthread_cond_wait returns, the mutex is again locked

3.1 Condition Variable Attributes

#include <pthread.h>int pthread_condattr_init(pthread_condattr_t *attr);int pthread_condattr_destroy(pthread_condattr_t *attr);Both return: 0 if OK, error number on failureint pthread_condattr_getpshared(const pthread_condattr_t * restrict attr, int *restrict pshared);int pthread_condattr_setpshared(pthread_condattr_t *attr, int pshared);Both return: 0 if OK, error number on failureint pthread_condattr_getclock(const pthread_condattr_t * restrict attr, clockid_t *restrict clock_id);int pthread_condattr_setclock(pthread_condattr_t *attr, clockid_t clock_id);Both return: 0 if OK, error number on failure

  pthread_condattr_initpthread_condattr_destroy与上面的Mutex的pthread_mutexattr_initpthread_mutexattr_destroy效果类似,在此不赘述。Condition Variable Attributes的有两个属性: process-sharedclock

  • process-shared:决定是否Condition Variable在多个进程间共享。
  • clock:决定pthread_cond_timedwaittsptr使用的时钟源。

四:实验部分


实验一

题目:使用哈希表来组织数据,使4个线程都能够并发的访问,修改哈希表。具体场景

  • 线程1-3创建一个新的节点,并加入哈希表,且打印该节点的值。
  • 线程4打印整个哈希表。
  • 与单线程版本相比,单单比较执行时间。

  上面的题目是博主自己想出来的,那么场景的假设,也是理解多线程的一个重要的环节,并不是所有的场合都适合多线程编程!。首先回忆下上一篇中提到多线程编程的优点,对于多个互不依赖的任务,多线程编程能够显著改善执行效率。当然,这又不是绝对的。对于一些明显顺序执行一些任务的场合(一个任务依赖另一个任务的结果),使用多线程依然能改善程序效率。
  先分析下这里假设的场景,显然线程1,2,3可以通过并发执行来改善效率,而线程4如果旨在打印整个被更新后的哈希表,那么很自然的依赖于线程1,2,3的执行结果。所以设计这个程序我遵循几个准则:

  • 对于所有哈希表的接口函数,都考虑多线程并发的可能性
  • 将4线程作为主phread来最后打印整个哈希表。
  • 使用MutexCondition Variable配合完成同步控制

实验二

题目:请构造一个使用RECURSIVE的Mutex的场合。

为了防止阅读疲劳,不增加篇幅,代码部分将单独记录。^_^