• 忘掉天地
  • 仿佛也想不起自己
bingliaolongBingliaolong  2020-03-31 08:20 Aet 隐藏边栏 |   抢沙发  1 
文章评分 0 次,平均分 0.0

线程的互斥

在Posix Thread中定义了一套专门用于线程互斥的mutex函数。mutex是一种简单的加锁的方法来控制对共享资源的存取,这个互斥锁只有两种状态(上锁和解锁),可以把互斥锁看作某种意义上的全局变量。为什么需要加锁,就是因为多个线程共用进程的资源,要访问的是公共区间时(全局变量),当一个线程访问的时候,需要加上锁以防止另外的线程对它进行访问,实现资源的独占。在一个时刻只能有一个线程掌握某个互斥锁,拥有上锁状态的线程能够对共享资源进行操作。若其他线程希望上锁一个已经上锁了的互斥锁,则该线程就会挂起,直到上锁的线程释放掉互斥锁为止。

创建和销毁锁

  • 两种方法创建互斥锁,静态方式和动态方式
    • 静态方式
    • 动态方式

      销毁一个互斥锁即意味着释放它所占用的资源,且要求锁当前处于开放状态。由于在Linux中,互斥锁并不占用任何资源,因此Linux Threads中的pthread_mutex_destroy()除了检查锁状态以外(锁定状态则返回EBUSY)没有其他动作。

互斥锁属性

互斥锁的属性在创建锁的时候指定,在LinuxThreads实现中仅有一个锁类型属性__mutexkind,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同也就是是否阻塞等待。有三个值可供选择:

  • PTHREAD_MUTEX_TIMED_NP,这是缺省值(直接写NULL就是表示这个缺省值),也就是普通锁(或快速锁)。当一个线程加锁以后,其余请求锁的线程将形成一个阻塞等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性

  • PTHREAD_MUTEX_RECURSIVE_NP,嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。

  • PTHREAD_MUTEX_ERRORCHECK_NP,检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。如果锁的类型是快速锁,一个线程加锁之后,又加锁,则此时就是死锁。

锁操作

  • 加锁
    • int pthread_mutex_lock(pthread_mutex_t *mutex)
  • 解锁
    • int pthread_mutex_unlock(pthread_mutex_t *mutex)
  • 测试加锁
    • int pthread_mutex_trylock(pthread_mutex_t *mutex)

pthread_mutex_lock:加锁,不论哪种类型的锁,都不可能被两个不同的线程同时得到,而必须等待解锁。对于普通锁类型,解锁者可以是同进程内任何线程;而检错锁则必须由加锁者解锁才有效,否则返回EPERM;对于嵌套锁,文档和实现要求必须由加锁者解锁,但实验结果表明并没有这种限制,这个不同目前还没有得到解释。在同一进程中的线程,如果加锁后没有解锁,则任何其他线程都无法再获得锁。

pthread_mutex_unlock:根据不同的锁类型,实现不同的行为

  • 对于快速锁,pthread_mutex_unlock解除锁定;
  • 对于递规锁,pthread_mutex_unlock使锁上的引用计数减1
  • 对于检错锁,如果锁是当前线程锁定的,则解除锁定,否则什么也不做

pthread_mutex_trylock:语义与pthread_mutex_lock()类似,不同的是在锁已经被占据时返回EBUSY而不是挂起等待。

加锁注意事项

如果线程在加锁后解锁前被取消,锁将永远保持锁定状态,因此如果在关键区段内有取消点存在,则必须在退出回调函数pthread_cleanup_push/pthread_cleanup_pop中解锁。同时不应该在信号处理函数中使用互斥锁,否则容易造成死锁。

互斥锁实例

总结

  • 定义一个全局的pthread_mutex_t lock; 或者用pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; //则main函数中不用init
  • 在main中调用 pthread_mutex_init函数进行初始化
  • 在子线程函数中调用pthread_mutex_lock加锁
  • 在子线程函数中调用pthread_mutex_unlock解锁
  • 最后在main中调用 pthread_mutex_destroy函数进行销毁

