一文搞定Linux线程间通讯 / 线程同步方式-互斥锁、读写锁、自旋锁、信号量、条件变量、信号等等

目录

线程间通讯 / 线程同步方式

锁机制

互斥锁(Mutex)

读写锁(rwlock)

自旋锁(spin)

信号量机制(Semaphore)

条件变量机制

信号(Signal)


扫描二维码关注公众号,回复: 16723651 查看本文章

线程间通讯 / 线程同步方式

p.s 以下有很多段落是直接引用,没有使用 markdown 的 “引用” 格式,出处均已放出。

参考 / 引用:

由于线程间共享进程变量资源,线程间的通信目的主要是用于线程同步(即约束多个线程的执行的顺序规则(信号量)或互斥规则(互斥锁)),所以线程没有像进程通信中的用于数据交换的通信机制。

互斥锁、条件变量和信号量的区别:

  • 互斥锁:互斥,一个线程占用了某个资源,那么其它的线程就无法访问,直到这个线程解锁,其它线程才可以访问。

  • 信号量:同步,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作。而且信号量有一个更加强大的功能,信号量可以用作为资源计数器,把信号量的值初始化为某个资源当前可用的数量,使用一个之后递减,归还一个之后递增。

  • 条件变量:同步,一个线程完成了某一个动作就通过条件变量发送信号告诉别的线程,别的线程再进行某些动作。条件变量必须和互斥锁配合使用。

锁机制适用于类似原子操作的情况,加锁后 快速的处理某一个临界区的资源,然后立马解锁,不适合长时间的加锁(更不好的情况就是在加锁后的临界区里被执行了中断,在中断里面又要阻塞的进行加锁,那么这时就发生死锁了,卡住了);而信号/信号量(和 条件变量)适合 长时间的 等待 信号/条件的发生,而且 在等待/阻塞 期间调用者是休眠的。

为了减少错误 和 复杂性,设计程序前应尽量考虑 不要在 中断中 使用 锁、信号 等之类的东西,中断程序里面设计的要 简洁、优雅。

锁机制

互斥锁(Mutex)

互斥锁 / 互斥量 用来防止多个线程 同时/并发的 访问某个临界资源。

互斥量本质上说是一把锁,在访问共享资源前对互斥量进行加锁,在访问完成后释放互斥量。对互斥量进行加锁以后,其他识图再次对互斥量加锁的线程都会被阻塞直到当前线程释放该互斥锁。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。

如果释放互斥量时有一个以上的线程阻塞,那么所有该锁上的阻塞线程都会变成可运行状态,第一个变成运行状态的线程可以对互斥量加锁,其他线程就会看到互斥量依然是锁着,只能再次阻塞等待它重新变成可用,这样,一次只有一个线程可以向前执行。

即 多个线程都要访问某个临界资源,比如某个全局变量时,需要互斥地访问:我访问时,你不能访问。

头文件:#include <pthread.h>

常用 API:

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr); // 初始化量
/* 该函数初始化一个互斥量,第一个参数是改互斥量指针,第二个参数为控制互斥量的属性,一般为 NULL。当函数成功后会返回 0,代表初始化互斥量成功。
    当然初始化互斥量也可以调用宏来快速初始化,代码如下:
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITALIZER;
*/
​
int pthread_mutex_lock(pthread_mutex_t *mutex);                                       // 加锁(阻塞)
int pthread_mutex_unlock(pthread_mutex_t *mutex);                                     // 解锁(非阻塞)
/* lock 函数与 unlock 函数分别为加锁解锁函数,只需要传入已经初始化好的 pthread_mutex_t 互斥量指针。成功后会返回 0。
    当某一个线程获得了执行权后,执行 lock 函数,一旦加锁成功后,其余线程遇到 lock 函数时候会发生阻塞,直至获取资源的线程执行 unlock 函数后。unlock 函数会唤醒其他正在等待互斥量的线程。
    特别注意的是,当获取 lock 之后,必须在逻辑处理结束后执行 unlock,否则会发生死锁现象!导致其余线程一直处于阻塞状态,无法执行下去。在使用互斥量的时候,尤其要注意使用 pthread_cancel 函数,防止发生死锁现象!
*/
​
int pthread_mutex_trylock(pthread_mutex_t *mutex);                                   // 互斥量加锁(非阻塞)
/* 该函数同样也是一个线程加锁函数,但该函数是非阻塞模式通过返回值来判断是否加锁成功,用法与上述阻塞加锁函数一致。 */
​
int pthread_mutex_destroy(pthread_mutex_t *mutex);                                    // 销毁互斥量
/* 该函数是用于销毁互斥量的,传入互斥量的指针,就可以完成互斥量的销毁,成功返回0。 */

例程:参考 pthread库-线程编程例程-来自百问网\01_文档配套源码\Pthread_Text10.c

互斥锁的属性:

