线程同步
同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。
"同"字从字面上容易理解为一起动作
其实不是,"同"字应是指协同、协助、互相配合。
如进程、线程同步,可理解为进程或线程A和B一块配合,A执行到一定程度时要依靠B的某个结果,于是停下来,示意B运行;B依言执行,再将结果给A;A再继续操作。
所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回,同时其它线程也不能调用这个方法。按照这个定义,其实绝大多数函数都是同步调用(例如sin, isdigit等)。但是一般而言,我们在说同步、异步的时候,特指那些需要其他部件协作或者需要一定时间完成的任务。例如Window API函数SendMessage。该函数发送一个消息给某个窗口,在对方处理完消息之前,这个函数不返回。当对方处理完毕以后,该函数才把消息处理函数所返回的LRESULT值返回给调用者。
在多线程编程里面,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何时刻,最多有一个线程访问,以保证数据的完整性。
线程同步的方式和机制
临界区(Critical Section)、互斥量(Mutex)、信号量(Semaphore)、事件(Event)的区别
1、临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。在任意时刻只允许一个线程对共享资源进行访问,如果有多个线程试图访问公共资源,那么在有一个线程进入后,其他试图访问公共资源的线程将被挂起,并一直等到进入临界区的线程离开,临界区在被释放后,其他线程才可以抢占。
2、互斥量:采用互斥对象机制。 只有拥有互斥对象的线程才有访问公共资源的权限,因为互斥对象只有一个,所以能保证公共资源不会同时被多个线程访问。互斥不仅能实现同一应用程序的公共资源安全共享,还能实现不同应用程序的公共资源安全共享
3、信号量:它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目
4、事 件: 通过通知操作的方式来保持线程的同步,还可以方便实现对多个线程的优先级比较的操作
线程的实现一般包括用户级和内核级以及组合的情况
但是组合的情况是比较理想化的
因此我们一般都是根据场景选择用户级线程创建或者内核级线程创建
他们的优缺点分别是
信号量处理线程同步
我们可以用信号量模拟一个线程的同步
例如我们要顺序输出五个abc
那么我们模拟这个线程是一个试衣间 0表示不能使用 1表示可以使用
然后 p v 操作时是对应的获取资源操作和释放资源操作
然后我们可以对abc三个试衣间进行循环的p v操作
这样就可以实现abc的顺序输出
那么三个试衣间 在这个场景下 我们就需要使用三个信号量来配合操作
对于信号量方法不熟悉的可以浏览我的往期博文
这是线程内部函数 我们可以看到在循环中对信号量进行p v 操作即可实现有序化控制
我们可以看到对信号量初始化的时候我们对a的初始化为1
因我我们的要求是顺序输出ABC
那么就要从a开始 我们就将a的初始化赋值为1
然后在循环中我们不停的进行p v操作
这样就可以实现一个有序化输出
互斥锁处理进程同步
在编程中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
那么为什么要使用互斥锁这个概念呢
我们就要再次提到我们上一节中提到的
如果有五个线程 每一个线程都对一个val进行加加操作
那么我们预计输出的结果是五千
但是其实是有几率出现不是五千的情况 这是因为我们又可能会出现两个线程对一个val同时加加
那么就会出现少加的情况 这样得到的数就会小于5000
这个和处理器的数量有关系
如果是单核处理器 那么同一时间就只允许一个线程访问
那么就会按照规则完成5000次加加 最后得到的数据一定是5000
但是如果处理器数大于1 就有几率出现两个线程抢占 然后导致最后得到的结果不是5000
可以看到我使用的本地unbantu的配置的处理器数量为四
那么我们出现小于5000的概率就会大大提升
可以看到仅一次运行就出现我们描述的问题
那么我们 可以通过互斥锁来解决
锁操作主要包括加锁pthread_mutex_lock()、解锁pthread_mutex_unlock()和测试加锁 pthread_mutex_trylock()三个,不论哪种类型的锁,都不可能被两个不同的线程同时得到,而必须等待解锁。对于普通锁和适应锁类型,解锁者可以是同进程内任何线程;而检错锁则必须由加锁者解锁才有效,否则返回EPERM;对于嵌套锁,文档和实现要求必须由加锁者解锁,但实验结果表明并没有这种限制,这个不同还没有得到解释。在同一进程中的线程,如果加锁后没有解锁,则任何其他线程都无法再获得锁。
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_trylock()语义与pthread_mutex_lock()类似,不同的是在锁已经被占据时返回EBUSY而不是挂起等待。
我们可以使用这个锁对关键语句进行操作
那么这段代码在运行时就是受到保护 便不会出现两个线程争抢的问题
我们可以看到运行多次都会是5000
这就是互斥锁的作用
那么关于互斥锁还有重要的一点是 互斥锁要加到最需要保护的代码段处
因为锁也会消耗时间 如果每次都在经历多段代码
那么代码运行成功的时间就会增加
读写锁处理线程同步
ReadWriteLock同Lock一样也是一个接口,提供了readLock和writeLock两种锁的操作机制,一个是只读的锁,一个是写锁。ReentranReadWriteLock是其实现类
读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的(排他的)。 每次只能有一个写线程,但是可以有多个线程并发地读数据。
所有读写锁的实现必须确保写操作对读操作的内存影响。换句话说,一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容。
理论上,读写锁比互斥锁允许对于共享数据更大程度的并发。与互斥锁相比,读写锁是否能够提高性能取决于读写数据的频率、读取和写入操作的持续时间、以及读线程和写线程之间的竞争。
读写锁和互斥锁的区别
1)读写锁区分读者和写者,而互斥锁不区分
2)互斥锁同一时间只允许一个线程访问该对象,无论读写;读写锁同一时间内只允许一个写者,但是允许多个读者同时读对象。
读写锁是在读端或者写端的时候进行锁
那么我们读的时候读都是被允许的 写不被允许
相反 写的时候写都是允许的 而读不被允许
例如读写操作比较多的时候我们就可以使用读写锁来实现线程同步
读写锁的实现就是表现形式和互斥锁不同
其它的操作均可以模仿互斥锁
唯一需要注意的是就是要把读写锁要对应
在读端要使用读锁
写端使用使用写锁
在多线程的实现中 线程步是十分重要的 它关乎线程能否稳定安全的实现既定的功能