Как разделить один и тот же контекст с разными потоками в шаблоне с несколькими командами в С#?

Существует расширенная реализация шаблона команды для поддержки нескольких команд (групп) в С#:

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); достаточно или мне нужно использовать блокировку или что-то еще?

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

Ответ 1

Предположим, что у нас есть класс 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 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.
  • Предполагается, что не требуется никакого состояния, основанного на рабочем потоке. Сообщение просто указывало, что куча задач должна выполняться только асинхронно, а не упорядоченной асинхронной.
  • Никакой валютный менеджер не нужен, потому что мы полагаемся на каждое закрытие/завершение команды асинхронной задачи, чтобы вернуть новый контекст в созданном потоке!

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

Ответ 2

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

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

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

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
}

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

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

Ответ 3

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