应用编程之线程(二-同步篇)

线程的主要优势在于,资源的共享性,譬如通过全局变量来实现信息共享,不过这种便捷的共享是有代价的,那就是多个线程并发访问共享数据所导致的数据不一 致的问题。

一、为什么需要线程同步

  • 线程同步是为了对共享资源的访问进行保护。
  • 保护的目的是为了解决数据一致性的问题。
  • 出现数据一致性问题其本质在于进程中的多个线程对共享资源的并发访问(同时访问)。

二、互斥锁

互斥锁( mutex )又叫互斥量,从本质上说是一把锁,在访问共享资源之前对互斥锁进行上锁,在访问完成后释放互斥锁(解锁)。
互斥锁使用 pthread_mutex_t 数据类型表示,在使用互斥锁之前,必须首先对它进行初始化操作,可以使用两种方式对互斥锁进行初始化操作。

1、互斥锁初始化

1)使用 PTHREAD_MUTEX_INITIALIZER 宏初始化互斥锁

        pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

THREAD_MUTEX_INITIALIZER 宏已经携带了互斥锁的默认属性。

2)使用 pthread_mutex_init()函数初始化互斥锁

使用 PTHREAD_MUTEX_INITIALIZER 宏只适用于在定义的时候就直接进行初始化,对于其它情况则不能使用这种方式,譬如先定义互斥锁,后再进行初始化,或者在堆中动态分配的互斥锁,譬如使用 malloc()函数申请分配的互斥锁对象,那么在这些情况下,可以使用 pthread_mutex_init() 函数对互斥锁进行初始化。

2、互斥锁加锁和解锁

互斥锁初始化之后,处于一个未锁定状态,调用函数 pthread_mutex_lock() 可以对互斥锁加锁、获取互斥锁,而调用函数 pthread_mutex_unlock() 可以对互斥锁解锁、释放互斥锁。
调用 pthread_mutex_lock() 函数对互斥锁进行上锁,如果互斥锁处于未锁定状态,则此次调用会上锁成功,函数调用将立马返回;如果互斥锁此时已经被其它线程锁定了,那么调用 pthread_mutex_lock() 会一直 阻塞,直到该互斥锁被解锁,到那时,调用将锁定互斥锁并返回。
调用 pthread_mutex_unlock() 函数将已经处于锁定状态的互斥锁进行解锁。以下行为均属错误:
对处于未锁定状态的互斥锁进行解锁操作;
解锁由其它线程锁定的互斥锁。
如 果 有 多 个 线 程 处 于 阻 塞 状 态 等 待 互 斥 锁 被 解 锁 , 当 互 斥 锁 被 当 前 锁 定 它 的 线 程 调 用 pthread_mutex_unlock()函数解锁后,这些等待着的线程都会有机会对互斥锁上锁,但无法判断究竟哪个线程会加锁成功。

3、销毁互斥锁

当不再需要互斥锁时,应该将其销毁,通过调用 pthread_mutex_destroy() 函数来销毁互斥锁。
不能销毁还没有解锁的互斥锁,否则将会出现错误;
没有初始化的互斥锁也不能销毁。
pthread_mutex_destroy() 销毁之后的互斥锁,就不能再对它进行上锁和解锁了,需要再次调用
pthread_mutex_init() 对互斥锁进行初始化之后才能使用。

4、互斥锁死锁

如果一个线程试图对同一个互斥锁加锁两次,会出现什么情况?情况就是该线程会陷入死锁
状态,一直被阻塞永远出不来;这就是出现死锁的一种情况。
有时,一个线程需要同时访问两个或更多不同的共享资源,而每个资源又由不同的互斥锁管理。当超过一个线程对同一组互斥锁(两个或两个以上的互斥锁)进行加锁时,就有可能发生死锁;譬如,程序中使用一个以上的互斥锁,如果允许一个线程一直占有第一个互斥锁,并且在试图锁住第二个互斥锁时处于阻塞状态,但是拥有第二个互斥锁的线程也在试图锁住第一个互斥锁。因为两个线程都在相互请求另一个线程拥有的资源,所以这两个线程都无法向前运行,会被一直阻塞,于是就产生了死锁。
要避免此类死锁的问题,最简单的方式就是定义互斥锁的层级关系,当多个线程对一组互斥锁操作时,总是应该按照相同的顺序对该组互斥锁进行锁定。

