线程通信
线程之间通信的两个基本问题是互斥和同步
线程同步windows和linux的区别
(1)针对共享全局变量的问题:两系统是一样的,除了采用bool变量,还有互斥量,即加锁。
(2)windows使用事件Event或信号量Semaphore,linux使用条件变量
linux在头文件sys/sem.h中有信号量
同步:一个线程执行依赖另一个线程,直到等到消息到达这个线程才被唤醒
互斥:对于共享的操作系统资源,任何时刻最多只允许一个线程去使用
从大的方面讲,线程的同步可分用户模式的线程同步和内核对象的线程同步两大类。
用户模式中线程(linux 线程大多如此,切换由用户态程序控制,自己控制内核切换,不能很好利用多核CPU)的同步方法主要有原子访问(对原子操作--写操作而言,使用volatile变量,写变量与之后的变量读建立联系,发生happens-before,意味着一个volatile变量的变化对其他线程是可见的)和临界区等方法。其特点是同步速度特别快,适合于对线程运行速度有严格要求的场合。操作系统内核不知道多线程的存在,一个线程阻塞是整个进程阻塞。
内核对象的线程(windows线程大多如此,切换由内核控制,线程切换就由用户态转变为内核态,很好利用smp,利用多核cpu)同步则主要由事件、等待定时器、信号量以及信号灯等内核对象构成。由于这种同步机制使用了内核对象,使用时必须将线程从用户模式切换到内核模式,而这种转换一般要耗费近千个CPU周期,因此同步速度较慢,但在适用性上却要远优于用户模式的线程同步方式。
在用户模式下进行线程同步到额最大好处就是速度快,在创建或清除内核对象时调用线程必须从用户态切换到内核模式,切换耗时。
在Windows下线程同步的方式有:互斥量,信号量,事件,关键代码段 。只有临界区不是内核对象,它不由操作系统的低级部件管理,而且不能使用句柄来操纵。线程的id是唯一对应的,线程id就是身份证,一个线程有多个句柄,句柄就是银行卡,同一银行卡必是一个人,不同银行卡不一定就不是一个人。
1.临界区Critical Section
一段独占对某些共享资源访问的代码,在任意时刻只允许一个线程对共享资源进行访问。如果有多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的
(1).创建临界区,声明CRITICAL_SECTION数据结构(全局的),该临界区结构的分配必须是全局的,这样该进程的不同线程就能访问它。
(2).在使用临界区同步线程之前,必须调用InitializeCriticalSection(参数为之前声明的CRITICAL_SECTION数据结构)来初始化临界区。在释放资源之前,只需要初始化一次。
(3).在线程中使用VOID EnterCriticalSection:阻塞函数。调用线程不能获取指定临界区的所有权时,该线程将睡眠,且在被唤醒之前,系统不会给它分配CPU。(拥有该临界区的线程,每一次针对此临界区的EnterCriticalSection调用都会成功(这里指的是重复调用也会立即返回),且会使得临界区标志(即一个CRITICAL_SECTION全局变量)的引用计数+1。在另一个线程能够拥有该临界区之前,拥有它的线程必须调用LeaveCriticalSection足够多次,在引用计数降为零后,另一线程才有可能拥有 该临界区。换言之,在一个正常使用临界区的线程中,calSection和LeaveCriticalSection应该成对使用。)
(4).执行临界区内的任务
(5).在线程中最后使用BOOL LeaveCriticalSection:非阻塞函数。将当前线程对指定临界区的引用计数减壹;在使用计数变为零时,另一等待此临界区的一个线程将被唤醒。
(6).当不需要再使用该临界区时,使用DeleteCriticalSection来释放临界区需要的资源。此函数执行后,再也不能使用 EnterCriticalSection和LeaveCriticalSection,除非再次使用 InitializeCriticalSection初始化了该临界区。
2.事件Event(可处于激发状态(进程、线程结束)/有信号状态或未激发状态/无信号状态(创建进程、线程))
根据状态变迁方式的不同,事件可分为两类:
(1)手动设置:这种对象只可能用程序手动设置,在需要该事件或者事件发生时,采用SetEvent及ResetEvent来进行设置。
(2)自动恢复:一旦事件发生并被处理后,自动恢复到没有事件状态,不需要再次设置。
如果跨进程访问事件,必须对事件命名,在对事件命名的时候,要注意不要与系统命名空间中的其它全局命名对象冲突;
由于event对象属于内核对象,CreateEvent创建或打开一个命名的或无名的事件对象,故进程B可以调用OpenEvent函数通过对象的名字获得进程A中event对象的句柄,然后将这个句柄用于ResetEvent(不发信号)、SetEvent(发信号)和WaitForMultipleObjects(线程设为睡眠等待内核对象变为有信号,等到位发信号状态时返回)等函数中。此法可以实现一个进程的线程控制另一进程中线程的运行
WaitForMultipleObjects会根据不同的内核对象,决定是否改变内核对象的信号状态。
DWORD WaitForSingleObject
(
HANDLE hHandle,//事件句柄
DWORD dwMilliseconds//时间间隔,INFINITE为永久,当函数执行时间超过就返回
);
(1)如果是自动置位事件,那么每一次WaitForSingleObject后,此时状态就会变成未激发状态,就要用SetEvent()进行**。
(2)如果是手动置位事件,每一次WaitForSingleObject后,不会改变原有的状态。要利用ResetEvent()设置为未**状态,再利用SetEvent()进行**。
使用方式:定义事件句柄 HANDLE hEvent,主函数创建事件hEvent=CreateEvent(),然后各线程调用WaitForSingleObject,根据事件置位属性操作
事件没有所有权属性,即线程获得事件后,一定要等待事件再次被触发
3.信号量Semaphore
信号量是维护0到指定最大值之间的同步对象。信号量状态在其计数大于0时是有信号的,而其计数是0时是无信号的。信号量对象在控制上可以支持有限数量共享资源的访问。信号量不为负,不超过最大值。
转载一些函数说明,
使用方式:定义事件句柄 HANDLE hSemaphore,主函数创建事件hEvent=CreateSemaphore(),然后各线程调用WaitForSingleObject,然后ReleaseSemaphore,这个函数有返回值,返回线程释放是否成功,所以这个函数是在释放前增加信号量(起始设的全局句柄)的数目,然后就将等待这句柄代表的线程释放了。
信号量没有线程所有权属性,即一个线程获得某个信号量后,在他释放该信号量之前,他不能再次进入信号量保护的区域
4.互斥量Mutex
采用互斥对象机制。 只有拥有互斥对象的线程才有访问公共资源的权限,因为互斥对象只有一个,所以能保证公共资源不会同时被多个线程访问。互斥不仅能实现同一应用程序的公共资源安全共享,还能实现不同应用程序的公共资源安全共享。
互斥器的功能和临界区域很相似。区别是:Mutex所花费的时间比Critical Section多的多,但是Mutex是核心对象(Event、Semaphore也是),可以跨进程使用,而且等待一个被锁住的Mutex可以设定 TIMEOUT,不会像Critical Section那样无法得知临界区域的情况,而一直死等。
创建互斥体CreateMutex() ,打开互斥体OpenMutex(),释放互斥体ReleaseMutex()。Mutex的拥有权并非属于那个产生它的线程,而是最后那个对此 Mutex进行等待操作(WaitForSingleObject等等)并且尚未进行ReleaseMutex()操作的线程。线程拥有Mutex就好像进入Critical Section一样,一次只能有一个线程拥有该Mutex。如果一个拥有Mutex的线程在返回之前没有调用ReleaseMutex(),那么这个 Mutex就被舍弃了,但是当其他线程等待(WaitForSingleObject等)这个Mutex时,仍能返回,并得到一个 WAIT_ABANDONED_0返回值。能够知道一个Mutex被舍弃是Mutex特有的。
全局声明句柄,主线程创建互斥量,各个线程WaitForSingleObject,然后ReleaseMutex释放
互斥量也和临界区一样有所有权属性,拥有互斥量的线程可以重复进入互斥量保护的区域
【个人总结理解所有权属性】
参考https://www.cnblogs.com/sou1boy/articles/4202813.html
像互斥量、临界区拥有所有权属性,线程可以重复进入保护区域,即不用等待释放互斥量,线程就能进入,只不过在标志位+1/-1,但是不同线程要进入必须这个标志位到0
而没有所有权属性的信号量、事件,不能重复进入,且必须等待能被触发以后才能进入,个人理解事件的手动设置属性下不进行resetevent的效果和互斥量、临界区的表现一样??
在Linux下线程同步的方式有:互斥锁,自旋锁,读写锁,屏障
1.互斥锁(只有两种状态,上锁和解锁)
1)在访问共享资源后临界区域前,对互斥锁进行加锁。
2)在访问完成后释放互斥锁导上的锁。
3)对互斥锁进行加锁后,任何其他试图再次对互斥锁加锁的线程将会被阻塞,直到锁被释放。
静态初始化互斥锁 (相当于动态初始化attr=NULL)
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
动态初始化互斥锁
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);//锁的地址;锁的类型
返回值:0,成功申请的锁默认是打开的;非0 错误码
int pthread_mutex_lock(pthread_mutex_t *mutex);//若上锁,调用者阻塞,直到互斥锁解锁
返回值:0成功,非0错误码
int pthread_mutex_trylock(pthread_mutex_t *mutex);//未加锁则上锁,返回0;已加锁,返回EBUSY
int pthread_mutex_unlock(pthread_mutex_t * mutex);//解锁函数,成功返回0,失败返回非0
int pthread_mutex_destroy(pthread_mutex_t *mutex);//(此时锁必需unlock,否则返回EBUSY)销毁互斥锁,释放资源,返回值同上
2.条件变量
条件变量是利用线程间共享全局变量进行同步的一种机制。条件变量上的基本操作有:触发条件(当条件变为 true 时);等待条件,挂起线程直到其他线程触发条件。
初始化或者pthread_cond_t cond=PTHREAD_COND_INITIALIER(前者为动态初始化,后者为静态初始化);属性通常置为NULL
int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr);
等待函数
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);//条件变量,互斥锁
必须和一个互斥锁配合,以防止多个线程同时请求(用 pthread_cond_wait() 或 pthread_cond_timedwait() 请求)竞争条件。mutex互斥锁必须是普通锁(PTHREAD_MUTEX_TIMED_NP)或者适应锁(PTHREAD_MUTEX_ADAPTIVE_NP),在调用pthread_cond_wait()前必须由本线程加锁(pthread_mutex_lock()),pthread_cond_wait 自动解锁互斥量(如同执行了pthread_unlock_mutex),并等待条件变量触发。这时线程挂起,不占用CPU时间,直到条件变量被触发(变量为ture)pthread_cond_wait函数返回前,自动重新对互斥量加锁(如同执行了pthread_lock_mutex)。
一般可能会有一种结构
while(1){
while(){
pthread_cond_wait();//因为pthread_cond_wait里的线程可能会被意外唤醒,如果这个时候head != NULL,则不是我们想要的情况。
}
}
int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);如果在给定时刻前条件没有满足,则返回ETIMEOUT,结束等待,其中abstime以与time()系统调用相同意义的绝对时间形式出现,0表示格林尼治时间1970年1月1日0时0分0秒。
int pthread_cond_destroy(pthread_cond_t *cond);//销毁条件变量,无线程等待,否则返回EBUSY
int pthread_cond_signal(pthread_cond_t *cond);//**一个等待该条件的线程(存在多个等待线程时按入队顺序**其中一个)
int pthread_cond_broadcast(pthread_cond_t *cond); //**所有等待的线程,其和pthread_cond_signal可以不在lock和unlock范围中使用
3.信号量
如同进程一样,线程也可以通过信号量来实现通信,虽然是轻量级的。
线程使用的基本信号量函数有四个:
#include <semaphore.h>
(1). 初始化信号量
int sem_init (sem_t *sem , int pshared, unsigned int value);/*sem - 指定要初始化的信号量;
pshared - 信号量 sem 的共享选项,linux只支持0,表示它是当前进程的局部信号量;
value - 信号量 sem 的初始值。*/
(2). 信号量值加1,给参数sem指定的信号量值加1。
int sem_post(sem_t *sem);
(3). 信号量值减1,给参数sem指定的信号量值减1。
int sem_wait(sem_t *sem);
如果sem所指的信号量的数值为0,函数将会等待直到有其它线程使它不再是0为止。
(4). 销毁信号量,销毁指定的信号量。进入 pthread_cond_destroy 之前,必须没有在该条件变量上等待的线程。
int sem_destroy(sem_t *sem);
4.读写锁
读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。
在读写锁保持期间也是抢占失效的。
如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里,直到没有任何写者或读者。如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放该读写锁。
一次只有一个线程可以占有写模式的读写锁, 但是可以有多个线程同时占有读模式的读写锁. 正是因为这个特性,
当读写锁是写加锁状态时, 在这个锁被解锁之前, 所有试图对这个锁加锁的线程都会被阻塞.
当读写锁在读加锁状态时, 所有试图以读模式对它进行加锁的线程都可以得到访问权, 但是如果线程希望以写模式对此锁进行加锁, 它必须直到所有的线程释放锁.
通常, 当读写锁处于读模式锁住状态时, 如果有另外线程试图以写模式加锁, 读写锁通常会阻塞随后的读模式锁请求, 这样可以避免读模式锁长期占用, 而等待的写模式锁请求长期阻塞.
读写锁适合于对数据结构的读次数比写次数多得多的情况. 因为, 读模式锁定时可以共享, 以写模式锁住时意味着独占, 所以读写锁又叫共享-独占锁
初始化和销毁
#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
成功则返回0, 出错则返回错误编号.
阻塞的读加锁和写加锁
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
成功则返回0, 出错则返回错误编号.
非阻塞获得读锁和写锁,如果可以获取则返回0, 否则返回错误的EBUSY.
#include <pthread.h>
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
成功则返回0, 出错则返回错误编号.
5.自旋锁
与其他锁一样,自旋锁也用于保护临界区,但是自旋锁主要是用于在SMP上保护临界区。
自旋锁(spinlock)是用在多个CPU系统中的锁机制,当一个CPU正访问自旋锁保护的临界区时,临界区将被锁上,其他需要访问此临界区的CPU只能忙等待(while循环),直到前面的CPU已访问完临界区,将临界区开锁。自旋锁上锁后让等待线程进行忙等待而不是睡眠阻塞,而信号量是让等待线程睡眠阻塞。自旋锁的忙等待浪费了处理器的时间,但时间通常很短,在1毫秒以下。
自旋锁用于多个CPU系统中,在单处理器系统中,自旋锁不起锁的作用,若内核支持抢占,spin_lock关抢占,spin_unlock关抢占。在自旋锁忙等待期间,内核抢占机制还是有效的,等待自旋锁释放的线程可能被更高优先级的线程抢占CPU。
自旋锁基于共享变量。一个线程通过给共享变量设置一个值来获取锁,其他等待线程查询共享变量是否为0来确定锁现是否可用,然后在忙等待的循环中"自旋"直到锁可用为止。
自旋锁的状态值为1表示解锁状态,说明有1个资源可用;0或负值表示加锁状态,0说明可用资源数为0。Linux内核为通用自旋锁提供了API函数初始化、测试和设置自旋锁。
声明锁:
spinlock_t lock;
初始化:
lock = SPIN_LOCK_UNLOCKED;或者spin_lock_init(&lock);
加锁有4个接口,3个会阻塞,1个不阻塞
spin_lock(&lock);//获取自旋锁
spin_lock_irq(&lock);//关中断,获取自旋锁,不建议使用
spin_lock_irqsave(&lock, flags);//关中断,保存中断状态,获取自旋锁
spin_trylock(&lock);//与spin_lock一样,但是获取不到的时候不阻塞,返回非0
对应的解锁接口有3个
spin_unlock(&lock);//spin_lock和spin_trylock都用该接口解锁
spin_unlock_irq(&lock);//不建议使用
spin_unlock_irqrestore(&lock, flags);
另外还提供了一个获取锁状态的接口:
spin_is_locked(&lock);//如果指定的锁被获取,返回非0,否则,返回0
6.屏障
屏障是Linux中协调多个线程并行工作的同步机制,屏障允许每个线程等待直到所有的合作线程到达某一点,然后继续从该点执行,pthread_join是一种屏障但只允许一个线程等待,pthread_barrier允许任意数量的线程等待!
int pthread_barrier_destroy(pthread_barrier_t *barrier); //两个函数的返回值:若成功,返回0;否则,返回错误编号
int pthread_barrier_init(pthread_barrier_t *restrict barrier,
const pthread_barrierattr_t *restrict attr, unsigned count);//可以使用count参数指定,在允许所有线程继续运行之前,必须到达屏障的线程数目,屏障属性attr设置为NULL表示使用默认属性。
int pthread_barrier_wait(pthread_barrier_t *barrier);//返回值:若成功,返回0或者PTHREAD_BARRIER_SERIAL_THREAD;否则,返回错误编号
调用pthread_barrier_wait的线程在屏障技术count未满足条件时,会进入休眠状态。如果该线程是最后一个调用pthread_barrier_wait的线程,就满足了屏障计数,所有的线程都被唤醒。
对于一个任意线程,pthread_barrier_wait函数返回PTHREAD_BARRIER_SERIAL_THREAD。剩下的线程看到的返回值是0。这使得一个线程可以作为主线程,它可以工作在其他所有线程已完成的工作结果上。