线程的同步

条件变量

条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:

  • 一个线程等待条件变量的条件成立而挂起;
  • 另一个线程使条件成立(给出条件成立信号)。

为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。

创建和注销

条件变量和互斥锁一样,都有静态、动态两种创建方式

  • 静态方式使PTHREAD_COND_INITIALIZER常量

  • 动态方式调用pthread_cond_init()函数

等待和激发

等待条件有两种方式

  • 无条件等待pthread_cond_wait()
  • 计时等待pthread_cond_timedwait()

线程解开mutex指向的锁并被条件变量cond阻塞。其中计时等待方式表示经历abstime段时间后,即使条件变量不满足,阻塞也被解除。无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求pthread_cond_wait()(或pthread_cond_timedwait(),下同)的竞争条件(Race Condition)。mutex互斥锁必须是普通锁(PTHREAD_MUTEX_TIMED_NP),且在调用pthread_cond_wait()前必须由本线程加锁(pthread_mutex_lock()),而在更新条件等待队列以前,mutex保持锁定状态,并在线程挂起进入等待前解锁。在条件满足从而离开pthread_cond_wait()之前,mutex将被重新加锁,以与进入pthread_cond_wait()前的加锁动作对应。(也就是说在做pthread_cond_wait之前,往往要用pthread_mutex_lock进行加锁,而调用pthread_cond_wait函数会将锁解开,然后将线程挂起阻塞。直到条件被pthread_cond_signal激发,再将锁状态恢复为锁定状态,最后再用pthread_mutex_unlock进行解锁)。

激发条件有两种形式,pthread_cond_signal()激活一个等待该条件的线程,存在多个等待线程时按入队顺序激活其中一个;而pthread_cond_broadcast()则激活所有等待线程

其他

pthread_cond_wait()和pthread_cond_timedwait()都被实现为取消点,也就是说如果pthread_cond_wait()被取消,则退出阻塞,然后将锁状态恢复,则此时mutex是保持锁定状态的,而当前线程已经被取消掉,那么解锁的操作就会得不到执行,此时锁得不到释放,就会造成死锁,因而需要定义退出回调函数来为其解锁。

以下示例集中演示了互斥锁和条件变量的结合使用,以及取消对于条件等待动作的影响。在例子中,有两个线程被启动,并等待同一个条件变量,如果不使用退出回调函数(见范例中的注释部分),则tid2将在pthread_mutex_lock()处永久等待。如果使用回调函数,则tid2的条件等待及主线程的条件激发都能正常工作。

不做注释1,2则导致child1中的unlock得不到执行,锁一直没有关闭,而child2中的锁不能执行lock,则会一直在pthread_mutex_lock()处永久等待。如果不做注释5的pthread_cancel()动作,即使没有那些sleep()延时操作,child1和child2都能正常工作。注释3和注释4的延迟使得child1有时间完成取消动作,从而使child2能在child1退出之后进入请求锁操作。如果没有注释1和注释2的回调函数定义,系统将挂起在child2请求锁的地方,因为child1没有释放锁;而如果同时也不做注释3和注释4的延时,child2能在child1完成取消动作以前得到控制,从而顺利执行申请锁的操作,但却可能挂起在pthread_cond_wait()中,因为其中也有申请mutex的操作。child1函数给出的是标准的条件变量的使用方式:回调函数保护,等待条件前锁定,pthread_cond_wait()返回后解锁。

条件变量机制和互斥锁一样,不能用于信号处理中,在信号处理函数中调用pthread_cond_signal()或者pthread_cond_broadcast()很可能引起死锁。

本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

bingliaolong
Bingliaolong 关注:0    粉丝:0 最后编辑于:2021-11-20
Everything will be better.

发表评论

表情 格式 链接 私密 签到
扫一扫二维码分享