Как поделиться одним и тем же контекстом с разными потоками в шаблоне multi-Command в 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) объект.

план выполнения веб-приложения необходимо разделить commandGroup1 и commandGroup2 группы в разных потоках. В частности,commandGroup2 будет выполнен в новом потоке и commandGroup1 в главном нитка.

исполнение теперь выглядит так:

//In Main Thread
commandGroup1.Execute();

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

как я могу резьба-безопасно одинаковые context object (ctx), чтобы иметь возможность откатить commandGroup1 из Нового Потока ?

и t.Start(ctx); достаточно или мне нужно использовать замок или что-то еще?

пример реализации кода здесь

3 ответов


предположим, что у нас есть класс MultiCommand, который объединяет список ICommands и в какое-то время должен выполнять все команды асинхронно. Все команды должны совместно использовать контекст. Каждая команда может изменить состояние контекста, но нет установленного порядка!

первым шагом является запуск всех методов ICommand Execute, проходящих в CTX. Следующим шагом является настройка прослушивателя событий для новых изменений 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);
    }
}

дальнейшие абстракции возможны в функции execute каждой команды. Но сейчас мы не будем это обсуждать.

вот что этот дизайн делает и не делает:

  1. он позволяет любой законченной задаче обновить новый контекст в потоке, который был впервые установлен в классе MultiCommand.
  2. это предполагает, что нет рабочего государства необходимый. Сообщение просто указывало, что куча задач должна была выполняться асинхронно, а не упорядоченным асинхронным способом.
  3. currencymanager не требуется, потому что мы полагаемся на закрытие/завершение каждой команды асинхронной задачи для возврата нового контекста в поток, который был создан!

Если вам нужен параллелизм, это означает, что состояние контекста важно, что дизайн похож на этот, но отличается. Этот дизайн легко реализовано с использованием функций и обратных вызовов для закрытия.


пример кода, безусловно, оставляет большое количество вопросов о вашем конкретном случае, однако, я попытаюсь ответить общей стратегии для реализации подобной задачи для многопоточной среды.

изменяется ли контекст или его данные в связанном, неатмоическом виде?

например, любая из ваших команд сделает что-то вроде:

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

тогда абсолютно вам нужно будет использовать lock(...) операторы где-то в вашем коде. Вопрос в том, где.

каково поведение безопасности потоков ваших вложенных контроллеров?

в связанном примере кода GIST CommandContext класс имеет свойства ServerController и ServiceController. Если вы не являетесь владельцем этих классов, вы должны тщательно проверить документацию по потокобезопасности этих классов.

например, если ваши команды работают на двух разных потоки выполняют такие вызовы, как:

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
}

нет волшебной пули, когда и где делать блокировки, но в целом, чем выше в стеке вызовов вы их ставите, тем проще и безопаснее будет ваш код, вероятно, за счет производительности - так как оба потока больше не могут выполняться одновременно. Чем дальше вы их размещаете, тем более параллельным будет ваш код, но и больше расходов.

в сторону: почти нет штрафа за фактическое взятие и освобождение замка, поэтому не нужно беспокоиться об этом.


пока каждый контекст используется только из одного потока одновременно, нет проблем с его использованием из нескольких потоков.