精华内容
下载资源
问答
  • 线程间通信
    千次阅读
    2021-02-27 08:58:57

    讲解Handler机制的博文很多,我也看了很多,但说实话,在我对Handler几乎不怎么了解的情况下,每一篇文章我都没太看懂,看完之后脑子里还是充满了疑问。究其原因,是因为几乎每一篇文章一上来就开始深入Handler源码,使得在一些宏观的问题上还是充满疑问,如果你从来没接触过Handler,对一些基础的问题还充满疑问,那深入源码去探究根源肯定会有些吃力。

    下面,我就从一个初学者思考的角度,来讲一讲Handler运行机制,准确的说应该是Android消息处理机制,因为虽然说Handler很重要,它最多被提到,但其它的几个角色也是各司其职,一个都不能少。

    先完整叙述一遍我们要讲解的问题:Android线程间通信机制

    一、两个关键词,两点疑问##

    在"Android线程间通信机制"这句话中,有两个关键词需要我们需要搞清,一个是线程间,一个是通信,当我一开始深入思考这两个关键词的时候,心中就有了些疑问,可是很多博文也没有讲到,这也是导致我一开始看别人写的文章就稀里糊涂的原因,发现文章看完,疑惑还在,而疑惑就来源于这两个关键词。

    1、线程间###

    我们知道,Android应用程序的一个进程当中可能会存在多个线程,但它们的地位是不一样的,分为两种:有一个是主线程(也叫UI线程),其它的都是普通的工作线程。那么线程间通信就会分为两种情况:主线程和工作线程通信;工作线程和工作线程通信。

    之所以要把这两种情况给提出来,是因为基本上所有的文章都是上来直接讲主线程和普通工作线程之间的通信机制,我还没有看到过讲两个普通工作线程通信的,这就让我有了一个疑问,Q1:难道线程间通信只能发生在主线程和工作线程之间吗?而没有两个工作线程通信的情况吗?

    答案是可以的,可能是因为主线程和工作线程通信的情况最常见,例如工作线程向主线程发送消息进行更新UI的操作,而两个工作线程通信的情况比较少见吧(我也是猜测,毕竟我的开发经验太有限)。虽然主线程和普通工作线程地位不同,但只要使用“线程间通信机制”(我们下面要讲的),线程间都是可以互相通信的。相信大家看完文章,不用解释就自然明白了。

    2、通信###

    通信是一个过程,但这个表达很模糊,不够具体,我们把它具体描述应该是:发送消息 + 接收消息 + 处理消息,这样一来定义就清晰了一些。如果我们把这个过程想到这儿,看着好像也明白了,不就那么个过程嘛。但如果大家再多想一步,仔细思考下这个过程,就会心生疑问,至少我当时就有这个疑问,是什么呢,大家先看两张图,是我画的两种通信方式的模型,下面我会解释。

    7657f541c461

    线程间通信.模型猜想一

    我们知道两个线程通信是使用Handler的,这个模型的意思是:我们使用一个Handler进行通信,线程A和线程B都可以发送消息给对方、接收对方传来的消息以及进行消息处理。

    7657f541c461

    线程间通信.模型猜想二

    这个模型的意思是:我们使用一个Handler进行通信,线程A和线程B之间,只有一个可以接收消息并处理,另一个只能够发送。

    那么我的疑问就是,Q2:当我们使用一个Handler进行线程间通信时,到底这两个模型,哪个是正确的?答案是第二个,解释在后面,或许当你看完,也不需要我解释了。

    下面开始正式讲解。

    二、消息处理中的几大角色##

    先给出一张图,这张图是我从别人的博文中看见的,画的不错,我直接拿过来用了,在文末有参考链接。

    7657f541c461

    Android线程间通信流程图

    Message:

    线程间通信就是在传递消息,Message就是消息的载体。常用的有四个字段:arg1,arg2,what,obj。obj可以携带Object对象,其余三个可以携带整形数据。

    MessageQueue:

    MessageQueue是消息队列,它主要用于存放所有通过Handler发送的消息(也就是一个个Message),这部分的消息会一直存在于消息队列中,等待被处理。每个线程中只会有一个MessageQueue对象。

    Looper:

    每个线程通过Handler发送的消息都保存在,MessageQueue中,Looper通过调用loop()的方法,就会进入到一个无限循环当中,然后每当发现Message Queue中存在一条消息,就会将它取出,并传递到Handler的handleMessage()方法中。每个线程中只会有一个Looper对象。

    Handler:

    它主要用于发送和处理消息的发送消息,一般使用sendMessage()方法,还有其他的一系列sendXXX的方法,但最终都是调用了sendMessageAtTime()方法,除了sendMessageAtFrontOfQueue()这个方法。你只要在Looper线程(就是实现了Looper的线程)构建Handler类,那么这个Handler实例就获取该Looper线程MessageQueue实例的引用,Handler 在sendMessage()的时候就通过这个引用往消息队列里插入新消息。

    ThreadLocal:

    这个类我也没太搞懂,不过参考别人的文章有如下一个解释,可以先暂时这样理解:线程本地存储区(Thread Local Storage,简称为TLS),每个线程都有自己的私有的本地存储区域,不同线程之间彼此不能访问对方的TLS区域。MessageQueue对象,和Looper对象在每个线程中都只会有一个对象,怎么能保证它只有一个对象,就通过ThreadLocal来保存

    三、创建Handler的两种方式##

    Handler的创建方式有两种:一个是在主线程中创建,一个是在普通工作线程中创建,两种创建方法是不一样的。Handler在哪个线程中创建,那该线程就负责接收和处理消息,其它的线程只能发送消息。为什么?请往下看。

    1、在主线程中使用Handler###

    在主线程中使用Handler的示例:

    public class TestHandlerActivity extends AppCompatActivity {

    private static final String TAG = "TestHandlerActivity";

    private Handler mHandler = new Handler(){

    @Override

    public void handleMessage(Message msg) {

    super.handleMessage(msg);

    //获得刚才发送的Message对象,然后在这里进行UI操作

    Log.e(TAG,"------------> msg.what = " + msg.what);

    }

    };

    @Override

    protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_handler_test);

    initData();

    }

    private void initData() {

    //开启一个线程模拟处理耗时的操作

    new Thread(new Runnable() {

    @Override

    public void run() {

    SystemClock.sleep(2000);

    //通过Handler发送一个消息切换回主线程(mHandler所在的线程)

    mHandler.sendEmptyMessage(0);

    }

    }).start();

    }

    大家如果使用过Handler,这个应该是最常见的了,也是使用最简单的。只需在主线程创建一个handler对象,在子线程通过在主线程创建的handler对象发送Message,在handleMessage()方法中接受这个Message对象进行处理。通过handler很容易的从子线程切换回主线程了。

    2、在普通工作线程中使用Handler###

    我们下面再看一下,在普通工作线程中应该如何使用Handler:

    public class TestHandlerActivity extends AppCompatActivity {

    private static final String TAG = "TestHandlerActivity";

    //主线程的Handler

    private Handler mHandler = new Handler(){

    @Override

    public void handleMessage(Message msg) {

    super.handleMessage(msg);

    //获得刚才发送的Message对象,然后在这里进行UI操作

    Log.e(TAG,"------------> msg.what = " + msg.what);

    }

    };

    //子线程中的Handler

    private Handler mHandlerThread = null;

    @Override

    protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_handler_test);

    initData();

    }

    private void initData() {

    //开启一个线程模拟处理耗时的操作

    new Thread(new Runnable() {

    @Override

    public void run() {

    SystemClock.sleep(2000);

    //通过Handler发送一个消息切换回主线程(mHandler所在的线程)

    mHandler.sendEmptyMessage(0);

    //调用Looper.prepare()方法

    Looper.prepare();

    mHandlerThread = new Handler(){

    @Override

    public void handleMessage(Message msg) {

    super.handleMessage(msg);

    Log.e("sub thread","---------> msg.what = " + msg.what);

    }

    };

    mHandlerThread.sendEmptyMessage(1);

    //调用Looper.loop()方法

    Looper.loop();

    }

    }).start();

    }

    与在主线程中创建的方式不同,在工作线程中创建的代码中,我们的Handler是在Looper.prepare()和Looper.loop()中间创建的,那这两行代码是做什么用的呢?我们看下源码:

    public final class Looper {

    …………

    private static void prepare(boolean quitAllowed) {

    //如果线程的TLS已有数据,则会抛出异常,一个线程只能有一个Looper,prepare不能重复调用。

    if (sThreadLocal.get() != null) {

    throw new RuntimeException("Only one Looper may be created per thread");

    }

    //往线程的TLS插入数据,简单理解相当于map.put(Thread.currentThread(),new Looper(quitAllowed));

    sThreadLocal.set(new Looper(quitAllowed));

    }

    …………

    }

    在这里可以看出,sThreadLocal对象保存了一个Looper对象,首先判断是否已经存在Looper对象了,以防止被调用两次。sThreadLocal对象是ThreadLocal类型,因此保证了每个线程中只有一个Looper对象。Looper对象在创建时做了什么呢,我们进入看看,如下:

    private Looper(boolean quitAllowed) {

    mQueue = new MessageQueue(quitAllowed);

    mThread = Thread.currentThread();

    }

    可以看出,这里在Looper构造函数中创建出了一个MessageQueue对象和保存了当前线程。从上面可以看出一个线程中只有一个Looper对象,而Message Queue对象是在Looper构造函数创建出来的,因此每一个线程也只会有一个MessageQueue对象。

    所以,当Looper.prepare()执行完了之后,普通的工作线程就变成了Looper线程,该线程就可以接收并处理消息了。如图:

    7657f541c461

    而Looper.loop()方法就是进入一个无限循环,不断的从MessageQueue当中获取消息,当没有消息时就阻塞在那里,这里不再详解。

    3、梳理一下###

    从我们刚才的讲解可以明白:只有实现了Looper和MessageQueue的线程,才能够处理消息,否则一个线程都没有MessageQueue,它又哪来的消息可处理呢?没有Looper,谁来取消息呢?它俩是一套的。

    因此,Handler 对象在哪个线程下构建(Handler的构造函数在哪个线程下调用),那么Handler 就会持有这个线程的Looper引用和这个线程的消息队列的引用。因为持有这个线程的消息队列的引用,意味着这个Handler对象可以在任意其他线程给该线程的消息队列添加消息,也意味着Handler的handlerMessage 肯定也是在该线程执行的。如果该线程不是Looper线程,在这个线程new Handler 就会报错!

    还记得我们刚开始提到的那个线程间通信的简单模型吗,就是下面这个:

    7657f541c461

    因为我们只创建了一个Handler,所以它必定持有某个线程(这里是线程B)的Looper引用和这个线程的消息队列的引用,也就只能在这一个线程中接收和处理消息,其它的只能发送消息。若想实现双向的通信,那就必须在令一个线程当中也创建Looper,并在该线程下再创建一个Handler。

    4、一点疑问###

    我们刚才讲了两种使用Handler的方式,一个是在主线程当中,一个是在其它的普通线程当中。而两种方式的不同就在于:在主线程当中创建Handler,并没有调用Looper.prepare()和Looper.loop()方法,也就是我们没有在主线程中创建Looper。那你可能就会问了,为什么主线程中没有创建Looper,它却可以用来处理消息。为什么?

    因为系统在启动之时,已经帮我们创建好了。也就是说:在任何进程下使用Handler来处理消息,都必须要先创建Looper,在创建Looper的过程中同时也就创建了MessageQueue,否则无法处理消息。之所以会存在两种创建方式,就是因为主线程已经在开始的时候帮我们都准备好了Looper,不用我们手动调用Looper.prepare()和Looper.loop()了。

    那系统是怎么做的呢?这个内容很多博文已经讲了,我会放上我参考的两个讲的比较好的文章在下面,大家感兴趣可以看看。

    完。

    更多相关内容
  • 多进程和多线程是系统执行多任务机制的重要手段,多任务同时进行自然少不了相互之间的通信工作。下面先将线程间通信方式总结一下,便于大家对比学习。
  • 你的程序中有多个线程,你需要在这些线程之间安全地交换信息或数据 解决方案 从一个线程向另一个线程发送数据最安全的方式可能就是使用 queue 库中的队列了。创建一个被多个线程共享的 Queue 对象,这些线程通过使用...
  • 线程间通信

    千次阅读 2021-05-23 05:40:41
    线程间通信前面一章讲了线程间同步,提到了信号量、互斥量、事件集等概念;本章接着上一章的内容,讲解线程间通信。在裸机编程中,经常会使用全局变量进行功能间的通信,如某些功能可能由于一些操作而改变全局变量的...

    线程间通信

    前面一章讲了线程间同步,提到了信号量、互斥量、事件集等概念;本章接着上一章的内容,讲解线程间通信。在裸机编程中,经常会使用全局变量进行功能间的通信,如某些功能可能由于一些操作而改变全局变量的值,另一个功能对此全局变量进行读取,根据读取到的全局变量值执行相应的动作,达到通信协作的目的。RT-Thread 中则提供了更多的工具帮助在不同的线程中间传递信息,本章会详细介绍这些工具。学习完本章,大家将学会如何将邮箱、消息队列、信号用于线程间的通信。

    邮箱

    邮箱服务是实时操作系统中一种典型的线程间通信方法。举一个简单的例子,有两个线程,线程 1 检测按键状态并发送,线程 2 读取按键状态并根据按键的状态相应地改变 LED 的亮灭。这里就可以使用邮箱的方式进行通信,线程 1 将按键的状态作为邮件发送到邮箱,线程 2 在邮箱中读取邮件获得按键状态并对 LED 执行亮灭操作。

    这里的线程 1 也可以扩展为多个线程。例如,共有三个线程,线程 1 检测并发送按键状态,线程 2 检测并发送 ADC 采样信息,线程 3 则根据接收的信息类型不同,执行不同的操作。

    邮箱的工作机制

    RT-Thread 操作系统的邮箱用于线程间通信,特点是开销比较低,效率较高。邮箱中的每一封邮件只能容纳固定的 4 字节内容(针对 32 位处理系统,指针的大小即为 4 个字节,所以一封邮件恰好能够容纳一个指针)。典型的邮箱也称作交换消息,如下图所示,线程或中断服务例程把一封 4 字节长度的邮件发送到邮箱中,而一个或多个线程可以从邮箱中接收这些邮件并进行处理。

    57c3db4bea56c5c8aa0cf9c92f9dd32c.png

    非阻塞方式的邮件发送过程能够安全的应用于中断服务中,是线程、中断服务、定时器向线程发送消息的有效手段。通常来说,邮件收取过程可能是阻塞的,这取决于邮箱中是否有邮件,以及收取邮件时设置的超时时间。当邮箱中不存在邮件且超时时间不为 0 时,邮件收取过程将变成阻塞方式。在这类情况下,只能由线程进行邮件的收取。

    当一个线程向邮箱发送邮件时,如果邮箱没满,将把邮件复制到邮箱中。如果邮箱已经满了,发送线程可以设置超时时间,选择等待挂起或直接返回 - RT_EFULL。如果发送线程选择挂起等待,那么当邮箱中的邮件被收取而空出空间来时,等待挂起的发送线程将被唤醒继续发送。

    当一个线程从邮箱中接收邮件时,如果邮箱是空的,接收线程可以选择是否等待挂起直到收到新的邮件而唤醒,或可以设置超时时间。当达到设置的超时时间,邮箱依然未收到邮件时,这个选择超时等待的线程将被唤醒并返回 - RT_ETIMEOUT。如果邮箱中存在邮件,那么接收线程将复制邮箱中的 4 个字节邮件到接收缓存中。

    邮箱控制块

    在 RT-Thread 中,邮箱控制块是操作系统用于管理邮箱的一个数据结构,由结构体 struct rt_mailbox 表示。另外一种 C 表达方式 rt_mailbox_t,表示的是邮箱的句柄,在 C 语言中的实现是邮箱控制块的指针。邮箱控制块结构的详细定义请见以下代码:

    struct rt_mailbox

    {

    struct rt_ipc_object parent;

    rt_uint32_t* msg_pool; /* 邮箱缓冲区的开始地址 */

    rt_uint16_t size; /* 邮箱缓冲区的大小 */

    rt_uint16_t entry; /* 邮箱中邮件的数目 */

    rt_uint16_t in_offset, out_offset; /* 邮箱缓冲的进出指针 */

    rt_list_t suspend_sender_thread; /* 发送线程的挂起等待队列 */

    };

    typedef struct rt_mailbox* rt_mailbox_t;

    rt_mailbox 对象从 rt_ipc_object 中派生,由 IPC 容器所管理。

    邮箱的管理方式

    邮箱控制块是一个结构体,其中含有事件相关的重要参数,在邮箱的功能实现中起重要的作用。邮箱的相关接口如下图所示,对一个邮箱的操作包含:创建 / 初始化邮箱、发送邮件、接收邮件、删除 / 脱离邮箱。

    656aedde41353004c6f2cbfcf1317a98.png

    创建和删除邮箱

    动态创建一个邮箱对象可以调用如下的函数接口:

    rt_mailbox_t rt_mb_create (const char* name, rt_size_t size, rt_uint8_t flag);

    创建邮箱对象时会先从对象管理器中分配一个邮箱对象,然后给邮箱动态分配一块内存空间用来存放邮件,这块内存的大小等于邮件大小(4 字节)与邮箱容量的乘积,接着初始化接收邮件数目和发送邮件在邮箱中的偏移量。下表描述了该函数的输入参数与返回值:

    rt_mb_create() 的输入参数和返回值

    参数

    描述

    name

    邮箱名称

    size

    邮箱容量

    flag

    邮箱标志,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO

    返回

    ——

    RT_NULL

    创建失败

    邮箱对象的句柄

    创建成功

    当用 rt_mb_create() 创建的邮箱不再被使用时,应该删除它来释放相应的系统资源,一旦操作完成,邮箱将被永久性的删除。删除邮箱的函数接口如下:

    rt_err_t rt_mb_delete (rt_mailbox_t mb);

    删除邮箱时,如果有线程被挂起在该邮箱对象上,内核先唤醒挂起在该邮箱上的所有线程(线程返回值是 -

    RT_ERROR),然后再释放邮箱使用的内存,最后删除邮箱对象。下表描述了该函数的输入参数与返回值:

    rt_mb_delete() 的输入参数和返回值

    参数

    描述

    mb

    邮箱对象的句柄

    返回

    ——

    RT_EOK

    成功

    初始化和脱离邮箱

    初始化邮箱跟创建邮箱类似,只是初始化邮箱用于静态邮箱对象的初始化。与创建邮箱不同的是,静态邮箱对象的内存是在系统编译时由编译器分配的,一般放于读写数据段或未初始化数据段中,其余的初始化工作与创建邮箱时相同。函数接口如下:

    rt_err_t rt_mb_init(rt_mailbox_t mb,

    const char* name,

    void* msgpool,

    rt_size_t size,

    rt_uint8_t flag)

    初始化邮箱时,该函数接口需要获得用户已经申请获得的邮箱对象控制块,缓冲区的指针,以及邮箱名称和邮箱容量(能够存储的邮件数)。下表描述了该函数的输入参数与返回值:

    rt_mb_init() 的输入参数和返回值

    参数

    描述

    mb

    邮箱对象的句柄

    name

    邮箱名称

    msgpool

    缓冲区指针

    size

    邮箱容量

    flag

    邮箱标志,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO

    返回

    ——

    RT_EOK

    成功

    这里的 size 参数指定的是邮箱的容量,即如果 msgpool 指向的缓冲区的字节数是 N,那么邮箱容量应该是 N/4。

    脱离邮箱将把静态初始化的邮箱对象从内核对象管理器中脱离。脱离邮箱使用下面的接口:

    rt_err_t rt_mb_detach(rt_mailbox_t mb);

    使用该函数接口后,内核先唤醒所有挂在该邮箱上的线程(线程获得返回值是 - RT_ERROR),然后将该邮箱对象从内核对象管理器中脱离。下表描述了该函数的输入参数与返回值:

    rt_mb_detach() 的输入参数和返回值

    参数

    描述

    mb

    邮箱对象的句柄

    返回

    ——

    RT_EOK

    成功

    发送邮件

    线程或者中断服务程序可以通过邮箱给其他线程发送邮件,发送邮件函数接口如下:

    rt_err_t rt_mb_send (rt_mailbox_t mb, rt_uint32_t value);

    发送的邮件可以是 32 位任意格式的数据,一个整型值或者一个指向缓冲区的指针。当邮箱中的邮件已经满时,发送邮件的线程或者中断程序会收到 -RT_EFULL 的返回值。下表描述了该函数的输入参数与返回值:

    rt_mb_send() 的输入参数和返回值

    参数

    描述

    mb

    邮箱对象的句柄

    value

    邮件内容

    返回

    ——

    RT_EOK

    发送成功

    -RT_EFULL

    邮箱已经满了

    等待方式发送邮件

    用户也可以通过如下的函数接口向指定邮箱发送邮件:

    rt_err_t rt_mb_send_wait (rt_mailbox_t mb,

    rt_uint32_t value,

    rt_int32_t timeout);

    rt_mb_send_wait() 与 rt_mb_send() 的区别在于有等待时间,如果邮箱已经满了,那么发送线程将根据设定的 timeout 参数等待邮箱中因为收取邮件而空出空间。如果设置的超时时间到达依然没有空出空间,这时发送线程将被唤醒并返回错误码。下表描述了该函数的输入参数与返回值:

    rt_mb_send_wait() 的输入参数和返回值

    参数

    描述

    mb

    邮箱对象的句柄

    value

    邮件内容

    timeout

    超时时间

    返回

    ——

    RT_EOK

    发送成功

    -RT_ETIMEOUT

    超时

    -RT_ERROR

    失败,返回错误

    接收邮件

    只有当接收者接收的邮箱中有邮件时,接收者才能立即取到邮件并返回 RT_EOK 的返回值,否则接收线程会根据超时时间设置,或挂起在邮箱的等待线程队列上,或直接返回。接收邮件函数接口如下:

    rt_err_t rt_mb_recv (rt_mailbox_t mb, rt_uint32_t* value, rt_int32_t timeout);

    接收邮件时,接收者需指定接收邮件的邮箱句柄,并指定接收到的邮件存放位置以及最多能够等待的超时时间。如果接收时设定了超时,当指定的时间内依然未收到邮件时,将返回 - RT_ETIMEOUT。下表描述了该函数的输入参数与返回值:

    rt_mb_recv() 的输入参数和返回值

    参数

    描述

    mb

    邮箱对象的句柄

    value

    邮件内容

    timeout

    超时时间

    返回

    ——

    RT_EOK

    发送成功

    -RT_ETIMEOUT

    超时

    -RT_ERROR

    失败,返回错误

    邮箱使用示例

    这是一个邮箱的应用例程,初始化 2 个静态线程,一个静态的邮箱对象,其中一个线程往邮箱中发送邮件,一个线程往邮箱中收取邮件。如下代码所示:

    邮箱的使用例程

    #include

    #define THREAD_PRIORITY 10

    #define THREAD_TIMESLICE 5

    /* 邮箱控制块 */

    static struct rt_mailbox mb;

    /* 用于放邮件的内存池 */

    static char mb_pool[128];

    static char mb_str1[] = "I'm a mail!";

    static char mb_str2[] = "this is another mail!";

    static char mb_str3[] = "over";

    ALIGN(RT_ALIGN_SIZE)

    static char thread1_stack[1024];

    static struct rt_thread thread1;

    /* 线程 1 入口 */

    static void thread1_entry(void *parameter)

    {

    char *str;

    while (1)

    {

    rt_kprintf("thread1: try to recv a mail\n");

    /* 从邮箱中收取邮件 */

    if (rt_mb_recv(&mb, (rt_uint32_t *)&str, RT_WAITING_FOREVER) == RT_EOK)

    {

    rt_kprintf("thread1: get a mail from mailbox, the content:%s\n", str);

    if (str == mb_str3)

    break;

    /* 延时 100ms */

    rt_thread_mdelay(100);

    }

    }

    /* 执行邮箱对象脱离 */

    rt_mb_detach(&mb);

    }

    ALIGN(RT_ALIGN_SIZE)

    static char thread2_stack[1024];

    static struct rt_thread thread2;

    /* 线程 2 入口 */

    static void thread2_entry(void *parameter)

    {

    rt_uint8_t count;

    count = 0;

    while (count < 10)

    {

    count ++;

    if (count & 0x1)

    {

    /* 发送 mb_str1 地址到邮箱中 */

    rt_mb_send(&mb, (rt_uint32_t)&mb_str1);

    }

    else

    {

    /* 发送 mb_str2 地址到邮箱中 */

    rt_mb_send(&mb, (rt_uint32_t)&mb_str2);

    }

    /* 延时 200ms */

    rt_thread_mdelay(200);

    }

    /* 发送邮件告诉线程 1,线程 2 已经运行结束 */

    rt_mb_send(&mb, (rt_uint32_t)&mb_str3);

    }

    int mailbox_sample(void)

    {

    rt_err_t result;

    /* 初始化一个 mailbox */

    result = rt_mb_init(&mb,

    "mbt", /* 名称是 mbt */

    &mb_pool[0], /* 邮箱用到的内存池是 mb_pool */

    sizeof(mb_pool) / 4, /* 邮箱中的邮件数目,因为一封邮件占 4 字节 */

    RT_IPC_FLAG_FIFO); /* 采用 FIFO 方式进行线程等待 */

    if (result != RT_EOK)

    {

    rt_kprintf("init mailbox failed.\n");

    return -1;

    }

    rt_thread_init(&thread1,

    "thread1",

    thread1_entry,

    RT_NULL,

    &thread1_stack[0],

    sizeof(thread1_stack),

    THREAD_PRIORITY, THREAD_TIMESLICE);

    rt_thread_startup(&thread1);

    rt_thread_init(&thread2,

    "thread2",

    thread2_entry,

    RT_NULL,

    &thread2_stack[0],

    sizeof(thread2_stack),

    THREAD_PRIORITY, THREAD_TIMESLICE);

    rt_thread_startup(&thread2);

    return 0;

    }

    /* 导出到 msh 命令列表中 */

    MSH_CMD_EXPORT(mailbox_sample, mailbox sample);

    仿真运行结果如下:

    \ | /

    - RT - Thread Operating System

    / | \ 3.1.0 build Aug 27 2018

    2006 - 2018 Copyright by rt-thread team

    msh >mailbox_sample

    thread1: try to recv a mail

    thread1: get a mail from mailbox, the content:I'm a mail!

    msh >thread1: try to recv a mail

    thread1: get a mail from mailbox, the content:this is another mail!

    thread1: try to recv a mail

    thread1: get a mail from mailbox, the content:this is another mail!

    thread1: try to recv a mail

    thread1: get a mail from mailbox, the content:over

    例程演示了邮箱的使用方法。线程 2 发送邮件,共发送 11 次;线程 1 接收邮件,共接收到 11 封邮件,将邮件内容打印出来,并判断结束。

    邮箱的使用场合

    邮箱是一种简单的线程间消息传递方式,特点是开销比较低,效率较高。在 RT-Thread 操作系统的实现中能够一次传递一个 4 字节大小的邮件,并且邮箱具备一定的存储功能,能够缓存一定数量的邮件数 (邮件数由创建、初始化邮箱时指定的容量决定)。邮箱中一封邮件的最大长度是 4 字节,所以邮箱能够用于不超过 4 字节的消息传递。由于在 32 系统上 4 字节的内容恰好可以放置一个指针,因此当需要在线程间传递比较大的消息时,可以把指向一个缓冲区的指针作为邮件发送到邮箱中,即邮箱也可以传递指针,例如:

    struct msg

    {

    rt_uint8_t *data_ptr;

    rt_uint32_t data_size;

    };

    对于这样一个消息结构体,其中包含了指向数据的指针 data_ptr 和数据块长度的变量 data_size。当一个线程需要把这个消息发送给另外一个线程时,可以采用如下的操作:

    struct msg* msg_ptr;

    msg_ptr = (struct msg*)rt_malloc(sizeof(struct msg));

    msg_ptr->data_ptr = ...; /* 指向相应的数据块地址 */

    msg_ptr->data_size = len; /* 数据块的长度 */

    /* 发送这个消息指针给 mb 邮箱 */

    rt_mb_send(mb, (rt_uint32_t)msg_ptr);

    而在接收线程中,因为收取过来的是指针,而 msg_ptr 是一个新分配出来的内存块,所以在接收线程处理完毕后,需要释放相应的内存块:

    struct msg* msg_ptr;

    if (rt_mb_recv(mb, (rt_uint32_t*)&msg_ptr) == RT_EOK)

    {

    /* 在接收线程处理完毕后,需要释放相应的内存块 */

    rt_free(msg_ptr);

    }

    消息队列

    消息队列是另一种常用的线程间通讯方式,是邮箱的扩展。可以应用在多种场合:线程间的消息交换、使用串口接收不定长数据等。

    消息队列的工作机制

    消息队列能够接收来自线程或中断服务例程中不固定长度的消息,并把消息缓存在自己的内存空间中。其他线程也能够从消息队列中读取相应的消息,而当消息队列是空的时候,可以挂起读取线程。当有新的消息到达时,挂起的线程将被唤醒以接收并处理消息。消息队列是一种异步的通信方式。

    如下图所示,线程或中断服务例程可以将一条或多条消息放入消息队列中。同样,一个或多个线程也可以从消息队列中获得消息。当有多个消息发送到消息队列时,通常将先进入消息队列的消息先传给线程,也就是说,线程先得到的是最先进入消息队列的消息,即先进先出原则 (FIFO)。

    c19f9578e9c2b9f9f17eea0a8cd748a9.png

    RT-Thread 操作系统的消息队列对象由多个元素组成,当消息队列被创建时,它就被分配了消息队列控制块:消息队列名称、内存缓冲区、消息大小以及队列长度等。同时每个消息队列对象中包含着多个消息框,每个消息框可以存放一条消息;消息队列中的第一个和最后一个消息框被分别称为消息链表头和消息链表尾,对应于消息队列控制块中的 msg_queue_head 和 msg_queue_tail;有些消息框可能是空的,它们通过 msg_queue_free 形成一个空闲消息框链表。所有消息队列中的消息框总数即是消息队列的长度,这个长度可在消息队列创建时指定。

    消息队列控制块

    在 RT-Thread 中,消息队列控制块是操作系统用于管理消息队列的一个数据结构,由结构体 struct rt_messagequeue 表示。另外一种 C 表达方式 rt_mq_t,表示的是消息队列的句柄,在 C 语言中的实现是消息队列控制块的指针。消息队列控制块结构的详细定义请见以下代码:

    struct rt_messagequeue

    {

    struct rt_ipc_object parent;

    void* msg_pool; /* 指向存放消息的缓冲区的指针 */

    rt_uint16_t msg_size; /* 每个消息的长度 */

    rt_uint16_t max_msgs; /* 最大能够容纳的消息数 */

    rt_uint16_t entry; /* 队列中已有的消息数 */

    void* msg_queue_head; /* 消息链表头 */

    void* msg_queue_tail; /* 消息链表尾 */

    void* msg_queue_free; /* 空闲消息链表 */

    rt_list_t suspend_sender_thread; /* 发送线程的挂起等待队列 */

    };

    typedef struct rt_messagequeue* rt_mq_t;

    rt_messagequeue 对象从 rt_ipc_object 中派生,由 IPC 容器所管理。

    消息队列的管理方式

    消息队列控制块是一个结构体,其中含有消息队列相关的重要参数,在消息队列的功能实现中起重要的作用。消息队列的相关接口如下图所示,对一个消息队列的操作包含:创建消息队列 - 发送消息 - 接收消息 - 删除消息队列。

    1fd3c41c36de5f3cd0307c38ff9980fe.png

    创建和删除消息队列

    消息队列在使用前,应该被创建出来,或对已有的静态消息队列对象进行初始化,创建消息队列的函数接口如下所示:

    rt_mq_t rt_mq_create(const char* name, rt_size_t msg_size,

    rt_size_t max_msgs, rt_uint8_t flag);

    创建消息队列时先从对象管理器中分配一个消息队列对象,然后给消息队列对象分配一块内存空间,组织成空闲消息链表,这块内存的大小 =[消息大小 + 消息头(用于链表连接)的大小]X 消息队列最大个数,接着再初始化消息队列,此时消息队列为空。下表描述了该函数的输入参数与返回值:

    rt_mq_create() 的输入参数和返回值

    参数

    描述

    name

    消息队列的名称

    msg_size

    消息队列中一条消息的最大长度,单位字节

    max_msgs

    消息队列的最大个数

    flag

    消息队列采用的等待方式,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO

    返回

    ——

    RT_EOK

    发送成功

    消息队列对象的句柄

    成功

    RT_NULL

    失败

    当消息队列不再被使用时,应该删除它以释放系统资源,一旦操作完成,消息队列将被永久性地删除。删除消息队列的函数接口如下:

    rt_err_t rt_mq_delete(rt_mq_t mq);

    删除消息队列时,如果有线程被挂起在该消息队列等待队列上,则内核先唤醒挂起在该消息等待队列上的所有线程(线程返回值是 - RT_ERROR),然后再释放消息队列使用的内存,最后删除消息队列对象。下表描述了该函数的输入参数与返回值:

    rt_mq_delete() 的输入参数和返回值

    参数

    描述

    mq

    消息队列对象的句柄

    返回

    ——

    RT_EOK

    成功

    初始化和脱离消息队列

    初始化静态消息队列对象跟创建消息队列对象类似,只是静态消息队列对象的内存是在系统编译时由编译器分配的,一般放于读数据段或未初始化数据段中。在使用这类静态消息队列对象前,需要进行初始化。初始化消息队列对象的函数接口如下:

    rt_err_t rt_mq_init(rt_mq_t mq, const char* name,

    void *msgpool, rt_size_t msg_size,

    rt_size_t pool_size, rt_uint8_t flag);

    初始化消息队列时,该接口需要用户已经申请获得的消息队列对象的句柄(即指向消息队列对象控制块的指针)、消息队列名、消息缓冲区指针、消息大小以及消息队列缓冲区大小。如下图所示,消息队列初始化后所有消息都挂在空闲消息链表上,消息队列为空。下表描述了该函数的输入参数与返回值:

    rt_mq_init() 的输入参数和返回值

    参数

    描述

    mq

    消息队列对象的句柄

    name

    消息队列的名称

    msgpool

    指向存放消息的缓冲区的指针

    msg_size

    消息队列中一条消息的最大长度,单位字节

    pool_size

    存放消息的缓冲区大小

    flag

    消息队列采用的等待方式,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO

    返回

    ——

    RT_EOK

    成功

    脱离消息队列将使消息队列对象被从内核对象管理器中脱离。脱离消息队列使用下面的接口:

    rt_err_t rt_mq_detach(rt_mq_t mq);

    使用该函数接口后,内核先唤醒所有挂在该消息等待队列对象上的线程(线程返回值是 -RT_ERROR),然后将该消息队列对象从内核对象管理器中脱离。下表描述了该函数的输入参数与返回值:

    rt_mq_detach() 的输入参数和返回值

    参数

    描述

    mq

    消息队列对象的句柄

    返回

    ——

    RT_EOK

    成功

    发送消息

    线程或者中断服务程序都可以给消息队列发送消息。当发送消息时,消息队列对象先从空闲消息链表上取下一个空闲消息块,把线程或者中断服务程序发送的消息内容复制到消息块上,然后把该消息块挂到消息队列的尾部。当且仅当空闲消息链表上有可用的空闲消息块时,发送者才能成功发送消息;当空闲消息链表上无可用消息块,说明消息队列已满,此时,发送消息的的线程或者中断程序会收到一个错误码(-RT_EFULL)。发送消息的函数接口如下:

    rt_err_t rt_mq_send (rt_mq_t mq, void* buffer, rt_size_t size);

    发送消息时,发送者需指定发送的消息队列的对象句柄(即指向消息队列控制块的指针),并且指定发送的消息内容以及消息大小。如下图所示,在发送一个普通消息之后,空闲消息链表上的队首消息被转移到了消息队列尾。下表描述了该函数的输入参数与返回值:

    rt_mq_send() 的输入参数和返回值

    参数

    描述

    mq

    消息队列对象的句柄

    buffer

    消息内容

    size

    消息大小

    返回

    ——

    RT_EOK

    成功

    -RT_EFULL

    消息队列已满

    -RT_ERROR

    失败,表示发送的消息长度大于消息队列中消息的最大长度

    等待方式发送消息

    用户也可以通过如下的函数接口向指定的消息队列中发送消息:

    rt_err_t rt_mq_send_wait(rt_mq_t mq,

    const void *buffer,

    rt_size_t size,

    rt_int32_t timeout);

    rt_mq_send_wait() 与 rt_mq_send() 的区别在于有等待时间,如果消息队列已经满了,那么发送线程将根据设定的 timeout 参数进行等待。如果设置的超时时间到达依然没有空出空间,这时发送线程将被唤醒并返回错误码。下表描述了该函数的输入参数与返回值:

    rt_mq_send_wait() 的输入参数和返回值

    参数

    描述

    mq

    消息队列对象的句柄

    buffer

    消息内容

    size

    消息大小

    timeout

    超时时间

    返回

    ——

    RT_EOK

    成功

    -RT_EFULL

    消息队列已满

    -RT_ERROR

    失败,表示发送的消息长度大于消息队列中消息的最大长度

    发送紧急消息

    发送紧急消息的过程与发送消息几乎一样,唯一的不同是,当发送紧急消息时,从空闲消息链表上取下来的消息块不是挂到消息队列的队尾,而是挂到队首,这样,接收者就能够优先接收到紧急消息,从而及时进行消息处理。发送紧急消息的函数接口如下:

    rt_err_t rt_mq_urgent(rt_mq_t mq, void* buffer, rt_size_t size);

    下表描述了该函数的输入参数与返回值:

    rt_mq_urgent() 的输入参数和返回值

    参数

    描述

    mq

    消息队列对象的句柄

    buffer

    消息内容

    size

    消息大小

    返回

    ——

    RT_EOK

    成功

    -RT_EFULL

    消息队列已满

    -RT_ERROR

    失败

    接收消息

    当消息队列中有消息时,接收者才能接收消息,否则接收者会根据超时时间设置,或挂起在消息队列的等待线程队列上,或直接返回。接收消息函数接口如下:

    rt_err_t rt_mq_recv (rt_mq_t mq, void* buffer,

    rt_size_t size, rt_int32_t timeout);

    接收消息时,接收者需指定存储消息的消息队列对象句柄,并且指定一个内存缓冲区,接收到的消息内容将被复制到该缓冲区里。此外,还需指定未能及时取到消息时的超时时间。如下图所示,接收一个消息后消息队列上的队首消息被转移到了空闲消息链表的尾部。下表描述了该函数的输入参数与返回值:

    rt_mq_recv() 的输入参数和返回值

    参数

    描述

    mq

    消息队列对象的句柄

    buffer

    消息内容

    size

    消息大小

    timeout

    指定的超时时间

    返回

    ——

    RT_EOK

    成功收到

    -RT_ETIMEOUT

    超时

    -RT_ERROR

    失败,返回错误

    消息队列应用示例

    这是一个消息队列的应用例程,例程中初始化了 2 个静态线程,一个线程会从消息队列中收取消息;另一个线程会定时给消息队列发送普通消息和紧急消息,如下代码所示:

    消息队列的使用例程

    #include

    /* 消息队列控制块 */

    static struct rt_messagequeue mq;

    /* 消息队列中用到的放置消息的内存池 */

    static rt_uint8_t msg_pool[2048];

    ALIGN(RT_ALIGN_SIZE)

    static char thread1_stack[1024];

    static struct rt_thread thread1;

    /* 线程 1 入口函数 */

    static void thread1_entry(void *parameter)

    {

    char buf = 0;

    rt_uint8_t cnt = 0;

    while (1)

    {

    /* 从消息队列中接收消息 */

    if (rt_mq_recv(&mq, &buf, sizeof(buf), RT_WAITING_FOREVER) == RT_EOK)

    {

    rt_kprintf("thread1: recv msg from msg queue, the content:%c\n", buf);

    if (cnt == 19)

    {

    break;

    }

    }

    /* 延时 50ms */

    cnt++;

    rt_thread_mdelay(50);

    }

    rt_kprintf("thread1: detach mq \n");

    rt_mq_detach(&mq);

    }

    ALIGN(RT_ALIGN_SIZE)

    static char thread2_stack[1024];

    static struct rt_thread thread2;

    /* 线程 2 入口 */

    static void thread2_entry(void *parameter)

    {

    int result;

    char buf = 'A';

    rt_uint8_t cnt = 0;

    while (1)

    {

    if (cnt == 8)

    {

    /* 发送紧急消息到消息队列中 */

    result = rt_mq_urgent(&mq, &buf, 1);

    if (result != RT_EOK)

    {

    rt_kprintf("rt_mq_urgent ERR\n");

    }

    else

    {

    rt_kprintf("thread2: send urgent message - %c\n", buf);

    }

    }

    else if (cnt>= 20)/* 发送 20 次消息之后退出 */

    {

    rt_kprintf("message queue stop send, thread2 quit\n");

    break;

    }

    else

    {

    /* 发送消息到消息队列中 */

    result = rt_mq_send(&mq, &buf, 1);

    if (result != RT_EOK)

    {

    rt_kprintf("rt_mq_send ERR\n");

    }

    rt_kprintf("thread2: send message - %c\n", buf);

    }

    buf++;

    cnt++;

    /* 延时 5ms */

    rt_thread_mdelay(5);

    }

    }

    /* 消息队列示例的初始化 */

    int msgq_sample(void)

    {

    rt_err_t result;

    /* 初始化消息队列 */

    result = rt_mq_init(&mq,

    "mqt",

    &msg_pool[0], /* 内存池指向 msg_pool */

    1, /* 每个消息的大小是 1 字节 */

    sizeof(msg_pool), /* 内存池的大小是 msg_pool 的大小 */

    RT_IPC_FLAG_FIFO); /* 如果有多个线程等待,按照先来先得到的方法分配消息 */

    if (result != RT_EOK)

    {

    rt_kprintf("init message queue failed.\n");

    return -1;

    }

    rt_thread_init(&thread1,

    "thread1",

    thread1_entry,

    RT_NULL,

    &thread1_stack[0],

    sizeof(thread1_stack), 25, 5);

    rt_thread_startup(&thread1);

    rt_thread_init(&thread2,

    "thread2",

    thread2_entry,

    RT_NULL,

    &thread2_stack[0],

    sizeof(thread2_stack), 25, 5);

    rt_thread_startup(&thread2);

    return 0;

    }

    /* 导出到 msh 命令列表中 */

    MSH_CMD_EXPORT(msgq_sample, msgq sample);

    仿真运行结果如下:

    \ | /

    - RT - Thread Operating System

    / | \ 3.1.0 build Aug 24 2018

    2006 - 2018 Copyright by rt-thread team

    msh > msgq_sample

    msh >thread2: send message - A

    thread1: recv msg from msg queue, the content:A

    thread2: send message - B

    thread2: send message - C

    thread2: send message - D

    thread2: send message - E

    thread1: recv msg from msg queue, the content:B

    thread2: send message - F

    thread2: send message - G

    thread2: send message - H

    thread2: send urgent message - I

    thread2: send message - J

    thread1: recv msg from msg queue, the content:I

    thread2: send message - K

    thread2: send message - L

    thread2: send message - M

    thread2: send message - N

    thread2: send message - O

    thread1: recv msg from msg queue, the content:C

    thread2: send message - P

    thread2: send message - Q

    thread2: send message - R

    thread2: send message - S

    thread2: send message - T

    thread1: recv msg from msg queue, the content:D

    message queue stop send, thread2 quit

    thread1: recv msg from msg queue, the content:E

    thread1: recv msg from msg queue, the content:F

    thread1: recv msg from msg queue, the content:G

    thread1: recv msg from msg queue, the content:T

    thread1: detach mq

    例程演示了消息队列的使用方法。线程 1 会从消息队列中收取消息;线程 2 定时给消息队列发送普通消息和紧急消息。由于线程 2 发送消息 “I” 是紧急消息,会直接插入消息队列的队首,所以线程 1 在接收到消息 “B” 后,接收的是该紧急消息,之后才接收消息“C”。

    消息队列的使用场合

    消息队列可以应用于发送不定长消息的场合,包括线程与线程间的消息交换,以及中断服务例程中给线程发送消息(中断服务例程不能接收消息)。下面分发送消息和同步消息两部分来介绍消息队列的使用。

    发送消息

    消息队列和邮箱的明显不同是消息的长度并不限定在 4 个字节以内;另外,消息队列也包括了一个发送紧急消息的函数接口。但是当创建的是一个所有消息的最大长度是 4 字节的消息队列时,消息队列对象将蜕化成邮箱。这个不限定长度的消息,也及时的反应到了代码编写的场合上,同样是类似邮箱的代码:

    struct msg

    {

    rt_uint8_t *data_ptr; /* 数据块首地址 */

    rt_uint32_t data_size; /* 数据块大小 */

    };

    和邮箱例子相同的消息结构定义,假设依然需要发送这样一个消息给接收线程。在邮箱例子中,这个结构只能够发送指向这个结构的指针(在函数指针被发送过去后,接收线程能够正确的访问指向这个地址的内容,通常这块数据需要留给接收线程来释放)。而使用消息队列的方式则大不相同:

    void send_op(void *data, rt_size_t length)

    {

    struct msg msg_ptr;

    msg_ptr.data_ptr = data; /* 指向相应的数据块地址 */

    msg_ptr.data_size = length; /* 数据块的长度 */

    /* 发送这个消息指针给 mq 消息队列 */

    rt_mq_send(mq, (void*)&msg_ptr, sizeof(struct msg));

    }

    注意,上面的代码中,是把一个局部变量的数据内容发送到了消息队列中。在接收线程中,同样也采用局部变量进行消息接收的结构体:

    void message_handler()

    {

    struct msg msg_ptr; /* 用于放置消息的局部变量 */

    /* 从消息队列中接收消息到 msg_ptr 中 */

    if (rt_mq_recv(mq, (void*)&msg_ptr, sizeof(struct msg), RT_WAITING_FOREVER) == RT_EOK)

    {

    /* 成功接收到消息,进行相应的数据处理 */

    }

    }

    因为消息队列是直接的数据内容复制,所以在上面的例子中,都采用了局部变量的方式保存消息结构体,这样也就免去动态内存分配的烦恼了(也就不用担心,接收线程在接收到消息时,消息内存空间已经被释放)。

    同步消息

    在一般的系统设计中会经常遇到要发送同步消息的问题,这个时候就可以根据当时状态的不同选择相应的实现:两个线程间可以采用[消息队列 + 信号量或邮箱]的形式实现。发送线程通过消息发送的形式发送相应的消息给消息队列,发送完毕后希望获得接收线程的收到确认,工作示意图如下图所示:

    5965b9cac3593ef1fde87f45e48bcd43.png

    根据消息确认的不同,可以把消息结构体定义成:

    struct msg

    {

    /* 消息结构其他成员 */

    struct rt_mailbox ack;

    };

    /* 或者 */

    struct msg

    {

    /* 消息结构其他成员 */

    struct rt_semaphore ack;

    };

    第一种类型的消息使用了邮箱来作为确认标志,而第二种类型的消息采用了信号量来作为确认标志。邮箱作为确认标志,代表着接收线程能够通知一些状态值给发送线程;而信号量作为确认标志只能够单一的通知发送线程,消息已经确认接收。

    信号

    信号(又称为软中断信号),在软件层次上是对中断机制的一种模拟,在原理上,一个线程收到一个信号与处理器收到一个中断请求可以说是类似的。

    信号的工作机制

    信号在 RT-Thread 中用作异步通信,POSIX 标准定义了 sigset_t 类型来定义一个信号集,然而 sigset_t 类型在不同的系统可能有不同的定义方式,在 RT-Thread 中,将 sigset_t 定义成了 unsigned long 型,并命名为 rt_sigset_t,应用程序能够使用的信号为 SIGUSR1(10)和 SIGUSR2(12)。

    信号本质是软中断,用来通知线程发生了异步事件,用做线程之间的异常通知、应急处理。一个线程不必通过任何操作来等待信号的到达,事实上,线程也不知道信号到底什么时候到达,线程之间可以互相通过调用 rt_thread_kill() 发送软中断信号。

    收到信号的线程对各种信号有不同的处理方法,处理方法可以分为三类:

    第一种是类似中断的处理程序,对于需要处理的信号,线程可以指定处理函数,由该函数来处理。

    第二种方法是,忽略某个信号,对该信号不做任何处理,就像未发生过一样。

    第三种方法是,对该信号的处理保留系统的默认值。

    如下图所示,假设线程 1 需要对信号进行处理,首先线程 1 安装一个信号并解除阻塞,并在安装的同时设定了对信号的异常处理方式;然后其他线程可以给线程 1 发送信号,触发线程 1 对该信号的处理。

    bc518ce334d531717eafdbfe904c8381.png

    当信号被传递给线程 1 时,如果它正处于挂起状态,那会把状态改为就绪状态去处理对应的信号。如果它正处于运行状态,那么会在它当前的线程栈基础上建立新栈帧空间去处理对应的信号,需要注意的是使用的线程栈大小也会相应增加。

    信号的管理方式

    对于信号的操作,有以下几种:安装信号、阻塞信号、阻塞解除、信号发送、信号等待。信号的接口详见下图:

    c934c3815959545f0f2745978f7ea96a.png

    安装信号

    如果线程要处理某一信号,那么就要在线程中安装该信号。安装信号主要用来确定信号值及线程针对该信号值的动作之间的映射关系,即线程将要处理哪个信号,该信号被传递给线程时,将执行何种操作。详细定义请见以下代码:

    rt_sighandler_t rt_signal_install(int signo, rt_sighandler_t[] handler);

    其中 rt_sighandler_t 是定义信号处理函数的函数指针类型。下表描述了该函数的输入参数与返回值:

    rt_signal_install() 的输入参数和返回值

    参数

    描述

    signo

    信号值(只有 SIGUSR1 和 SIGUSR2 是开放给用户使用的,下同)

    handler

    设置对信号值的处理方式

    返回

    ——

    SIG_ERR

    错误的信号

    安装信号前的 handler 值

    成功

    在信号安装时设定 handler 参数,决定了该信号的不同的处理方法。处理方法可以分为三种:

    1)类似中断的处理方式,参数指向当信号发生时用户自定义的处理函数,由该函数来处理。

    2)参数设为 SIG_IGN,忽略某个信号,对该信号不做任何处理,就像未发生过一样。

    3)参数设为 SIG_DFL,系统会调用默认的处理函数_signal_default_handler()。

    阻塞信号

    信号阻塞,也可以理解为屏蔽信号。如果该信号被阻塞,则该信号将不会递达给安装此信号的线程,也不会引发软中断处理。调 rt_signal_mask() 可以使信号阻塞:

    void rt_signal_mask(int signo);

    下表描述了该函数的输入参数:

    rt_signal_mask() 函数参数

    参数

    描述

    signo

    信号值

    解除信号阻塞

    线程中可以安装好几个信号,使用此函数可以对其中一些信号给予 “关注”,那么发送这些信号都会引发该线程的软中断。调用 rt_signal_unmask() 可以用来解除信号阻塞:

    void rt_signal_unmask(int signo);

    下表描述了该函数的输入参数:

    rt_signal_unmask() 函数参数

    参数

    描述

    signo

    信号值

    发送信号

    当需要进行异常处理时,可以给设定了处理异常的线程发送信号,调用 rt_thread_kill() 可以用来向任何线程发送信号:

    int rt_thread_kill(rt_thread_t tid, int sig);

    下表描述了该函数的输入参数与返回值:

    rt_thread_kill() 的输入参数和返回值

    参数

    描述

    tid

    接收信号的线程

    sig

    信号值

    返回

    ——

    RT_EOK

    发送成功

    -RT_EINVAL

    参数错误

    等待信号

    等待 set 信号的到来,如果没有等到这个信号,则将线程挂起,直到等到这个信号或者等待时间超过指定的超时时间 timeout。如果等到了该信号,则将指向该信号体的指针存入 si,如下是等待信号的函数。

    int rt_signal_wait(const rt_sigset_t *set,

    rt_siginfo_t[] *si, rt_int32_t timeout);

    其中 rt_siginfo_t 是定义信号信息的数据类型,下表描述了该函数的输入参数与返回值:

    rt_signal_wait() 的输入参数和返回值

    参数

    描述

    set

    指定等待的信号

    si

    指向存储等到信号信息的指针

    timeout

    指定的等待时间

    返回

    ——

    RT_EOK

    等到信号

    -RT_ETIMEOUT

    超时

    -RT_EINVAL

    参数错误

    信号应用示例

    这是一个信号的应用例程,如下代码所示。此例程创建了 1 个线程,在安装信号时,信号处理方式设为自定义处理,定义的信号的处理函数为 thread1_signal_handler()。待此线程运行起来安装好信号之后,给此线程发送信号。此线程将接收到信号,并打印信息。

    信号使用例程

    #include

    #define THREAD_PRIORITY 25

    #define THREAD_STACK_SIZE 512

    #define THREAD_TIMESLICE 5

    static rt_thread_t tid1 = RT_NULL;

    /* 线程 1 的信号处理函数 */

    void thread1_signal_handler(int sig)

    {

    rt_kprintf("thread1 received signal %d\n", sig);

    }

    /* 线程 1 的入口函数 */

    static void thread1_entry(void *parameter)

    {

    int cnt = 0;

    /* 安装信号 */

    rt_signal_install(SIGUSR1, thread1_signal_handler);

    rt_signal_unmask(SIGUSR1);

    /* 运行 10 次 */

    while (cnt < 10)

    {

    /* 线程 1 采用低优先级运行,一直打印计数值 */

    rt_kprintf("thread1 count : %d\n", cnt);

    cnt++;

    rt_thread_mdelay(100);

    }

    }

    /* 信号示例的初始化 */

    int signal_sample(void)

    {

    /* 创建线程 1 */

    tid1 = rt_thread_create("thread1",

    thread1_entry, RT_NULL,

    THREAD_STACK_SIZE,

    THREAD_PRIORITY, THREAD_TIMESLICE);

    if (tid1 != RT_NULL)

    rt_thread_startup(tid1);

    rt_thread_mdelay(300);

    /* 发送信号 SIGUSR1 给线程 1 */

    rt_thread_kill(tid1, SIGUSR1);

    return 0;

    }

    /* 导出到 msh 命令列表中 */

    MSH_CMD_EXPORT(signal_sample, signal sample);

    仿真运行结果如下:

    \ | /

    - RT - Thread Operating System

    / | \ 3.1.0 build Aug 24 2018

    2006 - 2018 Copyright by rt-thread team

    msh >signal_sample

    thread1 count : 0

    thread1 count : 1

    thread1 count : 2

    msh >thread1 received signal 10

    thread1 count : 3

    thread1 count : 4

    thread1 count : 5

    thread1 count : 6

    thread1 count : 7

    thread1 count : 8

    thread1 count : 9

    例程中,首先线程安装信号并解除阻塞,然后发送信号给线程。线程接收到信号并打印出了接收到的信号:SIGUSR1(10)。

    展开全文
  • Java 线程间通信

    千次阅读 2022-04-16 15:16:02
    Java线程间通信

    ⭐写在前面⭐

    🎉 内容回顾
    Java 多线程介绍及线程创建
    Java 多线程七大状态
    Java 多线程方法详解
    Java synchronized关键字实现线程同步
    📢今天我们进行 JDBC 获取数据库连接的5种方式 的学习,感谢你的阅读,内容若有不当之处,希望大家多多指正,一起进步💯!!!
    ♨️如果觉得博主文章还不错,可以👍三连支持⭐一下哦😀

    ☘️Java 线程间通信

    🍀线程通信方法

    在Java的Object类中提供了waitnotifynotifyAll等方法,这些方法可以实现线程间的通信,因为Object类是所有类的基类,因此所有的对象都具有线程间通信的方法
    在这里插入图片描述

    void wait():调用一个对象的wait方法,会导致当前持有该对象的锁的线程等待,直到该对象的另一个持有锁的线程调用notify或者notifyAll唤醒。

    void wait(long timeout):除了和wait相似,还具有超过定时的超时时间,时间到后或自动唤醒。

    void wait(long timeout,int nanou):与 void wait(long timeout) 相同,不过提供了纳秒级别的更精确的超时控制。

    void notify():调用一个对象的notify方法,会导致当前持有该锁的所有线程中的随机某一个线程被唤醒

    void notifyAll():调用一个对象的notifyAll方法,会导致当前持有该锁的所有线程被唤醒

    🍀线程间通信案例

    通信是在不同线程间的通信,一个线程处于wait状态阻塞等待被唤醒,另一个线程通过notify或者notifyAll唤醒,当前的唤醒操作必须是作用与同一个对象,注意在进行唤醒和阻塞时必须要加锁的,加锁需要使用synchronized关键字。

    WaitDemo类

    public class WaitDemo extends Thread{
        private Object obj;
    
        public WaitDemo(Object obj) {
            this.obj = obj;
        }
    
        @Override
        public void run() {
            synchronized (obj) {
                System.out.println(Thread.currentThread().getName() + "WaitDemo执行开始~~~");
                try {
                    obj.wait(); //调用wait方法阻塞线程执行
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "WaitDemo执行结束~~~");
            }
        }
    }
    
    

    NotifyDemo类

    public class NotifyDemo extends Thread{
        private Object obj;
    
        public NotifyDemo(Object obj) {
            this.obj = obj;
        }
    
        @Override
        public void run() {
            synchronized (obj) {
                System.out.println(Thread.currentThread().getName() + "NotifyDemo执行开始~~~");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                obj.notify(); //调用notify方法唤醒阻塞线程
                System.out.println(Thread.currentThread().getName() + "NotifyDemo执行结束~~~");
            }
        }
    }
    

    TestWaitAndNotify测试类

    public class TestWaitAndNotify {
        public static void main(String[] args) {
            Object object = new Object();
            WaitDemo waitDemo = new WaitDemo(object);
            NotifyDemo notifyDemo = new NotifyDemo(object);
    
            waitDemo.setName("WaitDemo线程");
            notifyDemo.setName("NotifyDemo线程");
    
            waitDemo.start();
            notifyDemo.start();
    
        }
    }
    

    🍁执行结果:
    在这里插入图片描述

    🌵使用注意点

    🌱调用notifywait方法必须是作用于同一个对象,如果不是通一个对象则无法完成通信。
    🌱对于waitnitifynotifyAll的调用,必须在该对象的同步方法或者代码块中,锁作用的对象和wait等方法必须是作用于同一个对象。
    🌱wait方法在调用后进入阻塞之前会释放锁,而sleepjoin是不会释放锁的。
    🌱线程状态转换时,当wait被唤醒或者超时时,线程并不是直接进入就绪状态,而是先进入阻塞状态,抢锁成功后才能进入到可运行状态。

    🌵注意点详解

    注意点1 当锁的对象和调用waitnotify的对象不是同一个对象时,会抛出IllegalMonitorStateException异常。
    在这里插入图片描述

    注意点2 wait方法在调用进入阻塞之前会释放锁
    在这里插入图片描述
    基于以上分析,一旦wait线程先调用则线程因为锁无法继续执行而阻塞下来,实际上notify依然可以获取锁进行执行,这是因为wait方法在调用进入阻塞之前释放锁,则调用notify操作的线程就可以抢到Object对象的锁,进而调用notify。

    注意点3 锁池和等待池

    在这里插入图片描述
    锁池:假设线程A已经拥有了某个对象的锁,而其他的线程想要调用这个对象的某个synchronized方法,由于这些线程在进入对象的synchronized方法之前必须先获取该对象的锁的拥有权,但是该对象的锁目前被线程A拥有,所以这些线程就回去进入到该对象的锁池
    等待池:假设一个线程A调用某个对象的wait方法,线程A就会释放该对象的锁后,进入到该对象的等待池

    🌵小试牛刀

    🌾有三个线程,分别为ABC线程,需要线程交替打印:ABCABC…打印10遍
    🌾 分析:需要使用线程间的通信,A给B通信,A进行notifyB进行wait;B给C通信,B进行notifyC进行Wait;同理C给A通信,C进行notifyA进行wait。

    🌾 思路分析:给每个线程给定编号,表明是第几个进程,再给定一个共享对象,共享对象进行notifywait等操作,共享对象本身需要携带信息表明下一个执行的线程编号,如果当前线程的编号与共享对象中的信息比较,如果相等就执行,否则就阻塞。

    🪴代码示例

    NextOpt类:共享对象,携带下一个要执行的线程编号信息

    //共享对象,携带下一个要执行的线程编号信息
    public class NextOpt {
        //下一个执行线程编号
        private Integer nextValue;
    
        public Integer getNextValue() {
            return nextValue;
        }
    
        public void setNextValue(Integer nextValue) {
            this.nextValue = nextValue;
        }
    }
    

    ThreadABC类

    public class ThreadABC extends Thread {
        //线程间通信对象
        private NextOpt opt;
        //打印名称通过数组获取
        private String[] abc = {"A", "B", "C"};
        //线程编号
        private int index;
        //执行次数
        int count = 0;
    
        public ThreadABC(NextOpt opt, int index) {
            this.opt = opt;
            this.index = index;
        }
    
        @Override
        public void run() {
            while (true) { //如果当前线程编号与下一个要执行的序号不一致就阻塞
                synchronized (opt) {
                    while (opt.getNextValue() != index) {
                        try {
                            opt.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
    
                    //打印
                    System.out.print(abc[index]);
                    //设置下一个要执行的线程的编号
                    opt.setNextValue((index + 1) % 3);
                    //唤醒所有的阻塞线程
                    opt.notifyAll();
    
                    if (count++ > 9) {
                        break;
                    }
                }
            }
        }
    }
    
    

    TestThreadABC测试类

    public class TestThreadABC {
        public static void main(String[] args) {
            NextOpt opt = new NextOpt();
    
            opt.setNextValue(0); //设置第一个要执行的线程编号
            ThreadABC thread1 = new ThreadABC(opt,0);
            ThreadABC thread2 = new ThreadABC(opt,1);
            ThreadABC thread3 = new ThreadABC(opt,2);
    
            thread1.start();
            thread2.start();
            thread3.start();
        }
    }
    

    运行结果:
    在这里插入图片描述

    🍀生产者消费者模型

    线程间通信典型案例:生产者消费者模型

    生产者-消费者(producer-consumer)问题,也称作有界缓冲区(bounded-buffer)问题,两个进程共享一个公共的固定大小的缓冲区。
    其中一个是生产者,用于将消息放入缓冲区;另外一个是消费者,用于从缓冲区中取出消息。
    问题出现在当缓冲区已经满了,而此时生产者还想向其中放入一个新的数据项的情形,其解决方法是让生产者此时进行休眠,等待消费者从缓冲区中取走了一个或者多个数据后再去唤醒它。
    同样地,当缓冲区已经空了,而消费者还想去取消息,此时也可以让消费者进行休眠,等待生产者放入一个或者多个数据时再唤醒它。
    再具体一点:
    a.生产者生产数据到缓冲区中,消费者从缓冲区中取数据。
    b. 如果缓冲区已经满了,则生产者线程阻塞。
    c. 如果缓冲区为空,那么消费者线程阻塞。
    上述过程的描述应该已经体现出生产者和消费者之间的线程通信的流程,生产者一旦将队列生成满了之后就要控制线程停止生产,直到消费者将队列中消费一个之后就可以通知生产者继续生产新的元素,当消费者线程将队列中的元素全部取出之后消费者线程就需要停止消费元素,直到生产者线程向队列中添加一个元素之后可以通知消费者线程继续消费元素。
    在这里插入图片描述

    编写一个生产者、消费者模型,给定要求:一个生产者、一个消费者、仓库是三个

    生产者

    public class Producer extends Thread{
        private LinkedList<Integer> cap;//共享仓库
        private Random random = new Random();
    
        public Producer(LinkedList<Integer> cap) {
            this.cap = cap;
        }
    
        @Override
        public void run() {
            while (true) {
                synchronized (cap){
                    if (cap.size() == 3) {//缓冲区满 生产者进行阻塞
                        try {
                            cap.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
    
                    //生产产品
                    int i = random.nextInt(1000);
                    System.out.println("生产者生产了" + i);
                    cap.add(i);
    
                    //通知消费者消费产品
                    cap.notify();
                }
            }
        }
    }
    

    消费者

    public class Consumer extends Thread{
        private LinkedList<Integer> cap;
    
        public Consumer(LinkedList<Integer> cap) {
            this.cap = cap;
        }
    
        @Override
        public void run() {
            while (true) {
                synchronized (cap) {
                    if (cap.size() == 0) { //如果缓冲区为0,消费者阻塞
                        try {
                            cap.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
    
                    //消费者消费产品
                    Integer i = cap.remove();
                    System.out.println("消费者消费了" + i);
    
                    //通知生产者生产
                    cap.notify();
                }
            }
        }
    }
    

    测试类

    public class Test {
        public static void main(String[] args) {
            LinkedList<Integer> cap = new LinkedList<>();
            Producer producer = new Producer(cap);
            Consumer consumer = new Consumer(cap);
    
            producer.start();
            consumer.start();
        }
    }
    

    运行结果
    在这里插入图片描述

    展开全文
  • C例子:线程间通信

    2016-01-24 23:28:05
    该程序是我写的博客“一起talk C栗子吧(第一百一十一回:C语言实例--线程间通信)”的配套程序,共享给大家使用
  • 进程间通信和线程间通信

    千次阅读 2020-08-18 20:26:22
    线程间通信 进程和线程的区别 程序只是一组指令的有序集合,它本身没有任何运行的含义,它只是一个静态的实体。而进程则不同,它是程序在某个数据集上的执行。进程是一个动态的实体,它有自己的生命周期。它因...

    进程间通信 转自  https://www.cnblogs.com/LUO77/p/5816326.html

    线程间通信  https://www.cnblogs.com/jobs1/p/10784021.html

    线程间通信 

     

    进程和线程的区别

    程序只是一组指令的有序集合,它本身没有任何运行的含义,它只是一个静态的实体。而进程则不同,它是程序在某个数据集上的执行。进程是一个动态的实体,它有自己的生命周期。它因创建而产生,因调度而运行,因等待资源或事件而被处于等待状态,因完成任务而被撤消。反映了一个程序在一定的数据集上运行的全部动态过程。

    进程:是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竞争计算机系统资源的基本单位。

    线程:是进程的一个执行单元,是进程内科调度实体。比进程更小的独立运行的基本单位。线程也被称为轻量级进程。

    一个程序至少一个进程,一个进程至少一个线程。

    为什么会有线程?

      每个进程都有自己的地址空间,即进程空间,在网络或多用户换机下,一个服务器通常需要接收大量不确定数量用户的并发请求,为每一个请求都创建一个进程显然行不通(系统开销大响应用户请求效率低),因此操作系统中线程概念被引进。

    • 线程的执行过程是线性的,尽管中间会发生中断或者暂停,但是进程所拥有的资源只为改线状执行过程服务,一旦发生线程切换,这些资源需要被保护起来。
    • 进程分为单线程进程和多线程进程,单线程进程宏观来看也是线性执行过程,微观上只有单一的执行过程。多线程进程宏观是线性的,微观上多个执行操作。

    线程的改变只代表CPU的执行过程的改变,而没有发生进程所拥有的资源的变化。 

    进程线程的区别:

    • 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
    • 资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间的资源是独立的。

         一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

         进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程

    • 执行过程:每个独立的进程程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
    • 线程是处理器调度的基本单位,但是进程不是。
    • 两者均可并发执行。

    优缺点:

      线程执行开销小,但是不利于资源的管理和保护。线程适合在SMP机器(双CPU系统)上运行。

      进程执行开销大,但是能够很好的进行资源管理和保护。进程可以跨机器前移。

    何时使用多进程,何时使用多线程?

    对资源的管理和保护要求高,不限制开销和效率时,使用多进程。

    要求效率高,频繁切换时,资源的保护管理要求不是很高时,使用多线程。

     

    进程间通信

    多进程:

    首先,先来讲一下fork之后,发生了什么事情。

    由fork创建的新进程被称为子进程(child process)。该函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值则是新进程(子进程)的进程 id。将子进程id返回给父进程的理由是:因为一个进程的子进程可以多于一个,没有一个函数使一个进程可以获得其所有子进程的进程id。对子进程来说,之所以fork返回0给它,是因为它随时可以调用getpid()来获取自己的pid;也可以调用getppid()来获取父进程的id。(进程id 0总是由交换进程使用,所以一个子进程的进程id不可能为0 )。

    fork之后,操作系统会复制一个与父进程完全相同的子进程,虽说是父子关系,但是在操作系统看来,他们更像兄弟关系,这2个进程共享代码空间,但是数据空间是互相独立的,子进程数据空间中的内容是父进程的完整拷贝,指令指针也完全相同,子进程拥有父进程当前运行到的位置两进程的程序计数器pc值相同,也就是说,子进程是从fork返回处开始执行的),但有一点不同,如果fork成功,子进程中fork的返回值是0,父进程中fork的返回值是子进程的进程号,如果fork不成功,父进程会返回错误。
    可以这样想象,2个进程一直同时运行,而且步调一致,在fork之后,他们分别作不同的工作,也就是分岔了。这也是fork为什么叫fork的原因

    至于那一个最先运行,可能与操作系统(调度算法)有关,而且这个问题在实际应用中并不重要,如果需要父子进程协同,可以通过原语的办法解决。


     

    常见的通信方式:

    1. 管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
    2. 命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
    4. 消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
    5. 共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
    6. 信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
    7. 套接字Socket:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
    8. 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

     

    信号:

    信号是Linux系统中用于进程之间通信或操作的一种机制,信号可以在任何时候发送给某一进程,而无须知道该进程的状态。如果该进程并未处于执行状态,则该信号就由内核保存起来,知道该进程恢复执行并传递给他为止。如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。

     

    Linux提供了几十种信号,分别代表着不同的意义。信号之间依靠他们的值来区分,但是通常在程序中使用信号的名字来表示一个信号。在Linux系统中,这些信号和以他们的名称命名的常量被定义在/usr/includebitssignum.h文件中。通常程序中直接包含<signal.h>就好。

     

    信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式,信号可以在用户空间进程和内核之间直接交互。内核也可以利用信号来通知用户空间的进程来通知用户空间发生了哪些系统事件。信号事件有两个来源:

    1)硬件来源,例如按下了cltr+C,通常产生中断信号sigint

    2)软件来源,例如使用系统调用或者命令发出信号。最常用的发送信号的系统函数是kill,raise,setitimer,sigation,sigqueue函数。软件来源还包括一些非法运算等操作。

     

    一旦有信号产生,用户进程对信号产生的相应有三种方式:

    1)执行默认操作,linux对每种信号都规定了默认操作。

    2)捕捉信号,定义信号处理函数,当信号发生时,执行相应的处理函数。

    3)忽略信号,当不希望接收到的信号对进程的执行产生影响,而让进程继续执行时,可以忽略该信号,即不对信号进程作任何处理。

      有两个信号是应用进程无法捕捉和忽略的,即SIGKILL和SEGSTOP,这是为了使系统管理员能在任何时候中断或结束某一特定的进程。

    上图表示了Linux中常见的命令

    1、信号发送:

    信号发送的关键使得系统知道向哪个进程发送信号以及发送什么信号。下面是信号操作中常用的函数:

    例子:创建子进程,为了使子进程不在父进程发出信号前结束,子进程中使用raise函数发送sigstop信号,使自己暂停;父进程使用信号操作的kill函数,向子进程发送sigkill信号,子进程收到此信号,结束子进程。

    2、信号处理

    当某个信号被发送到一个正在运行的进程时,该进程即对次特定的信号注册相应的信号处理函数,以完成所需处理。设置信号处理方式的是signal函数,在程序正常结束前,在应用signal函数恢复系统对信号的

    默认处理方式。

    3.信号阻塞

    有时候既不希望进程在接收到信号时立刻中断进程的执行,也不希望此信号完全被忽略掉,而是希望延迟一段时间再去调用信号处理函数,这个时候就需要信号阻塞来完成。

     

    例子:主程序阻塞了cltr+c的sigint信号。用sigpromask将sigint假如阻塞信号集合。

     

    管道:

    管道允许在进程之间按先进先出的方式传送数据,是进程间通信的一种常见方式。

    管道是Linux 支持的最初Unix IPC形式之一,具有以下特点:

    1) 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道

    2) 匿名管道只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程);

    3) 单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。

     

    管道分为pipe(无名管道)和fifo(命名管道)两种,除了建立、打开、删除的方式不同外,这两种管道几乎是一样的。他们都是通过内核缓冲区实现数据传输。

    • pipe用于相关进程之间的通信,例如父进程和子进程,它通过pipe()系统调用来创建并打开,当最后一个使用它的进程关闭对他的引用时,pipe将自动撤销。
    • FIFO即命名管道,在磁盘上有对应的节点,但没有数据块——换言之,只是拥有一个名字和相应的访问权限,通过mknode()系统调用或者mkfifo()函数来建立的。一旦建立,任何进程都可以通过文件名将其打开和进行读写,而不局限于父子进程,当然前提是进程对FIFO有适当的访问权。当不再被进程使用时,FIFO在内存中释放,但磁盘节点仍然存在。

    管道的实质是一个内核缓冲区,进程以先进先出的方式从缓冲区存取数据:管道一端的进程顺序地将进程数据写入缓冲区,另一端的进程则顺序地读取数据,该缓冲区可以看做一个循环队列,读和写的位置都是自动增加的,一个数据只能被读一次,读出以后再缓冲区都不复存在了。当缓冲区读空或者写满时,有一定的规则控制相应的读进程或写进程是否进入等待队列,当空的缓冲区有新数据写入或慢的缓冲区有数据读出时,就唤醒等待队列中的进程继续读写。

    无名管道:

    pipe的例子:父进程创建管道,并在管道中写入数据,而子进程从管道读出数据

    命名管道:

    和无名管道的主要区别在于,命名管道有一个名字,命名管道的名字对应于一个磁盘索引节点,有了这个文件名,任何进程有相应的权限都可以对它进行访问。

    而无名管道却不同,进程只能访问自己或祖先创建的管道,而不能访任意访问已经存在的管道——因为没有名字。

     

    Linux中通过系统调用mknod()或makefifo()来创建一个命名管道。最简单的方式是通过直接使用shell

    mkfifo myfifo

     

     等价于

    mknod myfifo p

     

    以上命令在当前目录下创建了一个名为myfifo的命名管道。用ls -p命令查看文件的类型时,可以看到命名管道对应的文件名后有一条竖线"|",表示该文件不是普通文件而是命名管道。

    使用open()函数通过文件名可以打开已经创建的命名管道,而无名管道不能由open来打开。当一个命名管道不再被任何进程打开时,它没有消失,还可以再次被打开,就像打开一个磁盘文件一样。

    可以用删除普通文件的方法将其删除,实际删除的事磁盘上对应的节点信息。

    例子:用命名管道实现聊天程序,一个张三端,一个李四端。两个程序都建立两个命名管道,fifo1,fifo2,张三写fifo1,李四读fifo1;李四写fifo2,张三读fifo2。

    用select把,管道描述符和stdin假如集合,用select进行阻塞,如果有i/o的时候唤醒进程。(粉红色部分为select部分,黄色部分为命名管道部分)

     

     

     

    在linux系统中,除了用pipe系统调用建立管道外,还可以使用C函数库中管道函数popen函数来建立管道,使用pclose关闭管道。

    例子:设计一个程序用popen创建管道,实现 ls -l |grep main.c的功能

    分析:先用popen函数创建一个读管道,调用fread函数将ls -l的结果存入buf变量,用printf函数输出内容,用pclose关闭读管道;

    接着用popen函数创建一个写管道,调用fprintf函数将buf的内容写入管道,运行grep命令。

    popen的函数原型:

    FILE* popen(const char* command,const char* type);

     

    参数说明:command是子进程要执行的命令,type表示管道的类型,r表示读管道,w代表写管道。如果成功返回管道文件的指针,否则返回NULL。

    使用popen函数读写管道,实际上也是调用pipe函数调用建立一个管道,再调用fork函数建立子进程,接着会建立一个shell 环境,并在这个shell环境中执行参数所指定的进程。

    消息队列:

    消息队列,就是一个消息的链表,是一系列保存在内核中消息的列表。用户进程可以向消息队列添加消息,也可以向消息队列读取消息。

    消息队列与管道通信相比,其优势是对每个消息指定特定的消息类型,接收的时候不需要按照队列次序,而是可以根据自定义条件接收特定类型的消息。

    可以把消息看做一个记录,具有特定的格式以及特定的优先级。对消息队列有写权限的进程可以向消息队列中按照一定的规则添加新消息,对消息队列有读权限的进程可以从消息队列中读取消息。

    消息队列的常用函数如下表:

    进程间通过消息队列通信,主要是:创建或打开消息队列,添加消息,读取消息和控制消息队列。

    例子:用函数msget创建消息队列,调用msgsnd函数,把输入的字符串添加到消息队列中,然后调用msgrcv函数,读取消息队列中的消息并打印输出,最后再调用msgctl函数,删除系统内核中的消息队列。(黄色部分是消息队列相关的关键代码,粉色部分是读取stdin的关键代码)

    共享内存:

    共享内存允许两个或多个进程共享一个给定的存储区,这一段存储区可以被两个或两个以上的进程映射至自身的地址空间中,一个进程写入共享内存的信息,可以被其他使用这个共享内存的进程,通过一个简单的内存读取错做读出,从而实现了进程间的通信。

     

    采用共享内存进行通信的一个主要好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝,对于像管道和消息队里等通信方式,则需要再内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次:一次从输入文件到共享内存区,另一次从共享内存到输出文件。

    一般而言,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时在重新建立共享内存区域;而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件,因此,采用共享内存的通信方式效率非常高。

    共享内存有两种实现方式:1、内存映射 2、共享内存机制

    1、内存映射

    内存映射 memory map机制使进程之间通过映射同一个普通文件实现共享内存,通过mmap()系统调用实现。普通文件被映射到进程地址空间后,进程可以

    像访问普通内存一样对文件进行访问,不必再调用read/write等文件操作函数。

    例子:创建子进程,父子进程通过匿名映射实现共享内存。

    分析:主程序中先调用mmap映射内存,然后再调用fork函数创建进程。那么在调用fork函数之后,子进程继承父进程匿名映射后的地址空间,同样也继承mmap函数的返回地址,这样,父子进程就可以通过映射区域进行通信了。

    2、UNIX System V共享内存机制

    IPC的共享内存指的是把所有的共享数据放在共享内存区域(IPC shared memory region),任何想要访问该数据的进程都必须在本进程的地址空间新增一块内存区域,用来映射存放共享数据的物理内存页面。

    和前面的mmap系统调用通过映射一个普通文件实现共享内存不同,UNIX system V共享内存是通过映射特殊文件系统shm中的文件实现进程间的共享内存通信。

    例子:设计两个程序,通过unix system v共享内存机制,一个程序写入共享区域,另一个程序读取共享区域。

    分析:一个程序调用fotk函数产生标准的key,接着调用shmget函数,获取共享内存区域的id,调用shmat函数,映射内存,循环计算年龄,另一个程序读取共享内存。

    (fotk函数在消息队列部分已经用过了,

    根据pathname指定的文件(或目录)名称,以及proj参数指定的数字,ftok函数为IPC对象生成一个唯一性的键值。)

    key_t ftok(char* pathname,char proj)

     

    c++ 线程间通信方式

     

    一:两个进程间的两个线程通信,相当于进程间通信

    二:一个进程中的两个线程间通信

      通信方式:

    1.互斥锁

      mutex;

      lock_guard (在构造函数里加锁,在析构函数里解锁)

      unique_lock 自动加锁、解锁

     

    2.读写锁

      shared_lock

    3.信号量

      c++11中未实现,可以自己使用mutex和conditon_variable 实现

      代码实现如下: 

    #pragma once
    #include <mutex>
    #include <condition_variable>
    class Semaphore
    {
    public:
     explicit Semaphore(unsigned int count); //用无符号数表示信号量资源 
     ~Semaphore();

    public:
     void wait();
     void signal();
    private:
     int m_count; //计数器必须是有符号数 
     std::mutex m_mutex;
     std::condition_variable m_condition_variable;
    };

     

    #include "Semaphore.h"

    Semaphore::Semaphore(unsigned int count) :m_count(count) {
    }

    Semaphore::~Semaphore()
    {
    }

    void Semaphore::wait() {
     std::unique_lock<std::mutex> unique_lock(m_mutex);
     --m_count;
     while (m_count < 0) {
      m_condition_variable.wait(unique_lock);
     }
    }

    void Semaphore::signal() {
     std::lock_guard<std::mutex> lg(m_mutex);
     if (++m_count < 1) {
      m_condition_variable.notify_one();
     }
    }

    4.条件变量

      condition_variable

    展开全文
  • Android进程间和线程间通信方式

    千次阅读 2021-01-17 13:22:41
    线程自己基本上不拥有系统资源,只拥有一些在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。区别:(1)、一个程序至少有一个进程,一个进...
  • Linux进程间通信-线程间通信

    千次阅读 2021-05-08 23:29:12
    Linux作为一种新兴的操作系统,几乎支持所有的Unix下常用的进程间通信方法:管道、消息队列、共享内存、信号量、套接口。1、管道管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘...
  • 文档详细介绍了Handler实现线程间通信过程中源码的实现机制,并对其中的Looper,MessageQueue,Message对象的初始化及sendMessage()通过sendMessageDelayed()和sendMessageAtTime()过程的操作机流程
  • 在《Android Handler之消息循环的深入解析》中谈到了Handler是用于操作线程内部的消息队列,所以Handler可以用来线程间通信ITC,这种方式更加安全和高效,可以大大减少同步的烦恼,甚至都可以不用syncrhonized。...
  • 线程间通信及同步方法介绍

    千次阅读 2021-05-23 11:01:04
    此前小编给大家介绍了进程间通信的方法,于是一些伙伴又好奇线程间的通信及同步方法,没关系,下面小编就继续给大家科普下线程间通信及同步的方法。线程间通信及同步方法介绍:一、线程间的通信方式1、使用全局变量...
  • python线程间通信

    2022-03-31 20:32:58
    python线程间通信
  • C#线程间通信

    2013-09-22 12:42:13
    采用的是C#的Winform开发,提供了两种线程交互的方式。 第一:在主线程中开启两个子线程,子线程用事件方式来进行通信。对于主线程的控件操作采用的是delegate委托的方式,避免主线程假死。 第二:采用的是...
  • 线程间通信方式2:参数传递方式

    热门讨论 2013-01-11 11:17:13
    线程间通信方式2:参数传递方式。通过3类线程的创建方法,演示了给线程传递方式的方式,包括;单参数、多参数和类3类。
  • 相比于进程间通信来说,线程间通信无疑是相对比较简单的。 首先我们来看看最简单的方法,那就是使用全局变量(静态变量也可以)来进行通信,由于属于同一个进程的各个线程是处于同一个进程空间中的,并且它们共享这...
  • Python 多线程及线程间通信

    千次阅读 多人点赞 2020-09-05 17:27:00
    一个线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每个线程并行执行不同的任务。 由于线程是操作系统直接支持的执行单元,因此,高级语言(如 Python、Java 等)通常都内置多线程的支持。...
  • 主要介绍了Java管道流实现线程间通信过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
  • 主要介绍了python基于event实现线程间通信控制,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
  • 进程间通信的概念 每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2...
  • Linux的进程/线程间通信方式总结

    千次阅读 2021-09-24 15:33:23
    Linux系统中的进程间通信方式 进程是一个独立的资源分配单元,不同的进程(通常指的是用户进程)之间的资源是独立的,没有关联,不能在同一个进程中直接访问另一个进程的资源。 进程间通信(IPC)的目的: 数据...
  • 使用threading.Event可以实现线程间相互通信,之前的Python:使用threading模块实现多线程编程七[使用Condition实现复杂同步]我们已经初步实现了线程间通信的基本功能,但是更为通用的一种做法是使用threading.Event...
  • 一、进程间通信(IPC,Inter-Process Communication)是指在不同进程间传播或交换信息 1. 无名管道 特点 半双工(数据流向仅有一个方向),具有固定的读端和写端 只能用于父进程或兄弟线程之间通信(具有血缘关系的...
  • 多线程的几个例子,例子中有线程的打开,暂停,停止和线程间通信,VC6环境打开并调试。
  • 在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。进程是一个具有独立功能的程序关于某个数据集合的一次运行活动。它可以申请和拥有系统资源,是一个...
  • MFC线程间通信

    2017-04-05 19:23:45
    线程间的通信一般采用四种方式:全局变量方式、消息传递方式、参数传递方式和线程同步法。线程间通信最简单的一种方法是使用全局变量,这用的就是全局变量的方式。
  • 主要介绍了java线程间通信的通俗解释,介绍了线程通信中的几个相关概念,然后分享了线程通信的实现方式及代码示例,具有一定参考价值 ,需要的朋友可以了解下。
  • 11_C++多线程及线程间通信

    千次阅读 2019-08-08 18:06:56
    参考: https://m.imooc.com/article/289630 C++11 标准库新引入的线程库 ...(一)多线程编程 #include <iostream> #include <thread> #include <mutex> #include <condi...
  • c++ 线程间通信方式

    万次阅读 多人点赞 2019-07-28 23:07:51
    但互斥无法限制访问者对资源的访问顺序,即访问是无序的,线程间不需要知道彼此的存在。 同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问,线程间知道彼此的存在。在大多数情况下...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 233,905
精华内容 93,562
关键字:

线程间通信

友情链接: castleGame2.345.zip