前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Linux 线程间通信和同步

Linux 线程间通信和同步

作者头像
Jasonangel
发布2022-10-25 21:21:54
1.4K0
发布2022-10-25 21:21:54
举报

很多时候,我们做项目并不会创建那么多进程,而是创建一个进程,在该进程中创建多个线程进行工作。

一、进程与线程

1、什么是进程、线程,有什么区别?

进程是资源(CPU、内存等)分配的基本单位,线程是 CPU 调度和分配的基本单位(程序执行的最小单位)。如果 CPU 是单核,同一时间只有一个进程在执行,多核 CPU 可以同一时间点有多个进程在执行。

2、多进程、多线程的优缺点

一个进程由进程控制块、数据段、代码段组成,进程本身不可以运行程序,而是像一个容器一样,先创建出一个主线程,分配给主线程一定的系统资源,这时候就可以在主线程开始实现各种功能。

当我们需要实现更复杂的功能时,可以在主线程里创建多个子线程,多个线程在同一个进程里,利用这个进程所拥有的系统资源合作完成某些功能。

优缺点:

  1. 一个进程死了不影响其他进程,一个线程崩溃很可能影响到它本身所处的整个进程。
  2. 创建多进程的系统花销大于创建多线程。
  3. 多进程通讯因为需要跨越进程边界,不适合大量数据的传送。多线程无需跨越进程边界,适合大量数据的传送。
3、什么时候用进程,什么时候用线程
  1. 创建和销毁较频繁使用线程,因为创建进程花销大。
  2. 需要大量数据传送使用线程,因为多线程切换速度快,不需要跨越进程边界。
  3. 安全稳定选进程;快速频繁选线程;

二、线程间通信/同步

上一篇文章我们讲了进程间通信的六种方式:管道和 FIFO、信号、消息队列、信号量、共享内存、套接字(Socket),今天我们讲一下线程间通信/同步的方式。

线程同步的方法:互斥锁、条件变量、自旋锁、读写锁,除此之外,还有信号量、屏障等等,在 Linux 应用开发当中,用的最多的还是互斥锁和条件变量。

为什么需要线程同步?

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

1、互斥锁

互斥锁(mutex),在访问共享资源之前对互斥锁进行上锁,在访问完成后释放互斥锁(解锁);对互斥锁进行上锁之后,任何其它试图再次对互斥锁进行加锁的线程都会被阻塞,直到当前线程释放互斥锁。如果释放互斥锁时有一个以上的线程阻塞,那么这些阻塞的线程会被唤醒,它们都会尝试对互斥锁进行加锁,当有一个线程成功对互斥锁上锁之后,其它线程就不能再次上锁了,只能再次陷入阻塞,等待下一次解锁。

初始化互斥锁

代码语言:javascript
复制
#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

加锁、解锁

代码语言:javascript
复制
#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

当互斥锁已经被其它线程锁住时,调用 pthread_mutex_lock()函数会被阻塞,直到互斥锁解锁;如果线程不希望被阻塞,可以使用 pthread_mutex_trylock()函数;调用 pthread_mutex_trylock()函数尝试对互斥锁进行加锁,如果互斥锁处于未锁住状态,那么调用 pthread_mutex_trylock()将会锁住互斥锁并立马返回,如果互斥锁已经被其它线程锁住,调用 pthread_mutex_trylock()加锁失败,但不会阻塞,而是返回错误码 EBUSY。

代码语言:javascript
复制
#include <pthread.h>

int pthread_mutex_trylock(pthread_mutex_t *mutex);

销毁互斥锁(不再需要互斥锁时,应该将其销毁)

代码语言:javascript
复制
#include <pthread.h>

int pthread_mutex_destroy(pthread_mutex_t *mutex);

互斥锁死锁

如果一个线程试图对同一个互斥锁加锁两次,该线程会陷入死锁状态,一直被阻塞永远出不来;这就是出现死锁的一种情况。

有时,一个线程需要同时访问两个或更多不同的共享资源,而每个资源又由不同的互斥锁管理。当超过一个线程对同一组互斥锁(两个或两个以上的互斥锁)进行加锁时,就有可能发生死锁;譬如,程序中使用一个以上的互斥锁,如果允许一个线程一直占有第一个互斥锁,并且在试图锁住第二个互斥锁时处于阻塞状态,但是拥有第二个互斥锁的线程也在试图锁住第一个互斥锁。因为两个线程都在相互请求另一个线程拥有的资源,所以这两个线程都无法向前运行,会被一直阻塞,于是就产生了死锁。

