【问题标题】:How to share the same context with different threads in multi-Command Pattern in C#?如何在 C# 中的多命令模式中与不同线程共享相同的上下文?
【发布时间】:2016-10-19 18:54:30
【问题描述】:

命令模式有一个扩展的实现来支持 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();

我正在共享同一个 context (ctx) 对象。

web应用的执行计划需要分开 commandGroup1commandGroup2 组在不同的线程中。具体来说,commandGroup2 将在新线程中执行,commandGroup1 在主线程中执行。

现在的执行如下:

//In Main Thread
commandGroup1.Execute();

//In the new Thread
commandGroup2.Execute();

我如何线程安全共享相同的context object (ctx),以便能够从新线程回滚commandGroup1

t.Start(ctx); 够用还是我必须使用锁之类的?

部分代码实现示例为here

【问题讨论】:

  • 这取决于 ctx 在命令中的使用方式。如果它同时使用(即两个线程可以同时访问它) - 例如,您可以在两个命令中锁定 ctx 变量本身。一般来说,您的问题不是很清楚,如果您提供如何使用这些多命令的具体示例,可能会更好。
  • 你查看并发包了吗?

标签: c# multithreading design-patterns command-pattern


【解决方案1】:

提供的示例代码肯定会留下大量关于您的特定用例的问题;但是,我将尝试回答在多线程环境中实现此类问题的一般策略。

上下文或其数据是否以耦合、非大气的方式被修改?

例如,您的任何命令是否会执行以下操作:

Context.Data.Item1 = "Hello"; // Setting both values is required, only
Context.Data.Item2 = "World"; // setting one would result in invalid state

那么您绝对需要在代码中的某处使用lock(...) 语句。问题是在哪里。

嵌套控制器的线程安全行为是什么?

在链接的 GIST 示例代码中,CommandContext 类具有属性 ServerControllerServiceController。如果您不是这些类的所有者,那么您还必须仔细检查有关这些类的线程安全性的文档。

例如,如果您在两个不同线程上运行的命令执行如下调用:

Context.ServiceController.Commit();   // On thread A

Context.ServiceController.Rollback(); // On thread B

如果控制器类的创建者不期望多线程使用,这两个操作很可能无法同时调用。

何时锁定以及锁定什么

当您需要执行多个必须完全发生或根本不发生的操作时,或者在调用不期望并发访问的长时间运行的操作时,请使用锁。尽快释放锁。

此外,只能对只读或常量属性或字段进行锁定。所以在你做类似的事情之前:

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
}

关于何时何地执行锁定并没有灵丹妙药,但一般来说,您将它们放在调用堆栈中的位置越高,您的代码可能会越简单和安全,但会牺牲性能 - 因为两者线程不能再同时执行。你把它们放得越远,你的代码就会越并发​​,但费用也就越多。

另外:实际获取和释放锁几乎没有性能损失,因此无需担心。

【讨论】:

    【解决方案2】:

    假设我们有一个 MultiCommand 类,它聚合了一个 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 类中首次设置的线程上的新上下文。
    2. 这假设没有必要的基于工作流的状态。该帖子仅表明一堆任务只需要异步运行,而不是以有序的异步方式运行。
    3. 没有货币管理器是必需的,因为我们依靠每个命令的异步任务的关闭/完成来返回它创建的线程上的新上下文!

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

    【讨论】:

      【解决方案3】:

      只要每个上下文仅在单个线程中同时使用,在多个线程中使用它就没有问题。

      【讨论】:

      • 你能举例说明如何在线程之间共享这个上下文对象吗?只需使用 t.Start(ctx); ?我需要使用 lock 吗? @usr
      • 您需要确保没有并发使用。做任何你喜欢的方式。例如Task.Run().Wait(); Task.Run().Wait(); 可能使用两个线程但不是并发的。如果适合您的架构,您也可以锁定。线程之间的共享是容易的部分。您只需要将对象 ref 传递给周围的上下文。
      • 你能帮我举一个关于如何共享这个上下文对象的例子吗?该对象对象正在通过所有命令的执行过程进行更新,并且每个命令都有一个需要上下文对象的回滚操作。谢谢@usr
      • 我很不确定您需要什么以及您的设计是什么。只要您可以从命令中访问上下文,您就已经成功地共享了对象。它可能比你想象的要简单。
      猜你喜欢
      • 2012-12-04
      • 2019-05-28
      • 2012-08-11
      • 2016-12-17
      • 2021-10-08
      • 2017-09-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多