【问题标题】:C# threading help needed需要 C# 线程帮助
【发布时间】:2012-04-18 11:33:20
【问题描述】:

有人要求我编写一个方法,允许调用者通过串行端口向硬件设备发送命令字符串。发送命令后,该方法必须等待设备的响应,然后将其返回给调用者。

为了使事情复杂化,硬件设备会定期向 PC 发送未经请求的数据包(应用必须存储以进行报告的数据)。所以当我发送一个串口命令时,我可能会在收到命令响应之前收到一个或多个数据包。

其他注意事项:可能有多个客户端可能同时发送串行命令,因为此方法将构成 WCF 服务的基础。此外,该方法需要同步(原因我不会在这里介绍),因此排除了使用回调将响应返回给客户端。

关于“多个客户端”,我打算使用 BlockingCollection 来对传入的命令进行排队,并使用一个后台线程一次执行一个任务,从而避免串行端口争用。

但是我不确定如何处理传入的串行数据。我最初的想法是有另一个后台线程不断读取串行端口,存储数据分析包,但也寻找命令响应。当接收到一个时,线程会以某种方式将响应数据返回给最初发送串行命令的方法(从那时起它一直在等待 - 请记住我有一个规定,该方法是同步的)。

这是我不确定的最后一点 - 我怎样才能让我的方法等到后台线程收到命令的响应?以及如何将后台线程的响应传递给我的等待方法,以便它可以将其返回给调用者?我是线程新手,所以我是不是走错路了?

提前致谢

安迪

【问题讨论】:

  • 不要“等待”返回。你在一个事件驱动的系统中。
  • 来自设备的数据是否有某种请求 ID,以便您知道数据的用途?
  • en.wikipedia.org/wiki/Actor_model !正如@Henk 所说,让它变得更基础,大部分复杂性都消失了。
  • @Peter,是的,响应字符串中会有一些内容来识别它响应的是哪个命令。
  • @Henk,我同意让它基于事件会简化事情,回调可能是我的答案!不幸的是,WCF 服务将被非 .Net 客户端调用(例如通过 REST),所以这必须是同步的。虽然我可以以事件驱动的方式编写大部分功能,但它可能必须在 WCF 服务中的某个点收敛到同步方法。

标签: c# multithreading


【解决方案1】:

首先:当你使用框架自带的SerialPort类时,数据接收事件已经是异步的了。当您发送内容时,数据会异步传入。

我想尝试的是:将所有需要等待答案的请求排队。在整个接收处理程序中,检查传入数据是否是其中一个请求的答案。如果是这样,将回复与请求信息一起存储(为此创建某种状态类)。所有其他传入数据均正常处理。

那么,如何让请求等待答复呢?发送命令并返回回复的调用将创建状态对象,将其排队并监视对象以查看是否收到了答案。如果收到应答,则调用返回结果。

一个可能的大纲可能是:

string SendAndWait(string command)
{
    StateObject state = new StateObject(command);
    state.ReplyReceived = new ManualResetEvent(false);
    try
    {
        SerialPortHandler.Instance.SendRequest(command, state);
        state.ReplyReceived.WaitOne();
    }
    finally
    {
        state.ReplyReceived.Close();
    }

    return state.Reply;
}

SerialPortHandler 是什么?我会将此作为一个单例类,其中包含一个 Instance 属性来访问单例实例。这个类完成了所有的串口工作。它还应该包含一个在“带外”信息进入时引发的事件(不是对命令的回复的数据)。

它还包含SendRequest 方法,该方法将命令发送到串行设备,将状态对象存储在内部列表中,等待命令的回复进入并使用回复更新状态对象。

状态对象包含一个名为ReplyReceived 的等待句柄,它在更改状态对象的Reply 属性后由SerialPortHandler 设置。这样你就不需要循环和Thread.Sleep。此外,您可以调用WaitOne(timeout) 而不是调用WaitOne(),而timeout 是等待回复进来的毫秒数。这样您就可以实现某种超时功能。

这就是它在SerialPortHandler 中的样子:

void HandlePossibleCommandReply(string reply)
{
    StateObject state = FindStateObjectForReply(reply);
    if (state != null)
    {
        state.Reply = reply;
        state.ReplyReceived.Set();

        m_internalStateList.Remove(state);
    }
}

请注意:这是我尝试开始的。我确信这可以得到非常优化,但是正如您所见,其中并没有涉及太多“多线程” - 只有 SendAndWait 方法应该以某种方式调用,以便多个客户端可以在另一个客户端仍在等待时发出命令它的回应。