2、条件变量

条件变量用于自动阻塞线程,直到某个特定事件发生或某个条件满足为止,通常情况下,条件变量是和互斥锁一起搭配使用的。使用条件变量主要包括两个动作:

  1. 一个线程等待某个条件满足而被阻塞;
  2. 另一个线程中,条件满足时发出“信号”。

条件变量通常搭配互斥锁来使用,是因为条件的检测是在互斥锁的保护下进行的,也就是说条件本身是由互斥锁保护的,线程在改变条件状态之前必须首先锁住互斥锁,不然就可能引发线程不安全的问题。

初始化和销毁条件变量

代码语言:javascript
复制
#include <pthread.h>

int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
int pthread_cond_destroy(pthread_cond_t *cond);

条件变量的主要操作是发送信号(signal)和等待。发送信号操作即是通知一个或多个处于等待状态的线程,某个共享变量的状态已经改变,这些处于等待状态的线程收到通知之后便会被唤醒,唤醒之后再检查条件是否满足。等待操作是指在收到一个通知前一直处于阻塞状态。

函数 pthread_cond_signal()和 pthread_cond_broadcast()均可向指定的条件变量发送信号,通知一个或多个处于等待状态的线程。调用 pthread_cond_wait()函数是线程阻塞,直到收到条件变量的通知。

通知条件变量

代码语言:javascript
复制
#include <pthread.h>

int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

pthread_cond_signal()函数至少能唤醒一个线程,而 pthread_cond_broadcast()函数则能唤醒所有线程。

等待条件变量

代码语言:javascript
复制
#include <pthread.h>

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
3、自旋锁

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

自旋锁与互斥锁之间的区别:

  1. 实现方式上的区别:互斥锁是基于自旋锁而实现的,所以自旋锁相较于互斥锁更加底层;
  2. 开销上的区别:获取不到互斥锁会陷入阻塞状态(休眠),直到获取到锁时被唤醒;而获取不到自旋锁会在原地“自旋”,直到获取到锁;休眠与唤醒开销是很大的,所以互斥锁的开销要远高于自旋锁、自旋锁的效率远高于互斥锁;但如果长时间的“自旋”等待,会使得 CPU 使用效率降低,故自旋锁不适用于等待时间比较长的情况。
  3. 使用场景的区别:自旋锁在用户态应用程序中使用的比较少,通常在内核代码中使用比较多;因为自旋锁可以在中断服务函数中使用,而互斥锁则不行,在执行中断服务函数时要求不能休眠、不能被抢占(内核中使用自旋锁会自动禁止抢占),一旦休眠意味着执行中断服务函数时主动交出了CPU 使用权,休眠结束时无法返回到中断服务函数中,这样就会导致死锁!

初始化和销毁自旋锁

代码语言:javascript
复制
#include <pthread.h>

int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_destroy(pthread_spinlock_t *lock);

加锁和解锁

代码语言:javascript
复制
#include <pthread.h>

int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);
4、读写锁

互斥锁或自旋锁要么是加锁状态、要么是不加锁状态,而且一次只有一个线程可以对其加锁。

读写锁有3 种状态:读模式下的加锁状态(以下简称读加锁状态)、写模式下的加锁状态(以下简称写加锁状态)和不加锁状态(见),一次只有一个线程可以占有写模式的读写锁,但是可以有多个线程同时占有读模式的读写锁。因此可知,读写锁比互斥锁具有更高的并行性!

读写锁有如下两个规则:

  1. 当读写锁处于写加锁状态时,在这个锁被解锁之前,所有试图对这个锁进行加锁操作(不管是以读模式加锁还是以写模式加锁)的线程都会被阻塞。
  2. 当读写锁处于读加锁状态时,所有试图以读模式对它进行加锁的线程都可以加锁成功;但是任何以写模式对它进行加锁的线程都会被阻塞,直到所有持有读模式锁的线程释放它们的锁为止。

读写锁非常适合于对共享数据读的次数远大于写的次数的情况。

初始化和销毁读写锁

代码语言:javascript
复制
#include <pthread.h>

int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

加锁和解锁

代码语言:javascript
复制
#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);
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-07-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 嵌入式Linux系统开发 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、进程与线程
    • 1、什么是进程、线程,有什么区别?
      • 2、多进程、多线程的优缺点
        • 3、什么时候用进程,什么时候用线程
        • 二、线程间通信/同步
          • 为什么需要线程同步?
            • 1、互斥锁
              • 2、条件变量
                • 3、自旋锁
                  • 4、读写锁
                  相关产品与服务
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档