什么是线程同步?
在多线程编程时,如果变量是只读的,多个线程读取变量时并不会有一致性问题,但是,当一个线程可以修改的变量,其他的线程也可以读或者修改的时候,我们就需要对线程就行同步,确保线程看到的数据必须一致,确保访问变量时不会出现无效的值
Linux对线程提供了几种基本的同步机制
一. 互斥量
互斥量从本质上来说是一把锁,多线程编程时,在访问共享资源时对互斥量进行设置(即加锁),访问完成之后,再释放(解锁)互斥量.
对互斥量进行加锁之后,任何试图再次对互斥量进行加锁的线程都会被阻塞直到当前线程释放该互斥锁.
如果释放时有一个以上的线程阻塞,那么该锁上的阻塞线程都会变成可运行状态,第一个变为运行的线程就可以对互斥量进行加锁,其他线程看到互斥锁依然是锁着的时候就只能回去再次重新阻塞等待.
互斥变量使用pthread_mutex_t数据类型表示的,使用互斥变量之前,需要对它进行初始化
·可以把互斥量设置为PTHREAD_MUTEX_INITIALIZER(使用于静态分配的互斥量)
·可以调用pthread_mutex_init函数进行初始化,如果动态分配了互斥量,在释放内存前需要调用pthread_mutex_destroy函数
销毁互斥量时需要注意:
·使用PTHREAD_MUTEX_INITIALIZER初始化的互斥量不需要销毁
·不要销毁一个已经加锁了的互斥量
·已经销毁的互斥量,要确保后面不会再有线程尝试加锁
int pthread_mutex_init(pthread_mutex_t* restrict mutex,
(const pthread_mutexattr_t* restrict attr);
int pthread_mutex_destroy(pthread_mutex_t* mutex);
对互斥量加锁解锁
·对互斥量进行加锁操作需要调用pthread_mutex_lock函数
·对互斥量进行解锁需要调用pthread_mutex_unlock函数
·如果线程不希望加锁时被阻塞,可以使用pthread_mutex_trylock函数尝试对互斥量进行加锁.如果调用时互斥量处于未锁状态,则这个函数会锁住互斥量,不会阻塞等待,否则pthread_mutex_trylock就会失败,不能锁住互斥量,返回EBUSY
举个栗子:
在主函数中,对全局变量互斥量mutex进行初始化,然后再创建四个线程,然后线程等待,最后再末尾调用pthread_mutex_destroy函数
![]()

四个线程使用同一个线程处理函数,每个线程都是一个循环,循环内部对操作进行加锁和解锁

注意,操作互斥量时,可能会出现死锁的情况
情况一:如果一个线程对同一个互斥量进行加锁两次,那么它就会陷入死锁状态,因为想要对互斥量加锁,必须保证互斥量没有被锁,不然就只能等待,当我加第二次锁的时候,发现互斥量已经加锁了,就阻塞的等待,但是因为没有解锁,就会一直等下去
情况二:程序中使用一个以上的互斥量时,如果允许一个线程一直占有第一个互斥量,并且在试图锁住第二个互斥量时处于阻塞状态,但是拥有第二个互斥量的线程也在试图锁住第一个互斥量.就造成两个线程都无法向前运行,就会产生死锁
二. 读写锁
读写锁(reader-writer lock)与互斥量类似,不过读写锁允许更高的并行性
互斥量只有两种状态,要么是不加锁状态,要么就是锁状态,而且一次只有一个线程可以对其加锁
读写锁有三个状态:
①读模式下的加锁状态
②写模式下的加锁状态
③不加锁状态
读写锁的特征:
·一次只有一个线程可以占有写模式的读写锁,但是可以有多个线程占有读模式的读写锁
·当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞.
·当读写锁是读加锁状态时,所有试图以读模式对它进行加锁的线程都可以访问,但是任何以希望以写模式对锁进行加锁的线程都会被阻塞,直到所有线程释放他们的读锁
·需要注意的一点.当读写锁处于读模式的状态时,线程试图以写模式获取上锁时,读写锁会阻塞后面的读模式的锁请求,这样可以避免读模式的锁被长期占用.
·读写锁适合于对数据结构读的次数远大于写的情况
·读写锁也叫共享互斥锁,当读写锁以读模式锁住时,就可以说成是以共享模式锁住的,当读写锁以写模式锁住时,就可以说成是以互斥模式锁住的
读写锁的初始化和销毁函数:
int pthread_rwlock_init(pthread_rwlock_t* restrict rwlock,
const pthread_rwlockattr_t* restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t* rwlock);
两个函数的返回值:成功返回0,否则返回错误编号
读写锁的加锁和解锁:
读模式加锁
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.失败返回错误编号
读写锁也有条件版本:
int pthread_rwlock_tryrdlock(pthread_rwlock_t* rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t* rwlock);
所有函数的返回值:成功返回0.失败返回错误编号
三. 条件变量
条件变量是线程可用的另一种同步机制,条件变量给多线程提供了一个会和的场所.条件变量与互斥量一起使用时,互斥锁用于上锁,条件变量则用于等待,运行线程以无竞争的方式等待特定的条件发生
条件变量本身是由互斥量保护的,线程在改变条件变量之前必须先锁住互斥量.其他线程在获得互斥量之前不会察觉到这种变化,因为互斥量必须是在锁定之后才能计算条件
条件变量是由pthread_cond_t数据类型表示的,有两种初始化方式,一种是把常量PTHREAD_COND_INITIALIZER赋给静态分配的条件变量,另一种是使用初始化函数
条件变量初始化函数
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *rest rict attr);
参数:
cond:要初始化的条件变量
attr:NULL
销毁函数
int pthread_cond_destroy(pthread_cond_t *cond);
使用pthread_cond_wait等待条件变量变为真
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mute x);
参数:
cond:要在这个条件变量上等待
mutex:互斥量
调用者给pthread_cond_wait函数传入一个互斥量mutex,这个互斥量会对条件进行保护,这个函数自动把调用线程放到等待条件的线程列表上,对互斥量进行解锁,然后等待条件变量触发.这是线程挂起,不占用CPU时间,直到条件变量被触发.这个函数返回前会自动重新给互斥量加锁
互斥量的解锁和在条件变量上挂起都是自动进行的.因此条件变量触发之前,所有线程都要对互斥量进行加锁.
唤醒等待
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
这两个函数用于通知线程条件已满足,pthread_cond_signal函数至少能唤起一个等待的线程,而pthread_cond_broadcast则可以唤起所有等待该条件的线程
举个栗子,创建两个线程,分别打印奇数和偶数