/* 与 线程属性类似的,先 声明变量,再用 pthread_mutexattr_init 初始化,
    再用 pthread_mutexattr_getxxx/pthread_mutexattr_setxxx 来 获取 / 设置 属性的某个选项,
    然后在 调用 互斥锁初始化 pthread_mutex_init 的时候 填入 该属性
    最后可以销毁 */
int pthread_mutexattr_init(pthread_mutexattr_t* attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t* attr);
​
int pthread_mutexattr_getshared(const pthread_mutexattr_t* attr, int* pshared);
int pthread_mutexattr_setshared(pthread_mutexattr_t* attr, int* pshared);
    pshared 的可以传入的参数:
        PTHREAD_PROCESS_SHARED:互斥锁可以被跨进程共享
        PTHREAD_PROCESS_PRIVATE:只能被初始化线程所属的进程中的线程共享
int pthread_mutexattr_gettype(const pthread_mutexattr_t* attr, int* type);
int pthread_mutexattr_settype(pthread_mutexattr_t* attr, int type);
    type 的可以传入的参数:
        PTHREAD_MUTEX_NOMAL:公平锁,对一个已经加锁的普通锁再次加锁,将引发死锁;对一个已经被其他线程加锁的普通锁解锁,或者对一个已经解锁的普通锁再次解锁,将导致不可预期的后果。
        PTHREAD_MUTEX_ERRORCHECK:检错锁,对一个已经加锁的检错锁再次加锁,则加锁操作返回EDEADLOCK。对一个已经被其他线程加锁的检错锁解锁,或者对一个已经解锁的检错锁再次解锁,则解锁操作返回EPERM。
        PTHREAD_MUTEX_RECURSIVE:嵌套锁,错误使用返回EPERM
        PTHREAD_MUTEX_DEFAULT:跟nomal差不多。
读写锁(rwlock)

 读写锁与互斥量类似,不过读写锁拥有更高的并行性。互斥量要么是锁住状态,要么是不加锁状态,而且一次只有一个线程可以对其加锁。读写锁有 3 种状态:读模式下加锁状态,写模式下加锁状态,不加锁状态

一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞。当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是任何希望以写模式对此锁进行加锁的线程都会阻塞,直到所有的线程释放它们的读锁为止。

头文件:#include <pthread.h>

常用 API:

int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *rwlockattr); // 初始化读写锁
​
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);                                       // 读模式锁定读写锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);                                       // 写模式锁定读写锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);                                       // 解锁读写锁
​
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);                                      // 销毁读写锁
自旋锁(spin)

如果 互斥锁 被占用(被锁住),另一个线程进入时,互斥锁会引起线程切换(不死等,而是 yield 一次去运行其它线程)。适合锁的内容比较多的。

对于自旋锁,如果锁被占用(被锁住),来了的线程会一直等待直到获取这把锁相当于 while(1);(死等,跑满一个时间片时间)。适合锁的内容比较少的 当线程切换的代价远远比等待的代价大的时候,使用自旋锁。(如果对于 RTOS 有了解的,就会明白 yeild、跑满一个时间片时间 等的概念,可以玩一玩 单片机上的 FreeRTOS、RTT 就了解了)。

自旋锁与互斥量类似,但它不是通过休眠使进程阻塞,而是在获取锁之前一直处于忙等(自旋)阻塞状态。自旋锁可以用于以下情况:锁被持有的时间短,而且线程并不希望在重新调度上花费太多的成本。

如果一个可执行线程试图获得一个被争用(已经被持有的)自旋锁,那么该线程就会一直进行忙等待,自旋,也就是空转,等待锁重新可用(等待被解锁)。一个被争用的自旋锁使得请求它的线程在等待锁重新可用时自旋,特别的浪费CPU时间,所以自旋锁不应该被长时间的持有。实际上,这就是自旋锁的设计初衷,在短时间内进行轻量级加锁。自旋锁 也不能用在 中断程序里面,自旋锁保持期间是抢占失效的(内核不允许被抢占)。

头文件:#include <pthread.h>

常用 API:

int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
/* 自旋锁 初始化函数,里面没有 属性变量,pshared 可以选择的参数:
    PTHREAD_PROCESS_PRIVATE 只在本进程内 使用该锁
    PTHREAD_PROCESS_SHARED 该锁可能位于共享内存对象中,在多个进程之间共享
    如果想要使用自旋锁同步多进程,那么设置 pshared 为 PTHREAD_PROCESS_SHARED,然后在进程共享内存中分配 pthread_spinlock_t 对象即可(pthread_mutex_t 亦如此)。
*/
​
int pthread_spin_destroy(pthread_spinlock_t *lock);
​
// 自旋锁操作
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);

信号量机制(Semaphore)

信号量 是一个大于等于 0 的量,为 0 时候线程阻塞。通过 sem_pos 函数(非阻塞) 给信号量增加 1,sem_wait 函数(阻塞) 当 信号量大于 0 时候对其 减少 1,等于 0 时阻塞。

