【问题标题】:Providing Asynchronous Serial Port Communication提供异步串口通信
【发布时间】:2011-08-03 04:57:45
【问题描述】:

目前我们的应用程序通过串行端口连接到 Arduino。我们发送一些 ASCII 格式的命令,并得到相同的回报。为此,我们有一个命令队列,一个专用于将这些命令写入端口的线程,以及一个专用于读取和处理所有传入回复的线程。类本身负责发送回复,这给了它太多的责任(应该只负责端口操作,而不是业务逻辑)。

我们宁愿以异步方式执行此操作。系统中的任何东西都可以发送带有回调函数和超时的命令。如果串口得到正确回复,则调用回调函数。否则,它会超时并可能调用第二个回调(或者可能是带有 succeeded? 标志的单个回调)。

但是,我们只使用过异步方法(尤其是在 Web 操作中),没有编写过这样的系统。谁能给我们一些关于如何进行的指示?

我们当前的计划是存储这些命令的队列。在任何回复中,如果找到关联的命令(通过比较 ASCII 值),则将其出列并执行回调。计时器将定期检查超时、出列并执行适当的回调。这似乎是一个简单的解决方案,但支持它的代码量正在大幅增加,我们希望确保没有更好的内置解决方案或最佳实践。

编辑:为了进一步澄清,这个特定的类是一个单例(无论好坏),并且有许多其他线程正在运行可以访问它。例如,一个线程可能想要请求传感器值,而另一个线程可能正在控制电机。这些命令及其相关回复不会以线性方式发生;时间可能会颠倒。因此,传统的生产者-消费者模型是不够的;这更像是一个调度程序。

例如,我们称这个单例类为ArduinoThread A 正在运行,并且想发送命令"*03",所以它调用Arduino.Instance.SendCommand("*03")。同时,Thread B 发送一个命令"*88",这两个命令都是近实时发送的。稍后,ArduinoSerialPort.Read() 线程收到*88 的回复,然后收到*03 的回复(即以相反的顺序发送它们)。我们如何允许Thread AThread B 正确阻止等待特定回复的到来?我们假设我们将在每个线程中使用AutoResetEvent,并使用异步回调让我们.Set 它。

