如何在 C# 中的多命令模式中与不同线程共享相同的上下文?
How to share the same context with different threads in multi-Command Pattern in C#?
命令模式 的扩展实现支持 C# 中的多命令(组):
var ctx= //the context object I am sharing...
var commandGroup1 = new MultiItemCommand(ctx, new List<ICommand>
{
new Command1(ctx),
new Command2(ctx)
});
var commandGroup2 = new MultiItemCommand(ctx, new List<ICommand>
{
new Command3(ctx),
new Command4(ctx)
});
var groups = new MultiCommand(new List<ICommand>
{
commandGroup1 ,
commandGroup2
}, null);
现在,执行是这样的:
groups.Execute();
我正在共享相同的 上下文 (ctx) 对象。
web应用的执行计划需要分开
commandGroup1
和 commandGroup2
组在不同的线程中。具体来说,commandGroup2
会在新线程中执行,commandGroup1
会在主线程中执行。
执行现在看起来像:
//In Main Thread
commandGroup1.Execute();
//In the new Thread
commandGroup2.Execute();
如何线程安全共享同一个context object (ctx)
,以便能够从新线程回滚commandGroup1
?
t.Start(ctx);
够了吗,还是我必须使用锁或其他东西?
一些代码实现示例是here
只要每个上下文仅从单个线程同时使用,从多个线程使用它就没有问题。
所提供的示例代码肯定会留下大量关于您的特定用例的问题;但是,我将尝试回答在多线程环境中实现此类问题的一般策略。
上下文或其数据是否以耦合的、非大气的方式被修改?
例如,您的任何命令都会执行以下操作:
Context.Data.Item1 = "Hello"; // Setting both values is required, only
Context.Data.Item2 = "World"; // setting one would result in invalid state
那么您绝对需要在代码中的某处使用 lock(...)
语句。问题是在哪里。
嵌套控制器的线程安全行为是什么?
在链接的 GIST 示例代码中,CommandContext
class 具有属性 ServerController
和 ServiceController
。如果您不是这些 classes 的所有者,那么您还必须仔细检查有关这些 classes 的线程安全性的文档。
例如,如果您的命令 运行 在两个不同的线程上执行调用,例如:
Context.ServiceController.Commit(); // On thread A
Context.ServiceController.Rollback(); // On thread B
如果控制器的创建者 class 不希望使用多线程,则很有可能无法同时调用这两个操作。
何时锁定以及锁定什么
每当您需要执行必须完全发生或根本不发生的多个操作时,或者在调用不需要并发访问的长运行 操作时获取锁。尽快释放锁。
此外,只能对只读或常量属性或字段进行锁定。所以在你做类似的事情之前:
lock(Context.Data)
{
// Manipulate data sub-properties here
}
请记住,可以换出 Data
指向的对象。最安全的实现是提供一个特殊的锁定对象:
internal readonly object dataSyncRoot = new object();
internal readonly object serviceSyncRoot = new object();
internal readonly object serverSyncRoot = new object();
对于需要独占访问和使用的每个子对象:
lock(Context.dataSyncRoot)
{
// Manipulate data sub-properties here
}
没有关于何时何地进行锁定的灵丹妙药,但一般来说,将它们放在调用堆栈的较高位置,您的代码可能会更简单、更安全,但会牺牲性能 -因为两个线程不能再同时执行。放置得越靠下,代码的并发性就越高,但开销也越大。
旁白:实际获取和释放锁几乎没有性能损失,因此无需担心。
假设我们有一个 MultiCommand class,它聚合了一个 ICommand 列表,并且有时必须异步执行所有命令。所有命令必须共享上下文。每个命令都可以更改上下文状态,但没有固定顺序!
第一步是启动所有传入 CTX 的 ICommand Execute 方法。下一步是为新的 CTX 更改设置事件侦听器。
public class MultiCommand
{
private System.Collections.Generic.List<ICommand> list;
public List<ICommand> Commands { get { return list; } }
public CommandContext SharedContext { get; set; }
public MultiCommand() { }
public MultiCommand(System.Collections.Generic.List<ICommand> list)
{
this.list = list;
//Hook up listener for new Command CTX from other tasks
XEvents.CommandCTX += OnCommandCTX;
}
private void OnCommandCTX(object sender, CommandContext e)
{
//Some other task finished, update SharedContext
SharedContext = e;
}
public MultiCommand Add(ICommand cc)
{
list.Add(cc);
return this;
}
internal void Execute()
{
list.ForEach(cmd =>
{
cmd.Execute(SharedContext);
});
}
public static MultiCommand New()
{
return new MultiCommand();
}
}
每个命令处理异步部分类似于:
internal class Command1 : ICommand
{
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
throw new NotImplementedException();
}
public async void Execute(object parameter)
{
var ctx = (CommandContext)parameter;
var newCTX = await Task<CommandContext>.Run(() => {
//the command context is here running in it's own independent Task
//Any changes here are only known here, unless we return the changes using a 'closure'
//the closure is this code - var newCTX = await Task<CommandContext>Run
//newCTX is said to be 'closing' over the task results
ctx.Data = GetNewData();
return ctx;
});
newCTX.NotifyNewCommmandContext();
}
private RequiredData GetNewData()
{
throw new NotImplementedException();
}
}
最后我们建立了一个通用的事件处理程序和通知系统。
public static class XEvents
{
public static EventHandler<CommandContext> CommandCTX { get; set; }
public static void NotifyNewCommmandContext(this CommandContext ctx, [CallerMemberName] string caller = "")
{
if (CommandCTX != null) CommandCTX(caller, ctx);
}
}
在每个命令的执行函数中可以进行进一步的抽象。但我们现在不讨论这个。
以下是此设计的作用和不作用:
- 它允许任何已完成的任务更新它最初在 MultiCommand 中设置的线程上的新上下文 class。
- 这假定不需要基于工作流的状态。 post 仅仅表示一堆任务只需要 运行 异步而不是有序的异步方式。
- 不需要 currencymanager,因为我们依赖异步任务的每个命令 closure/completion 到 return 它创建的线程上的新上下文!
如果您需要并发,那么这意味着上下文状态很重要,该设计与本设计相似但不同。使用闭包的函数和回调很容易实现该设计。
命令模式 的扩展实现支持 C# 中的多命令(组):
var ctx= //the context object I am sharing...
var commandGroup1 = new MultiItemCommand(ctx, new List<ICommand>
{
new Command1(ctx),
new Command2(ctx)
});
var commandGroup2 = new MultiItemCommand(ctx, new List<ICommand>
{
new Command3(ctx),
new Command4(ctx)
});
var groups = new MultiCommand(new List<ICommand>
{
commandGroup1 ,
commandGroup2
}, null);
现在,执行是这样的:
groups.Execute();
我正在共享相同的 上下文 (ctx) 对象。
web应用的执行计划需要分开
commandGroup1
和 commandGroup2
组在不同的线程中。具体来说,commandGroup2
会在新线程中执行,commandGroup1
会在主线程中执行。
执行现在看起来像:
//In Main Thread
commandGroup1.Execute();
//In the new Thread
commandGroup2.Execute();
如何线程安全共享同一个context object (ctx)
,以便能够从新线程回滚commandGroup1
?
t.Start(ctx);
够了吗,还是我必须使用锁或其他东西?
一些代码实现示例是here
只要每个上下文仅从单个线程同时使用,从多个线程使用它就没有问题。
所提供的示例代码肯定会留下大量关于您的特定用例的问题;但是,我将尝试回答在多线程环境中实现此类问题的一般策略。
上下文或其数据是否以耦合的、非大气的方式被修改?
例如,您的任何命令都会执行以下操作:
Context.Data.Item1 = "Hello"; // Setting both values is required, only
Context.Data.Item2 = "World"; // setting one would result in invalid state
那么您绝对需要在代码中的某处使用 lock(...)
语句。问题是在哪里。
嵌套控制器的线程安全行为是什么?
在链接的 GIST 示例代码中,CommandContext
class 具有属性 ServerController
和 ServiceController
。如果您不是这些 classes 的所有者,那么您还必须仔细检查有关这些 classes 的线程安全性的文档。
例如,如果您的命令 运行 在两个不同的线程上执行调用,例如:
Context.ServiceController.Commit(); // On thread A
Context.ServiceController.Rollback(); // On thread B
如果控制器的创建者 class 不希望使用多线程,则很有可能无法同时调用这两个操作。
何时锁定以及锁定什么
每当您需要执行必须完全发生或根本不发生的多个操作时,或者在调用不需要并发访问的长运行 操作时获取锁。尽快释放锁。
此外,只能对只读或常量属性或字段进行锁定。所以在你做类似的事情之前:
lock(Context.Data)
{
// Manipulate data sub-properties here
}
请记住,可以换出 Data
指向的对象。最安全的实现是提供一个特殊的锁定对象:
internal readonly object dataSyncRoot = new object();
internal readonly object serviceSyncRoot = new object();
internal readonly object serverSyncRoot = new object();
对于需要独占访问和使用的每个子对象:
lock(Context.dataSyncRoot)
{
// Manipulate data sub-properties here
}
没有关于何时何地进行锁定的灵丹妙药,但一般来说,将它们放在调用堆栈的较高位置,您的代码可能会更简单、更安全,但会牺牲性能 -因为两个线程不能再同时执行。放置得越靠下,代码的并发性就越高,但开销也越大。
旁白:实际获取和释放锁几乎没有性能损失,因此无需担心。
假设我们有一个 MultiCommand class,它聚合了一个 ICommand 列表,并且有时必须异步执行所有命令。所有命令必须共享上下文。每个命令都可以更改上下文状态,但没有固定顺序!
第一步是启动所有传入 CTX 的 ICommand Execute 方法。下一步是为新的 CTX 更改设置事件侦听器。
public class MultiCommand
{
private System.Collections.Generic.List<ICommand> list;
public List<ICommand> Commands { get { return list; } }
public CommandContext SharedContext { get; set; }
public MultiCommand() { }
public MultiCommand(System.Collections.Generic.List<ICommand> list)
{
this.list = list;
//Hook up listener for new Command CTX from other tasks
XEvents.CommandCTX += OnCommandCTX;
}
private void OnCommandCTX(object sender, CommandContext e)
{
//Some other task finished, update SharedContext
SharedContext = e;
}
public MultiCommand Add(ICommand cc)
{
list.Add(cc);
return this;
}
internal void Execute()
{
list.ForEach(cmd =>
{
cmd.Execute(SharedContext);
});
}
public static MultiCommand New()
{
return new MultiCommand();
}
}
每个命令处理异步部分类似于:
internal class Command1 : ICommand
{
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
throw new NotImplementedException();
}
public async void Execute(object parameter)
{
var ctx = (CommandContext)parameter;
var newCTX = await Task<CommandContext>.Run(() => {
//the command context is here running in it's own independent Task
//Any changes here are only known here, unless we return the changes using a 'closure'
//the closure is this code - var newCTX = await Task<CommandContext>Run
//newCTX is said to be 'closing' over the task results
ctx.Data = GetNewData();
return ctx;
});
newCTX.NotifyNewCommmandContext();
}
private RequiredData GetNewData()
{
throw new NotImplementedException();
}
}
最后我们建立了一个通用的事件处理程序和通知系统。
public static class XEvents
{
public static EventHandler<CommandContext> CommandCTX { get; set; }
public static void NotifyNewCommmandContext(this CommandContext ctx, [CallerMemberName] string caller = "")
{
if (CommandCTX != null) CommandCTX(caller, ctx);
}
}
在每个命令的执行函数中可以进行进一步的抽象。但我们现在不讨论这个。
以下是此设计的作用和不作用:
- 它允许任何已完成的任务更新它最初在 MultiCommand 中设置的线程上的新上下文 class。
- 这假定不需要基于工作流的状态。 post 仅仅表示一堆任务只需要 运行 异步而不是有序的异步方式。
- 不需要 currencymanager,因为我们依赖异步任务的每个命令 closure/completion 到 return 它创建的线程上的新上下文!
如果您需要并发,那么这意味着上下文状态很重要,该设计与本设计相似但不同。使用闭包的函数和回调很容易实现该设计。