How to share the same context with different threads in multi-Command Pattern in C#?
Asked Answered
G

3

10

There is an extended implementation of command pattern to support multi-commands (groups) in 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);

Now , the execution is like:

groups.Execute();

I am sharing the same context (ctx) object.

The execution plan of the web app needs to separate commandGroup1 and commandGroup2 groups in different thread. In specific, commandGroup2 will be executed in a new thread and commandGroup1 in the main thread.

Execution now looks like:

//In Main Thread
commandGroup1.Execute();

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

How can I thread-safely share the same context object (ctx), so as to be able to rollback the commandGroup1 from the new Thread ?

Is t.Start(ctx); enough or do I have to use lock or something?

Some code implementation example is here

Gastrotrich answered 17/6, 2016 at 20:33 Comment(2)
It depends on how that ctx is used in commands. If it is used concurrently (i.e. both threads can access it at the same time) - you can for example lock ctx variable itself in both commands. In general you question is not very clear, maybe will be better if you provide concrete example of how you use those multicommands.Nieshanieto
Have you looked into the concurrent bag?Purehearted
P
1

Assume we have a MultiCommand class that aggregates a list of ICommands and at some time must execute all commands Asynchronously. All Commands must share context. Each command could change context state, but there is no set order!

The first step is to kick off all ICommand Execute methods passing in the CTX. The next step is to set up an event listener for new CTX Changes.

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();
    }
}

Each command handles the asynchronous part similar to this:

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();
    }
}

Finally we set up a common event handler and notification system.

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);
    }
}

Further abstractions are possible in each Command's execute function. But we won't discuss that now.

Here's what this design does and doesn't do:

  1. It allows any finished task to update the new context on the thread it was first set in the MultiCommand class.
  2. This assumes there is no workflow based state necessary. The post merely indicated a bunch of task only had to run asynchronous rather than in an ordered asynchronous manner.
  3. No currencymanager is necessary because we are relying on each command's closure/completion of the asynchronous task to return the new context on the thread it was created!

If you need concurrency then that implies that the context state is important, that design is similar to this one but different. That design is easily implemented using functions and callbacks for the closure.

Purehearted answered 26/6, 2016 at 2:39 Comment(0)
S
2

The provided sample code certainly leaves open a large number of questions about your particular use-case; however, I will attempt to answer the general strategy to implementing this type of problem for a multi-threaded environment.

Does the context or its data get modified in a coupled, non-atmoic way?

For example, would any of your commands do something like:

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

Then absolutely you would need to utilize lock(...) statements somewhere in your code. The question is where.

What is the thread-safety behavior of your nested controllers?

In the linked GIST sample code, the CommandContext class has properties ServerController and ServiceController. If you are not the owner of these classes, then you must carefully check the documentation on the thread-safety of of these classes as well.

For example, if your commands running on two different threads perform calls such as:

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

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

There is a strong possibility that these two actions cannot be invoked concurrently if the creator of the controller class was not expecting multi-threaded usage.

When to lock and what to lock on

Take the lock whenever you need to perform multiple actions that must happen completely or not at all, or when invoking long-running operations that do not expect concurrent access. Release the lock as soon as possible.

Also, locks should only be taken on read-only or constant properties or fields. So before you do something like:

lock(Context.Data)
{
    // Manipulate data sub-properties here
}

Remember that it is possible to swap out the object that Data is pointing to. The safest implementation is to provide a special locking objects:

internal readonly object dataSyncRoot = new object();
internal readonly object serviceSyncRoot = new object();
internal readonly object serverSyncRoot = new object();

for each sub-object that requires exclusive access and use:

lock(Context.dataSyncRoot)
{
    // Manipulate data sub-properties here
}

There is no magic bullet on when and where to do the locks, but in general, the higher up in the call stack you put them, the simpler and safer your code will probably be, at the expense of performance - since both threads cannot execute simultaneously anymore. The further down you place them, the more concurrent your code will be, but also more expense.

Aside: there is almost no performance penalty for the actual taking and releasing of the lock, so no need to worry about that.

Sharilyn answered 20/6, 2016 at 15:25 Comment(0)
P
1

Assume we have a MultiCommand class that aggregates a list of ICommands and at some time must execute all commands Asynchronously. All Commands must share context. Each command could change context state, but there is no set order!

The first step is to kick off all ICommand Execute methods passing in the CTX. The next step is to set up an event listener for new CTX Changes.

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();
    }
}

Each command handles the asynchronous part similar to this:

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();
    }
}

Finally we set up a common event handler and notification system.

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);
    }
}

Further abstractions are possible in each Command's execute function. But we won't discuss that now.

Here's what this design does and doesn't do:

  1. It allows any finished task to update the new context on the thread it was first set in the MultiCommand class.
  2. This assumes there is no workflow based state necessary. The post merely indicated a bunch of task only had to run asynchronous rather than in an ordered asynchronous manner.
  3. No currencymanager is necessary because we are relying on each command's closure/completion of the asynchronous task to return the new context on the thread it was created!

If you need concurrency then that implies that the context state is important, that design is similar to this one but different. That design is easily implemented using functions and callbacks for the closure.

Purehearted answered 26/6, 2016 at 2:39 Comment(0)
V
0

As long as each context is only used from a single thread concurrently there is no problem with using it from multiple threads.

Vendace answered 17/6, 2016 at 20:43 Comment(4)
Can you provide an example how to share this context object between the threads?Just using t.Start(ctx); ? Do I need to use lock ? @VendaceGastrotrich
You need to ensure that there are no concurrent usages. Do that any way you like. For example Task.Run().Wait(); Task.Run().Wait(); might use two threads but is not concurrent. You can lock as well if that fits your architecture. Sharing between threads is the easy part. You just need to pass the object ref to the context around.Vendace
Can you help me more with an example about how to share this context object? This object object is being updated through the execution process of all commands and every command has a rollabck action that needs the context object as well.thanks @VendaceGastrotrich
I'm quite unsure what you need and what your design is. As long as you can access the context from your commands you already have succeeded in sharing the object. It's maybe simpler than you imagine.Vendace

© 2022 - 2024 — McMap. All rights reserved.