三、条件变量

条件变量是线程可用的另一种同步机制。条件变量用于自动阻塞线程,知道某个特定事件发生或某个条件满足为止,通常情况下,条件变量是和互斥锁一起搭配使用的。
使用条件变量主要包括两个动作:
一个线程等待某个条件满足而被阻塞;
另一个线程中,条件满足时发出“信号”。
条件变量通常搭配互斥锁来使用,是因为条件的检测是在互斥锁的保护下进行的,也就是说条件本身是由互斥锁保护的,线程在改变条件状态之前必须首先锁住互斥锁,不然就可能引发线程不安全的问题。

1、条件变量初始化

条件变量使用 pthread_cond_t 数据类型来表示,类似于互斥锁,在使用条件变量之前必须对其进行初始 化。初始化方式同样也有两种:使用宏 PTHREAD_COND_INITIALIZER 或者使用函数 pthread_cond_init() , 使用宏的初始化方法与互斥锁的初始化宏一样
对于初始化与销毁操作,有以下问题需要注意:
在使用条件变量之前必须对条件变量进行初始化操作,使用 PTHREAD_COND_INITIALIZER 宏或者函数 pthread_cond_init() 都行;
对已经初始化的条件变量再次进行初始化,将可能会导致未定义行为;
对没有进行初始化的条件变量进行销毁,也将可能会导致未定义行为;
对某个条件变量而言,仅当没有任何线程等待它时,将其销毁才是最安全的;
pthread_cond_destroy() 销毁的条件变量,可以再次调用 pthread_cond_init() 对其进行重新初始化

2、通知和等待条件变量

条件变量的主要操作便是发送信号(signal )和等待。发送信号操作即是通知一个或多个处于等待状态的线程,某个共享变量的状态已经改变,这些处于等待状态的线程收到通知之后便会被唤醒,唤醒之后再检查条件是否满足。等待操作是指在收到一个通知前一直处于阻塞状态。
函数 pthread_cond_signal() pthread_cond_broadcast() 均可向指定的条件变量发送信号,通知一个或多个处于等待状态的线程。调用 pthread_cond_wait()函数是线程阻塞,直到收到条件变量的通知。当程序当中使用条件变量,当判断某个条件不满足时,调用 pthread_cond_wait()函数将线程设置为等待状态(阻塞)。
注意注意的是,条件变量并不保存状态信息,只是传递应用程序状态信息的一种通讯机制。如果调用pthread_cond_signal()和 pthread_cond_broadcast() 向指定条件变量发送信号时,若无任何线程等待该条件变量, 这个信号也就会不了了之。
当调用 pthread_cond_broadcast() 同时唤醒所有线程时,互斥锁也只能被某一线程锁住,其它线程获取锁失败又会陷入阻塞。
综上,当线程锁住互斥锁之后,线程内会判断此时判定条件是否符合,不符合就休眠并解锁,当收到其他线程信号时会被唤醒,再次判断条件是否满足,满足就执行接下来的语句,不满足继续休眠。

3、条件变量的判断条件

使用条件变量,都会有与之相关的判断条件,通常情况下,会涉及到一个或多个共享变量。

判断条件必须使用 while 循环,而不是 if 语句,这是一种通用的设计原则:当线程从 pthread_cond_wait()返回时, 并不能确定判断条件的状态,应该立即重新检查判断条件,如果条件不满足,那就继续休眠等待。
pthread_cond_wait() 返回后,并不能确定判断条件是真还是假,其理由如下:
当有多于一个线程在等待条件变量时,任何线程都有可能会率先醒来获取互斥锁,率先醒来获取到 互斥锁的线程可能会对共享变量进行修改,进而改变判断条件的状态。
可能会发出虚假的通知。

四、自旋锁