编辑
另请注意:您是说该方法应构成 WCF 服务的基础。这使事情变得更容易,就像您配置服务权限一样,将为每次调用服务创建一个服务类的实例,因此SendAndWait 方法将“活”在它自己的服务实例中并且不会甚至根本需要重入。在这种情况下,您只需确保 SerialPortHandler 始终处于活动状态(=> 独立于实际的 WCF 服务创建和运行),无论当前是否存在您的服务类的实例。

编辑 2
按照 cmets 的建议,我将示例代码更改为不循环和休眠。

【讨论】:

  • 一开始听起来不错,然后我看到了 Sleep(100)。不过,状态对象很好。我在回答中将其称为“serialAPU”,这基本上就是您的建议。我认为您错过的只是状态对象中的同步(可能是 autoResetEvent),以供原始线程等待。
  • 好吧,我认为同步在这里不是什么大问题,但我会尝试将其纳入我的答案中。
  • 我猜这不是真正的同步,只是发信号。问题是,操作系统书籍坚持调用事件、信号量、互斥锁等“同步对象”。
  • 我已经相应地更改了我的回复 - 这也可以轻松实现超时。
  • 超时完全是另一个问题。串行端口线程/s 可能在向量或列表中有对 stateObject 的引用。这需要以某种方式删除。它可能有一个可以设置为“EpsTimedOut”的内部状态枚举,并且串行线程通常可以在遇到这些对象时从容器中删除它们。状态的设置必须是线程安全的,以防止请求线程和串行线程做出错误的决定。
【解决方案2】:

如果你真的想要阻塞直到后台线程收到你的命令响应,你可以考虑让后台线程在你排队你的命令并将它返回给你时锁定一个对象。接下来,您等待锁定并继续:

// in main code:
var locker = mySerialManager.Enquee(command);
lock (locker)
{
     // this will only be executed, when mySerialManager unlocks the lock
}

// in SerialManager
public object Enqueue(object command)
{
    var locker = new Object();
    Monitor.Enter(locker); 
    // NOTE: Monitor.Exit() gets called when command result 
    // arrives on serial port
    EnqueueCommand(command, locker);
    return locker;
}

【讨论】:

    【解决方案3】:

    几件事。您需要能够将串行响应绑定到请求它们的命令。我假设有一些索引或序列号随命令发出并在响应中返回?

    鉴于此,您应该没问题。您需要某种“serialAPU”类来表示请求和响应。我不知道这些是什么,也许只是字符串,我不知道。该类也应该有一个 autoResetEvent。无论如何,在你的'DoSerialProtocol()'函数中,创建一个serialAPU,用请求数据加载它,将它排入串行线程并等待autoResetEvent。当线程获得serialAPU后,它可以在serialAPU中存储一个索引/序列号,将serialAPU存储在一个vector中并发送请求。

    当数据进来时,你协议的东西,如果数据是一个有效的响应,从数据中获取索引/序列,并在向量中的 serialAPU 中查找匹配值。从向量中移除匹配的serialAPU,用响应数据加载它并发出autoResetEvent信号。最初调用 'DoSerialProtocol()' 的线程将继续运行并可以处理响应数据。

    当然有很多“摆动”。超时是其中之一。我很想在 serialAPU 中有一个状态枚举,受 CritcalSection 或 atomicCompareandSwap 保护,初始化为“Esubmitted”。如果起始线程在 autoResetEvent 上的等待超时,它会尝试将其 serialAPU 中的状态枚举设置为“EtimedOut”。如果成功,很好,它会向调用者返回一个错误。同样,在串行线程中,如果它找到状态为 EtimedOut 的串行 APU,它只是将其从容器中删除。如果找到与响应数据匹配的serialAPU,它会尝试将状态更改为“EdataRx”,如果成功。触发 autoRestEvent。

    另一个是烦人的OOB数据。如果出现这种情况,创建一个serialAPU,加载OOB数据,将状态设置为'EOOBdata'并用它调用一些'OOBevent'。

    【讨论】:

      【解决方案4】:

      我建议你看看BackgroundWorker-Class

      这个类中有一个事件(RunWorkerCompleted),当工人完成他的工作时会触发它。

      【讨论】:

      • BackgroundWorker 并非在所有情况下都是正确的工具 - 而这种情况似乎是后台工作人员不是正确工具的情况之一......
      • 我支持@ThorstenDittmar。我非常不喜欢这种方法,因此应该投反对票。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-08-27
      • 1970-01-01
      相关资源
      最近更新 更多