1

我正在使用其Modbus协议向硬件控制器编写 C# 包装器。
控制器有 12 个输入和 12 个输出。

包装器有两个任务:
1. 以恒定的时间间隔(即 50 毫秒)轮询控制器的输入。
2. 运行预先配置的序列,这会改变控制器的输出。

该序列是基于 XML 的:

<opcode>
  <register>0</register>
  <bit>1</bit>
  <duration>500</duration>
</opcode>
<opcode>
  <register>0</register>
  <bit>0</bit>
  <duration>0</duration>
</opcode>
....

在上面的示例中,控制器应打开输出#0 并在 500 毫秒后将其关闭。
操作之间的暂停是使用Thread.Sleep().
过去我只使用了一个 BackgroundWorker。当不运行序列时,它会进行轮询。

挑战:
对包装器的新需求是它可以在运行序列时检测控制器输入的变化。
我已将包装器修改为具有 2 个后台工作程序,一个用于轮询,另一个用于设置输出寄存器。
每个 BackgroundWorkers 在控制器上调用一个单独的函数,它们不会尝试访问彼此的数据,也不会共享任何数据。

当前代码:

private void workerSequence_DoWork(object sender, DoWorkEventArgs e)
{
    if (!terminating)
        if (e.Argument != null)
            DoSequence(e);
}

private void workerPoll_DoWork(object sender, DoWorkEventArgs e)
{
    if (!terminating)
    {
        DoPoll();
        Thread.Sleep(pollInterval);
    }
}

private void DoSequence(DoWorkEventArgs e)
{
    string sequenceName = e.Argument.ToString();

    foreach (configurationSequencesSequenceOpcode opcode in sequencesList[sequenceName])
    {
        if (workerSequence.CancellationPending)
            break;

        byte register = opcode.register;
        bool bit = opcode.bit;
        int duration = opcode.duration;

        SetRegister(register, bit, false);
        Thread.Sleep(duration);
    }

    e.Result = e.Argument;
}

问题:
这两个 BackgroundWorker 似乎相互干扰。我试过使用Semaphore,但是处理序列Monitor.Wait()ManualResetEvent.WaitOne()BackgroundWorker 不能很好地处理它们。主要问题 - 它的睡眠时间与以前不一致。

欢迎任何建议。

4

2 回答 2

1

在测试代​​码之外使用Thread.Sleep通常并不理想。

您可以将System.Threading.Timer对象用于这两个要求。


编辑如果要防止并发调用,可以使用锁来实现互斥。

创建两个计时器处理程序都可以看到的要锁定的对象:

public object gate = new object();

对于轮询,您要设置一个像这样的计时器:

var pollTimer = new Timer( HandlePoll, null, 0, Timeout.Infinite );
...

void HandlePoll( object state )
{
  lock ( gate )
  {
    DoPoll();
  }
  pollTimer.Change( pollInterval, Timeout.Infinite );
}

我已将时间段设置为,Timeout.Infinte因此计时器不会重复。这意味着一个轮询完成和另一个开始之间的时间 - 如果需要一些时间pollInterval,这将与轮询周期不同。DoPoll()

这样做还可以防止计时器在之前的轮询仍在运行时再次滴答作响。


您可以使用相同的主体来发送命令,但您必须将当前索引管理到sequencesList

var seqTimer = new Timer( HandleSequence, null, 0, Timeout.Infinate );
int seqIndex = 0;
...

void HandleSequence( object state )
{
  var opcode = sequencesList[sequenceName][ seqIndex ];

  byte register = opcode.register;
  bool bit = opcode.bit;
  int duration = opcode.duration;

  lock( gate )
  {
    SetRegister(register, bit, false);
  }

  seqIndex++;
  if ( seqIndex < sequencesList[sequenceName].Count )
  {
    seqTimer.Change( duration, Timeout.Infinte );
  }
}

或类似的东西(但有适当的错误处理!)

您只需处理计时器即可处理终止和取消。


顺便说一句,您可以阅读一本关于线程的优秀(免费)电子书:

Albahari: Threading in C#

于 2013-01-21T13:42:18.280 回答
0

将序列名称排队到等待 BlockingCollection 的一个线程怎么样?

伪,每个设备通道一个线程:

while(true)
{
    if outputQueue.TryTake(thisSeqName,50)
    {
        foreach (configurationSequencesSequenceOpcode opcode in sequencesList[thisSeqName])
        {
            outputOpcode(opcode);
            DoPoll();
        }
    }
    else
        DoPoll();
}

嗯..这将无法正常工作,因为在发送序列时它可能会过于频繁地轮询。需要重新考虑,但我仍然认为每个设备一个线程将是一种更好的方法。

int startTick=Environment.TickCount;
while(true){
    int now=Environment.TickCount;
    waitInterval=50-(now-startTick);
    if outputQueue.TryTake(thisSeqName,waitInterval)
    {
        foreach (configurationSequencesSequenceOpcode opcode in sequencesList[thisSeqName])
        {
            outputOpcode(opcode);
            int now=Environment.TickCount;
            waitInterval=50-(now-startTick);
            if (waitInterval<=0)
            {
                DoPoll();
                startTick=Environment.TickCount;
            }
        }
    }
    else
    {
        DoPoll();
        startTick=Environment.TickCount;
    }
}
于 2013-01-21T13:24:20.610 回答