自旋锁与互斥锁很相似,从本质上说也是一把锁,在访问共享资源之前对自旋锁进行上锁,在访问完成 后释放自旋锁(解锁);事实上,从实现方式上来说,互斥锁是基于自旋锁来实现的,所以自旋锁相较于互斥锁更加底层。

如果在获取自旋锁时,自旋锁处于未锁定状态,那么将立即获得锁(对自旋锁上锁);如果在获取自旋锁时,自旋锁已经处于锁定状态了,那么获取锁操作将会在原地“自旋”,直到该自旋锁的持有者释放了锁。
由此介绍可知,自旋锁与互斥锁相似,但是互斥锁在无法获取到锁时会让线程陷入阻塞等待状态;而自旋锁在无法获取到锁时,将会在原地“自旋”等待。“自旋”其实就是调用者一直在循环查看该自旋锁的持有者 是否已经释放了锁,“自旋”一词因此得名。
自旋锁的不足之处在于:自旋锁一直占用的 CPU ,它在未获得锁的情况下,一直处于运行状态(自旋), 所以占着 CPU ,如果不能在很短的时间内获取锁,这无疑会使 CPU 效率降低。
试图对同一自旋锁加锁两次必然会导致死锁,而试图对同一互斥锁加锁两次不一定会导致死锁,原因在于互斥锁有不同的类型,当设置为 PTHREAD_MUTEX_ERRORCHECK 类型时,会进行错误检查,第二次加锁会返回错误,所以不会进入死锁状态。
因此我们要谨慎使用自旋锁,自旋锁通常用于以下情况:需要保护的代码段执行时间很短,这样就会使 得持有锁的线程会很快释放锁,而“自旋”等待的线程也只需等待很短的时间;在这种情况下就比较适合使用自旋锁,效率高!
综上所述,再来总结下自旋锁与互斥锁之间的区别:
实现方式上的区别:互斥锁是基于自旋锁而实现的,所以自旋锁相较于互斥锁更加底层;
开销上的区别:获取不到互斥锁会陷入阻塞状态(休眠),直到获取到锁时被唤醒;而获取不到自 旋锁会在原地“自旋”,直到获取到锁;休眠与唤醒开销是很大的,所以互斥锁的开销要远高于自 旋锁、自旋锁的效率远高于互斥锁;但如果长时间的“自旋”等待,会使得 CPU 使用效率降低,
故自旋锁不适用于等待时间比较长的情况。(举例来说,当自旋锁发现锁被别人锁住了,它就会一直在旁边等这把锁开,互斥锁发现锁被锁住后会找地方休眠并等着别的函数发信号来唤醒它
使用场景的区别:自旋锁在用户态应用程序中使用的比较少,通常在内核代码中使用比较多;因为自旋锁可以在中断服务函数中使用,而互斥锁则不行,在执行中断服务函数时要求不能休眠、不能被抢占(内核中使用自旋锁会自动禁止抢占),一旦休眠意味着执行中断服务函数时主动交出了CPU 使用权,休眠结束时无法返回到中断服务函数中,这样就会导致死锁!

五、读写锁

互斥锁或自旋锁要么是加锁状态、要么是不加锁状态,而且一次只有一个线程可以对其加锁。读写锁有 3 种状态:读模式下的加锁状态(以下简称读加锁状态)、写模式下的加锁状态(以下简称写加锁状态)和不加锁状态。
一次只有一个线程可以占有写模式的读写锁,但是可以有多个线程同时占有读模式的读写锁。因此可知,读写锁比互斥锁具有更高的并行性!
读写锁有如下两个规则:
当读写锁处于写加锁状态时,在这个锁被解锁之前,所有试图对这个锁进行加锁操作(不管是以读模式加锁还是以写模式加锁)的线程都会被阻塞。
当读写锁处于读加锁状态时,所有试图以读模式对它进行加锁的线程都可以加锁成功;但是任何以写模式对它进行加锁的线程都会被阻塞,直到所有持有读模式锁的线程释放它们的锁为止。
读写锁也叫做共享互斥锁。当读写锁是读模式锁住时,就可以说成是共享模式锁住。当它是写模式锁住时,就可以说成是互斥模式锁住。
  • 7
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值