可以用来做通知用,也可以用来计数:

  • 信号量 可以控制多个 本来先后执行是随机无序的线程 的 执行顺序可控 和 可预测。例如 线程 A 在等待某件事,线程 B 完成了这件事后就可以给线程 A 发信号。

  • 当需要一个计数器来限制可以使用某共享资源的线程数目时,可以使用“信号量”对象。信号量 保存了对当前访问某一个指定资源的线程的计数值,该计数值是当前还可以使用该资源的线程数目。如果这个计数达到了零,则所有对这个 信号量 所控制的资源的访问尝试都被放入到一个队列中等待,直到超时或计数值不为零为止。

头文件:#include <semaphore.h>

常用 API:

int sem_init(sem_t *sem, int pshared, unsigned int value);    // 初始化一个信号量 
/* 该函数可以初始化一个信号量,第一个参数传入sem_t类型指针。
    第二个参数传入 0 代表线程控制(多线程内使用),否则为进程控制(多进程使用)。
    第三个参数表示信号量的初始值,0代表阻塞,1以及更大的数字代表初始值(不阻塞)。
    待初始化结束信号量后,若执行成功会返回0。 */
int sem_destroy(sem_t * sem);                                 // 销毁信号量
​
int sem_wait(sem_t * sem);                                    // 信号量减少 1(阻塞),P 操作
int sem_post(sem_t * sem);                                    // 信号量增加 1(非阻塞),V 操作
int sem_trywait(sem_t *sem);                                  // 信号量减少 1(非阻塞),成功返回 0 
/* sem_wait 函数作用为检测指定信号量是否有资源可用,若无资源可用会阻塞等待,若有资源可用会自动的执行 sem - 1 的操作。
   sem_post 函数会释放指定信号量的资源,执行 sem + 1 操作。
   即信号量的 申请 与 释放 */
​
int sem_timedwait (sem_t * sem,const struct timespec * abstime); // 在 sem_wait 的 等待/阻塞 期间内最多 等待 abstime 秒的时间 就返回
int sem_post_multiple (sem_t * sem,int count);                // 一次 信号量增加 count(非阻塞)
​
int sem_getvalue(sem_t * sem, int * sval);                    // 获取当前信号量的值

例程:参考 pthread库-线程编程例程-来自百问网\01_文档配套源码\Pthread_Text12.cpthread库-线程编程例程-来自百问网\02_视频配套源码\pthread3.c 和 pthread4.c

条件变量机制

条件变量时一种同步机制,用来通知其他线程条件满足了,即 一个线程被挂起,直到某件事件发生。一般是用来通知对方共享数据的状态信息,因此条件变量时结合互斥量来使用的。

条件变量 是在多线程程序中用来实现 “等待--->唤醒” 逻辑常用的方法。

头文件:#include <pthread.h>

常用 API:

int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);  // 初始化条件变量
/* 若 不需要设置属性则 cond_attr 填为N ULL */
/* 另一种以默认方式初始化 pthread_cond_t cond = PTHREAD_COND_INITIALIZER; */
int pthread_cond_destroy(pthread_cond_t *cond);                               // 销毁条件变量
​
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);          // 无条件等待条件变量变为真
int pthread_cond_timewait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *tsptr);  // 在给定时间内,等待条件变量变为真
​
int pthread_cond_signal(pthread_cond_t *cond); // 通知条件变量 条件满足啦,pthread_cond_signal 函数只会唤醒一个等待 cond 条件变量的线程
​
使用:
    
    发信号:
    pthread_mutex_lock(&mutex);    先加锁
        这里修改临界资源             再修改资源
    pthread_cond_signal(&cond);    再发条件变量信号 
    pthread_mutex_unlock(&mutex);  再解锁
​
    等待信号:
    pthread_mutex_lock(&mutex);    先加锁 /* 可以加锁的时候 加锁 然后往后运行,否则阻塞 */
    pthread_cond_wait(&cond, &mutex); 再等待条件变量信号 /* 如果条件不满足则,会 先解锁 mutex,然后 阻塞/休眠 在这里等待条件满足;条件满足后被唤醒,先加锁 mutex 然后取消阻塞 往后执行*/
        这里修改临界资源             再修改资源
    pthread_mutex_unlock(&mutex);  再解锁
    ...

例程:参考 pthread库-线程编程例程-来自百问网\02_视频配套源码\pthread5.c

信号(Signal)

在 一个进程的 多个线程 之间处理 信号(signal),与 进程中的信号处理不一样,需要多学、多用一下。

例如,当你发送一个SIGSTP信号给进程,这个进程的所有线程都会停止。因为所有线程内用同样的内存空间,所以对一个signal的handler都是一样的,但不同的线程有不同的管理结构所以不同的线程可以有不同的mask(引自 Linux线程的实现 & LinuxThread vs. NPTL & 用户级内核级线程 & 线程与信号处理 - blcblc - 博客园 (cnblogs.com))。

参考:

猜你喜欢

转载自blog.csdn.net/Staokgo/article/details/132630769