在两个线程处理函数之中,比如处理偶数的函数中,如果当前value值是偶数,就打印并把value自增1,然后使用pthread_cond_signal函数通知条件变量odd,如果不为偶数,就使用pthread_cond_wait函数阻塞的等待

这里也是同样的做法

这是最后的结果:

使用条件变量的优点:
① 可以避免线程竞争,由于线程之间是异步进行的,所以每个线程得到锁的概率依照线程的优先级来决定,如果想要让某个线程执行特定的任务,就可以利用条件变量,让系统立即调度拥有特定条件变量的线程,避免线程竞争
② 可以提高效率,如果一个线程得到锁,那么其他的线程得不到锁,就会不停的去查询锁有没有被释放,这样会浪费大量的资源,利用条件变量可以使得不到锁的线程处于阻塞的等待状态,直到被使用pthread_cond_signal函数通知,可以提高效率
四. POSIX信号量
POSIX信号量和SystemV信号量的作用相同,都是用于同步操作,达到无冲突的访问共享资源的目的,但是POSIX信号量可以用于线程的同步
创建一个信号量,创建的时候还要求初始化信号量的值
// 无名信号量的初始化和销毁
int sem_init(sem_t* sem, int pshared, unsigned int value);
参数:
pshared,0表示线程共享,非0表示进程共享
value: 信号量初始值
int sem_destroy(sem_t* sem);
sem_init() 用于无名信号量的初始化。无名信号量在初始化前一定要子啊内存中分配一个 sem_t 信号量类型,这就是无名信号量又称为基于内存的信号量的原因。
// 有名信号量的创建删除
sem_t *sem_open(const char *name, int oflag);
sem_t *sem_open(const char *name, int oflag,
mode_t mode, unsigned int value);
oflag 参数可以为:0 , O_CREAT ,O_EXCL ,如果为 0 表示打开一个已存在的信号量,如果为 O_CREAT ,表示如果信号量不存在就创建一个信号量,如果存在则打开被返回。此时 mode 和 value 需要指定。如果为 O_CREAT | O_EXCL ,表示如果信号量已存在会返回错误。
mode 参数用于创建信号量时,表示信号量的权限位,和 open 函数一样包括:S_IRUSR , S_IWUSR ,S_IRGRP ,S_IWGRP ,S_IROTH ,S_IWOTH
value 表示创建信号量时,信号量的初始值。
等待信号量, 该操作会检查信号量的值,如果其值小于或等于0 ,那就阻塞,知道该值变成大于 0 ,然后等待进程将信号量的值减 1 ,进程获得共享资源的访问权限.这整个操作必须是一个原子操作.
int sem_wait(sem_t* sem);
#ifdef __USE_XOPEN2K
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
#endif
int sem_trywait (sem_t * sem);
发布信号量.该操作将信号量的值加 1 ,如果有进程阻塞着等待该信号量,那么其中一个进程将被唤醒.该操作也必须是一个原子操作
int sem_post(sem_t* sem);
获取当前信号量的值
int sem_getvalue(sem_t *sem, int *sval);
//成功返回0,失败返回-1
五. 自旋锁
自旋锁(spinlock)与互斥量 类似,但是它不是通过休眠使进程阻塞,而是在获取锁之前一直处于忙等阻塞状态.
自旋锁可以用于以下情况:锁被持有的时间端,而且线程并不希望在重新调度上花太多的成本
自旋锁用于多个CPU系统中,在单处理器系统中,自旋锁不起锁的作用,只是禁止或启用内核抢占.在自旋锁忙等待期间,内核抢占机制还是有效的,等待自旋锁释放的线程可能被更高优先级的线程抢占CPU
自旋锁基于共享变量。一个线程通过给共享变量设置一个值来获取锁,其他等待线程查询共享变量是否为0来确定锁现是否可用,然后在忙等待的循环中"自旋"直到锁可用为止
自旋锁的接口与互斥量类似,这使得它可以比较容易的从一个替换为另一个.可以用pthread_spin_init函数对自旋锁进行初始化,使用pthread_spin_destroy函数进行自旋锁的反复初始化
自旋锁的初始化
int pthread_spin_init(pthread_spinlock_t* lock, int pshared);
int pthread_spin_destroy(pthread_spinlock_t* lock);
两个函数的返回值:若成功返回0,,失败返回错误编号
自旋锁初始化函数中的参数pshared表示进程共享属性,表明自旋锁是如何获取的.
如果pshared被设为PTHREAD_PROCESS_SHARED,则自旋锁可以被访问锁底层的内存的线程获取,即使线程属于不同的进程
如果被设置为PTHREAD_PROCESS_PRIVATE,自旋锁就只能被初始化该锁的进程内部的线程获取
自旋锁的加锁和解锁
int pthread_spin_lock(pthread_spinlokc_t* lock);
int pthread_spin_trylock(pthread_spinlokc_t* lock);
int pthread_spin_unlock(pthread_spinlokc_t* lock);
自旋锁的注意事项:
1.临界区代码不要存在睡眠情况(主要因为发生睡眠不可预知睡眠多长时间,另外长时间睡眠,导致即将进入临界区其他线程,长时间得不到自旋锁,无休止自旋,从而导致死锁),所以临界区调用导致睡眠函数,不能选择自旋锁。
2.保证进入临界区的线程,不发生内核抢占。(这一点不必担心,持有自旋锁情况,Linux内核不进行抢占)
3.临界区代码,执行时间不能太长。(因为其他线程,如果要进入话,导致自旋,过多消耗CPU资源)
4.选择自旋锁时,也要注意中断情况(上半部分中断(硬件中断)和下半部分中断(软中断),中断会抢占即中断到来时,打断目前临界区代码执行,转往执行中断代码),当中断要进入自旋锁保护临界区代码时,将导致线程与中断发生死锁可能。
六. 屏障
屏障(barrier)是用户协调多个线程并行工作的同步机制.屏障允许每个线程等待,直到所有的合作线程都达到某一点,然后从该点继续执行.
屏障的初始化和销毁
int pthread_barrier_destroy(pthread_barrier_t *barrier);
int pthread_barrier_init(pthread_barrier_t *restrict barrier,
const pthread_barrierattr_t *restrict attr, unsigned count);
两个函数的返回值:若成功,返回0;否则,返回错误编号
初始化屏障时, 可以使用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,这使得一个线程可以作为主线程,它可以工作在其他所有线程已完成的工作结果上.
参考的一部分博客:
https://blog.csdn.net/lifuxin73/article/details/51637837
https://blog.csdn.net/xiaofei0859/article/details/23629287

1470

被折叠的 条评论
为什么被折叠?



