我正在使用线程构建管道循环。周期是Fetch,Decode,Execute和Write Back。我编写了一个GUI按钮,触发每个循环。我遇到的问题是当我希望它以特定顺序(F-> D-> X-> W)时输出全部混乱。另外,我希望这些线程同时运行,这意味着我不能使用带监视器的锁。根据我对监视器和锁的了解,他们一次运行一个线程,这对我不起作用。以下是我的代码的简单概述:
public void nextInstruction()
{
// These all trigger at once
// and run in parallel
fetchEvent.Set();
decodeEvent.Set();
executeEvent.Set();
wbEvent.Set();
}
public void fetch()
{
fetchEvent.waitone();
// do stuff...
}
public void decode()
{
decodEvent.waitone();
// do stuff...
}
public void execute()
{
exeEvent.waitone();
// do stuff...
}
public void writeBack()
{
wbEvent.waitone();
// do stuff...
}
和当前输出:
F D X W
F lda $50
F sta $2
D lda #$10
F lda #$0
D sta $1
X sta $0
D lda $50
W sta $0
X lda #$10
F sta $0
F lda #$10
D lda $50
X sta $1
W sta $0
F sta $1
W sta $0
X lda $50
D sta $2
D lda #$0
X lda $50
W sta $0
我想要的输出:
F D X W
F lda $10
F sta $0
D lda #$10
F lda #$11
D sta $0
X lda #$10
F sta $0
D lda $11
X sta $0
W lda #$10
F lda #$10
D sta $0
X lda $11
W sta $0
F sta $1
D lda #$10
X sta $0
W lda $11
D sta $1
X lda #$10
W sta $0
我的格式是这样的。让我更容易看到订购。同样,这些打印语句中的每一个--F,D,X,W - 均由不同的线程触发。有关如何实现这一目标的任何意见?
我也愿意使用锁,如果有办法使用它我不知道。
答案 0 :(得分:1)
原因是您一次设置所有事件。
一旦主线程被唤醒,它们被Windows调度的执行顺序是未定义的,并且最后一个线程可能比第一个线程更早开始。
查看生产者 - 消费者集合,例如BlockingCollection:http://msdn.microsoft.com/en-us/library/dd267312(v=vs.110).aspx
这些将允许您同时运行所有四个,消耗前一个产品并将输出发送到下一个产品。
您将问题描述为由每个输入元素触发的一系列顺序步骤。在处理块之间,您可能有一些其他类型的对象和不同数量的对象,但这仍然是生产者 - 消费者模式。
在这种情况下,生产者 - 消费者将应用于所有后续处理器对。他们正在积极运行,等待前任的输入并将输出发送给后继者。一旦你开始考虑如何管理资源来存储这些临时输出,你自然会想到排队。
绘制流程图,为每个数据流链接创建一个BlockingCollection,并在构造函数中为其提供一些队列以确保排序(如果需要)。另外,指定每个项目数量的限制,这将是您的“缓冲区大小”。消费者将使用“GetConsumingEnumerable”方法并在其上应用foreach - 当生产者队列中没有数据时,它将自动阻止。
完成后,重新检查每个生产者 - 消费者对,以确保它们平均以相同的速度运行。如果任何消费者的运行速度明显加快,请考虑将其代码合并到前任生产者中,因为在该链接上排队是无用的。
答案 1 :(得分:1)
以这种方式试试,
public void Button_OnClick() {
nextInstruction();
}
public void nextInstruction()
{
fetchEvent.Set();
}
public void fetch()
{
while(true) {
fetchEvent.waitone();
// do stuff...
decodeEvent.Set();
}
}
public void decode()
{
while(true) {
decodEvent.waitone();
// do stuff...
executeEvent.Set();
}
}
答案 2 :(得分:1)
注意!以下内容仅适用于.NET 4或更高版本。旧的.NET版本中既没有 Parallel 也没有 ConcurrentDictionary 。
使用Parallel.Invoke(),您可以同时执行您的操作,同时在所有操作完成之前不会返回此方法的优势。
使用类似集合或字典的数据类型,其中操作可以存储其(输出)结果,以便以后进行顺序处理。
在下面的示例代码中,我使用了ConcurrentDictionary。 ConcurrentDictionary的键是动作本身。如果要在其他地方处理此字典,将操作本身用作键是不明智的。在这种情况下,您应该实现一个公共枚举(表示操作)作为字典的键。
由于动作同时运行并且可能在同一时间访问字典,因此选择了线程安全的ConcurrentDictionary类型。 (普通字典不是线程安全的,因此可能会导致零星的,看似随机的错误。)
public class InstructionCycles
{
private readonly ConcurrentDictionary<Action, string> _dictActionResults = new ConcurrentDictionary<Action, string>();
private void fetch()
{
// do something and store the result in the dictionary
_dictActionResults[fetch] = "FetchResult";
}
private void decode()
{
// do something and store the result in the dictionary
_dictActionResults[decode] = "DecodeResult";
}
private void execute()
{
// do something and store the result in the dictionary
_dictActionResults[execute] = "ExecuteResult";
}
private void writeBack()
{
// do something and store the result in the dictionary
_dictActionResults[writeBack] = "WriteBackResult";
}
public static void nextInstruction()
{
InstructionCycles instrCycles = new InstructionCycles();
Action[] actions =
{
instrCycles.fetch,
instrCycles.decode,
instrCycles.execute,
instrCycles.writeBack
};
Parallel.Invoke(actions);
// output the results in sequential order
foreach (Action a in actions)
{
Console.Out.WriteLine(instrCycles._dictActionResults[a]);
}
}
}
通过调用InstructionCycles.nextInstruction()
执行指令。
使用静态方法nextInstruction()(内部创建 InstructionCycles 的实例)允许在需要时并行运行多个指令,因为每条指令都使用自己的结果字典而不会干扰其他指令
如果不需要静态方法,并且也不要求并行执行指令,那么 nextInstruction()可以改为:
private readonly object _lockObj = new object();
public void nextInstruction()
{
Action[] actions =
{
fetch,
decode,
execute,
writeBack
};
lock (_lockObj)
{
_dictActionResults.Clear();
Parallel.Invoke(actions);
// output the results in sequential order
foreach (Action a in actions)
{
Console.Out.WriteLine(_dictActionResults[a]);
}
}
}
请注意lock
声明。如果在任何情况下调用 instructionNext()而另一个线程已在执行 instructionNext(),则锁将阻止第二个线程的执行,直到第一个线程完成< EM> instructionNext()的。作为锁定对象,应该选择一个不能从类外部访问的私有对象,这将避免许多潜在的死锁情况。
答案 3 :(得分:1)
这是演示如何实现类似于我在第一个答案中给出的Parallel.Invoke()
示例,但这次仅使用 AutoResetEvent 。
( ManualResetEvent 对象代替 AutoResetEvent ,但是在 nextInstruction()的情况下,代码需要注意重置这些对象应该再次调用em>方法。)
每个指令周期任务都会休眠一段任意时间(“模拟”不同的执行时间),以便演示AutoResetEvents确保输出结果的正确顺序的效果。
在等待输出结果的权限之前,所有周期将同时运行。 只有在相应的AutoResetEvent上调用 WaitOne()方法(等待输出结果的权限),其余任务(输出结果)才会按顺序执行。
public class InstructionCycles
{
private readonly AutoResetEvent DecodeAllowedToOutputEvent = new AutoResetEvent(false);
private readonly AutoResetEvent ExecuteAllowedToOutputEvent = new AutoResetEvent(false);
private readonly AutoResetEvent WriteBackAllowedToOutputEvent = new AutoResetEvent(false);
//
// The InstructionFinishedEvent would not be necessary,
// if nextInstruction() does not need to wait for the instruction to finish.
//
AutoResetEvent InstructionFinishedEvent = new AutoResetEvent(false);
private void fetch()
{
try
{
// do something useful...
// For demo purpose, lets just sleep some arbitrary time
Thread.Sleep(500);
// This is the 1st cycle.
// So we don't need to wait for a previous cycle outputting its result.
Console.Out.WriteLine("FetchResult");
}
finally
{
// Allow the next cycle to output its results...
DecodeAllowedToOutputEvent.Set();
}
}
private void decode()
{
try
{
// do something useful...
// For demo purpose, lets just sleep some arbitrary time
Thread.Sleep(200);
// Processing done.
// Now wait to be allowed to output the result.
DecodeAllowedToOutputEvent.WaitOne();
Console.Out.WriteLine("DecodeResult");
}
finally
{
// Allow the next cycle to output its results...
ExecuteAllowedToOutputEvent.Set();
}
}
private void execute()
{
try
{
// do something useful...
// For demo purpose, lets just sleep some arbitrary time
Thread.Sleep(300);
// Processing done.
// Now wait to be allowed to output the result.
ExecuteAllowedToOutputEvent.WaitOne();
Console.Out.WriteLine("ExecuteResult");
}
finally
{
// Allow the next cycle to output its results...
WriteBackAllowedToOutputEvent.Set();
}
}
private void writeBack()
{
try
{
// do something useful...
// For demo purpose, lets just sleep some arbitrary time
Thread.Sleep(100);
// Processing done.
// Now wait to be allowed to output the result.
WriteBackAllowedToOutputEvent.WaitOne();
Console.Out.WriteLine("WriteBackResult");
}
finally
{
// Signal that the instruction (including outputting the result) has finished....
InstructionFinishedEvent.Set();
}
}
public void nextInstruction()
{
//
// The order in which the cycles are started doesn't really matter,
// since the way how the AutoResetEvents are being used will ensure
// correct sequence of outputting results.
//
Task.Factory.StartNew(fetch);
Task.Factory.StartNew(decode);
Task.Factory.StartNew(execute);
Task.Factory.StartNew(writeBack);
//
// The InstructionFinishedEvent would not be necessary,
// if nextInstruction() does not need to wait for the instruction to finish.
//
InstructionFinishedEvent.WaitOne();
}
}
通过利用try {...} finally {...}
,即使其中一个线程中的某些代码决定抛出异常,也确保事件链不受阻碍地工作。
即使您敢于故意处置一个或多个AutoResetEvents然后调用 nextInstruction(),指令周期的执行仍将同时发生。但是,结果的输出将不再按预期发生,因为当其中一个循环尝试等待已处置且现在无效的AutoResetEvent时,将抛出异常,并且将向下一个线程发出输出其结果的权限(感谢到try-finally块的工作方式。)
注意:乍一看,代码可能与好奇的答案类似。但是,程序流/事件处理方面存在差异,这些差异对整个流程的行为很重要。
另请注意,上面给出的 nextInstruction()本身并不是线程安全的。要允许来自不同调用线程的多个 nextInstruction()调用,就像在我的其他答案中一样,必须使用lock
来确保一次只执行一条指令:
private readonly object _lockObj = new object();
public void nextInstruction()
{
//
// The order in which the cycles are started doesn't really matter,
// since the way how the AutoResetEvents are being used will ensure
// correct sequence of outputting results.
//
lock (_lockObj)
{
Task.Factory.StartNew(fetch);
Task.Factory.StartNew(decode);
Task.Factory.StartNew(execute);
Task.Factory.StartNew(writeBack);
//
// When using lock, InstructionFinishedEvent must
// be used to ensure that nextInstance() remains
// in the lock until the instruction finishes.
//
InstructionFinishedEvent.WaitOne();
}
}