同步两个BackgroundWorkers

时间:2013-01-21 12:20:54

标签: c# multithreading thread-safety backgroundworker thread-sleep

我正在使用其Modbus协议向硬件控制器编写C#包装器 控制器有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;
}

问题:
看起来两个BackgroundWorkers相互干扰。我已尝试使用SemaphoreMonitor.Wait()ManualResetEvent.WaitOne(),但处理序列的BackgroundWorker不能很好地处理它们。主要问题 - 睡眠持续时间与以前不一致。

欢迎任何建议。

2 个答案:

答案 0 :(得分: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#

答案 1 :(得分: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;
    }
}