如何在 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应用的执行计划需要分开 commandGroup1commandGroup2 组在不同的线程中。具体来说,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 具有属性 ServerControllerServiceController。如果您不是这些 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);
    }
}

在每个命令的执行函数中可以进行进一步的抽象。但我们现在不讨论这个。

以下是此设计的作用和不作用:

  1. 它允许任何已完成的任务更新它最初在 MultiCommand 中设置的线程上的新上下文 class。
  2. 这假定不需要基于工作流的状态。 post 仅仅表示一堆任务只需要 运行 异步而不是有序的异步方式。
  3. 不需要 currencymanager,因为我们依赖异步任务的每个命令 closure/completion 到 return 它创建的线程上的新上下文!

如果您需要并发,那么这意味着上下文状态很重要,该设计与本设计相似但不同。使用闭包的函数和回调很容易实现该设计。