同步两个后台工作者

本文关键字:两个 后台 工作者 同步 | 更新日期: 2023-09-27 17:54:59

我正在写一个c#包装器,使用它的Modbus协议到硬件控制器。
控制器有12路输入,12路输出。

包装器有两个任务:
1. 以恒定的间隔(即50ms)轮询控制器的输入。
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打开,并在500ms后关闭。
操作间的暂停使用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;
}

问题:
这两个后台工作者好像在互相干扰。我试过使用Semaphore, Monitor.Wait()ManualResetEvent.WaitOne(),但处理序列的BackgroundWorker不能很好地处理它们。主要问题是——它的睡眠时间不像以前那样一致。

同步两个后台工作者

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

您可以使用System.Threading.Timer对象来满足这两个需求。


EDIT如果你想防止并发调用,你可以使用锁来实现互斥。

创建一个可以被两个定时器处理程序看到的对象:

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#

如何将序列名称排队到等待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;
    }
}