【问题讨论】:

    标签: c# multithreading asynchronous serial-port


    【解决方案1】:

    如果性能是您所追求的,并且异步处于最佳水平,我建议您研究 Completion Ports。这就是最终隐藏在 Windows 内核中的内容,而且非常棒。当我使用它们时,我使用了 C++,甚至因此发现了一个内核错误,但是我仅限于该语言。

    我在 CodeProject 上看到了 this article,这可能值得探索,看看您可以在哪里进一步推进您的想法和/或使用那里的代码。

    完成端口的本质是处理回调。也就是说,一般来说,您将请求“放入”队列中,当有东西到达那里时,会读取请求并读取指定的回调。事实上,这是一个队列,但就像我说的,处于最低(可管理)级别(在几乎进入金属之前)。

    编辑:我已经编写了一种带有完成端口的 FTP 服务器/客户端测试实用程序,因此基本过程是相同的 - 在 queuable 时尚。希望对您有所帮助。

    编辑 #2: 好的,这就是我将根据您的反馈和 cmets 执行的操作。我会有一个“传出队列”ConcurrentQueue<Message>。您可以通过将每条消息出列来有一个单独的线程来发送消息。 注意,如果你想让它更“安全”一点,我建议查看消息,发送它,然后将它出队。 无论如何,Message 类可以是内部的,看起来像这样:

    private class Message {
        public string Command { get; set; }
        ... additonal properties, like timeouts, etc. ...
    }
    

    在单例类中(我称之为CommunicationService),我也会有一个ConcurrentBag<Action<Response>>。现在是乐趣开始的地方:o)。当一个单独的关注点想要做某事时,它会注册自己,例如,如果你有一个TemepratureMeter,我会让它做这样的事情:

    public class TemperatureMeter {
       private AutoResetEvent _signal = new AutoResetEvent(false);
    
       public TemperatureMeter {
         CommunicationService.AddHandler(HandlePotentialTemperatureResponse);
       }
    
       public bool HandlePotentialTemperatureResponse(Response response) {
         // if response is what I'm looking for
         _signal.Set();
    
         // store the result in a queue or something =)
       }
    
       public decimal ReadTemperature() {
         CommunicationService.SendCommand(Commands.ReadTemperature);
         _signal.WaitOne(Commands.ReadTemperature.TimeOut); // or smth like this
    
         return /* dequeued value from the handle potential temperature response */;
       }
    
    }
    

    现在,在您的 CommunicationService 中,当您收到响应时,您只需发送一个

    foreach(var action in this._callbacks) {
       action(rcvResponse);
    }
    

    瞧,关注点分离。它能更好地回答你的问题吗?

    另一种可能的策略是,将消息和回调耦合,但让回调为 Func<Response, bool> 并且调度程序线程检查从 Func 返回的结果是否为真,然后此回调已处理.

    【讨论】:

    • 这个挺有意思的,我来看看。在这种特殊情况下,我们不太关心性能,而更多地关心问题的管理。 Arduino 类不应该关心对数据做了什么(就像现在一样),它应该把这个责任留给负责发送原始命令的类。考虑到每分钟只发送
    • 您的回复是否可以乱序发送?还是总是让您的底层系统以排队的方式处理它们?
    • 它们可以随时以任何顺序发送。系统中的各种组件将在近乎随机的时间发出请求,而微控制器将在某种程度上异步处理它们。例如,一个组件可能需要温度,这是一个快速操作,而另一个组件正在移动步进电机,这需要更长的时间。传感器请求和响应可能发生在电机运行期间。请求是线性处理的,但我得到的回复没有特定的顺序。
    • 但是您可以推断出哪个回复是针对哪个消息的?如果是这样,那么我建议将全部关注转移到基于管道的系统中。因此,使用队列进行调度,使用管道进行到达回复。这意味着每个成员都会收到响应通知。我将在上面编辑#2 我的答案以反映这一点。
    • 这实际上与我们的想法一致,但它有助于我们进一步抽象它。我认为将这部分分离到TemperatureMeter 类中是缺失的部分。我们试图直接从另一个线程执行此操作,这就是代码不断增长的原因。我们可能会更进一步,将这种逻辑封装在abstract class 中,并允许工厂从配置中创建每个基类。感谢您帮助我们清理它!
    【解决方案2】:

    如果您使用 4.0,更好的选择是为此使用 BlockingCollection,对于旧版本,请使用 Queue<T>AutoResetEvent 的组合。因此,当添加项目并在消费者线程上时,您将收到通知,然后就可以使用它了。在这里,我们使用的是推送技术,在您当前的实施中,您“每次询问是否有任何数据”时都使用轮询技术。

    示例:4.0

    //declare the buffer
    private BlockingCollection<Data> _buffer = new BlockingCollection<Data>(new ConcurrentQueue<Data>());
    
    //at the producer method "whenever you received an item":
    _messageBuffer.Add(new Data());
    
    //at the consumer thread "another thread(s) that is running without to consume the data when it arrived."
    foreach (Data data in _buffer.GetConsumingEnumerable())// or "_buffer.Take" it will block here automatically waiting from new items to be added
    {
        //handle the data here.
    }
    

    示例:其他“较低”版本:

    private ConcurrentQueue<Data> _queue = new ConcurrentQueue<Data>();
    private AutoResetEvent _queueNotifier = new AutoResetEvent(false);
    
    //at the producer:
    _queue.Enqueue(new Data());
    _queueNotifier.Set();
    
    //at the consumer:
    while (true)//or some condition
    {
        _queueNotifier.WaitOne();//here we will block until receive signal notification.
        Data data;
        if (_queue.TryDequeue(out data))
        {
            //handle the data
        }
    }
    

    【讨论】:

    • 我们基本上已经这样做了,以处理来自串行端口的传入数据。我们封装每个回复并将其放在BlockingCollection 中。然后,另一个线程只是等待一个新的回复进来,然后在收到它后处理它。这里的问题是如何以允许第二个消费者(特定于我得到的回复)等待该回复的方式处理它。
    猜你喜欢
    • 2015-04-11
    • 2016-11-25
    • 1970-01-01
    • 2017-05-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多