我需要构建一个应用程序,其中一些对象的实例同时生成“脉冲”。 (基本上这只是意味着他们正在递增一个计数器。)我还需要跟踪每个对象的总计数器。此外,每当我在计数器上执行读操作时,都需要将其重置为零。
所以我正在和一个正在工作的人交谈,他提到了Retlang和基于消息的并发,这听起来非常有趣。但显然我对这个概念很新。所以我已经构建了一个小型原型,我得到了预期的结果,这很棒 - 但我不确定我是否有可能犯了一些逻辑错误并让软件对错误开放,因为我缺乏Retlang和一般的并发编程。
首先,我有这些课程:
public class Plc {
private readonly IChannel<Pulse> _channel;
private readonly IFiber _fiber;
private readonly int _pulseInterval;
private readonly int _plcId;
public Plc(IChannel<Pulse> channel, int plcId, int pulseInterval) {
_channel = channel;
_pulseInterval = pulseInterval;
_fiber = new PoolFiber();
_plcId = plcId;
}
public void Start() {
_fiber.Start();
// Not sure if it's safe to pass in a delegate which will run in an infinite loop...
// AND use a shared channel object...
_fiber.Enqueue(() => {
SendPulse();
});
}
private void SendPulse() {
while (true) {
// Not sure if it's safe to use the same channel object in different
// IFibers...
_channel.Publish(new Pulse() { PlcId = _plcId });
Thread.Sleep(_pulseInterval);
}
}
}
public class Pulse {
public int PlcId { get; set; }
}
这里的想法是我可以实例化多个Plc,将每个Plc传递给同一个IChannel,然后让它们同时执行SendPulse功能,这样每个Plc都可以发布到同一个通道。但正如你从我的评论中看到的那样,我有点怀疑我所做的事实际上是合法的。我主要担心在不同的IFibers上下文中使用相同的IChannel对象发布,但我也担心永远不会从传递给Enqueue的委托中返回。我希望有人可以提供一些关于如何处理这个问题的见解。
此外,这是“订阅者”类:
public class PulseReceiver {
private int[] _pulseTotals;
private readonly IFiber _fiber;
private readonly IChannel<Pulse> _channel;
private object _pulseTotalsLock;
public PulseReceiver(IChannel<Pulse> channel, int numberOfPlcs) {
_pulseTotals = new int[numberOfPlcs];
_channel = channel;
_fiber = new PoolFiber();
_pulseTotalsLock = new object();
}
public void Start() {
_fiber.Start();
_channel.Subscribe(_fiber, this.UpdatePulseTotals);
}
private void UpdatePulseTotals(Pulse pulse) {
// This occurs in the execution context of the IFiber.
// If we were just dealing with the the published Pulses from the channel, I think
// we wouldn't need the lock, since I THINK the published messages would be taken
// from a queue (i.e. each Plc is publishing concurrently, but Retlang enqueues
// the messages).
lock(_pulseTotalsLock) {
_pulseTotals[pulse.PlcId - 1]++;
}
}
public int GetTotalForPlc(int plcId) {
// However, this access takes place in the application thread, not in the IFiber,
// and I think there could potentially be a race condition here. I.e. the array
// is being updated from the IFiber, but I think I'm reading from it and resetting values
// concurrently in a different thread.
lock(_pulseTotalsLock) {
if (plcId <= _pulseTotals.Length) {
int currentTotal = _pulseTotals[plcId - 1];
_pulseTotals[plcId - 1] = 0;
return currentTotal;
}
}
return -1;
}
}
所以在这里,我正在重用与Plc实例相同的IChannel,但是有一个不同的IFiber订阅它。理想情况下,我可以从每个Plc接收消息,并更新我的类中的单个私有字段,但是以线程安全的方式。
根据我的理解(我在评论中提到),我认为只更新我给订阅功能的委托中的_pulseTotals数组是安全的,因为我会连续收到来自Plcs的每条消息
但是,我不确定如何最好地处理我需要读取总数并重置它们的位。从代码和注释中可以看出,我最终围绕对_pulseTotals数组的任何访问进行了锁定。但我不确定这是否必要,我很想知道a)实际上是否有必要这样做,为什么,或b)实现类似的正确方法。
最后,为了更好的衡量,这是我的主要功能:
static void Main(string[] args) {
Channel<Pulse> pulseChannel = new Channel<Pulse>();
PulseReceiver pulseReceiver = new PulseReceiver(pulseChannel, 3);
pulseReceiver.Start();
List<Plc> plcs = new List<Plc>() {
new Plc(pulseChannel, 1, 500),
new Plc(pulseChannel, 2, 250),
new Plc(pulseChannel, 3, 1000)
};
plcs.ForEach(plc => plc.Start());
while (true) {
Thread.Sleep(10000);
Console.WriteLine(string.Format("Plc 1: {0}\nPlc 2: {1}\nPlc 3: {2}\n", pulseReceiver.GetTotalForPlc(1), pulseReceiver.GetTotalForPlc(2), pulseReceiver.GetTotalForPlc(3)));
}
}
我实例化一个单独的IChannel,将其传递给所有内容,Receiver在内部订阅IFiber,并且Plc使用IFibers“排队”一个不断发布到频道的非返回方法。
同样,控制台输出看起来与我期望它看起来完全一样,即在等待10秒后我看到Plc 1的20“脉冲”。并且在读取之后计数器的重置似乎也起作用,即Plc 1在每10秒增量之后具有20“脉冲”。但这并不能让我放心,我并没有忽视一些重要的事情。
我很高兴能够学习更多关于Retlang和并发编程技术的知识,所以希望有人有时间筛选我的代码并针对我的具体问题提供一些建议,或者根据我的要求提供不同的设计!