10 votes

Comment partager le même contexte avec différents threads dans le modèle multi-commande en C# ?

Il existe une implémentation étendue de schéma de commande pour prendre en charge les multi-commandes (groupes) en 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);

Maintenant, l'exécution est comme :

groups.Execute();

Je partage la même contexte (ctx) objet.

Le plan d'exécution de l'application web doit séparer commandGroup1 y commandGroup2 dans un fil différent. En particulier, commandGroup2 sera exécuté dans un nouveau thread et commandGroup1 dans le fil principal.

L'exécution ressemble maintenant à :

//In Main Thread
commandGroup1.Execute();

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

Comment puis-je en toute sécurité partagent le même context object (ctx) afin de pouvoir revenir en arrière. commandGroup1 du nouveau fil de discussion ?

Est t.Start(ctx); suffisant ou dois-je utiliser un verrou ou autre ?

Voici un exemple de mise en œuvre du code aquí

2voto

nicholas Points 1363

L'exemple de code fourni laisse certainement ouvert un grand nombre de questions sur votre cas d'utilisation particulier ; cependant, je vais tenter de répondre à la stratégie générale de mise en œuvre de ce type de problème pour un environnement multithread.

Le contexte ou ses données sont-ils modifiés de manière couplée et non atmoïque ?

Par exemple, est-ce que l'une de vos commandes ferait quelque chose comme :

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

Dans ce cas, il faut absolument que vous utilisiez lock(...) quelque part dans votre code. La question est de savoir où.

Quel est le comportement de vos contrôleurs imbriqués en matière de sécurité des threads ?

Dans le code d'exemple GIST lié, l'élément CommandContext La classe a des propriétés ServerController y ServiceController . Si vous n'êtes pas le propriétaire de ces classes, vous devez vérifier soigneusement la documentation sur la sécurité des threads de ces classes également.

Par exemple, si vos commandes s'exécutant sur deux threads différents effectuent des appels tels que :

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

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

Il est fort possible que ces deux actions ne puissent être invoquées simultanément si le créateur de la classe de contrôleur ne s'attendait pas à une utilisation multithread.

Quand verrouiller et sur quoi verrouiller

Prenez le verrou chaque fois que vous devez effectuer plusieurs actions qui doivent se dérouler complètement ou pas du tout, ou lorsque vous invoquez des opérations de longue durée qui ne s'attendent pas à un accès simultané. Libérez le verrou dès que possible.

De même, les verrous ne doivent être pris que sur des propriétés ou des champs en lecture seule ou constants. Donc avant de faire quelque chose comme :

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

N'oubliez pas qu'il est possible d'échanger l'objet qui Data pointe vers. L'implémentation la plus sûre consiste à fournir un objet de verrouillage spécial :

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

pour chaque sous-objet qui nécessite un accès et une utilisation exclusifs :

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

Il n'y a pas de recette miracle pour savoir quand et où placer les verrous, mais en général, plus vous les placez haut dans la pile d'appels, plus votre code sera probablement simple et sûr, au détriment des performances - puisque les deux threads ne peuvent plus s'exécuter simultanément. Plus vous les placez vers le bas, plus votre code sera concurrent, mais aussi plus coûteux.

Par ailleurs, il n'y a pratiquement aucune pénalité de performance pour la prise et le relâchement du verrou, il n'y a donc pas lieu de s'inquiéter à ce sujet.

1voto

John Peters Points 3662

Supposons que nous ayons une classe MultiCommand qui regroupe une liste de ICommands et qui, à un moment donné, doit exécuter toutes les commandes de manière asynchrone. Toutes les commandes doivent partager le contexte. Chaque commande peut changer l'état du contexte, mais il n'y a pas d'ordre fixe !

La première étape consiste à lancer toutes les méthodes ICommand Execute en passant par le CTX. L'étape suivante consiste à mettre en place un écouteur d'événements pour les nouveaux changements de 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();
    }
}

Chaque commande gère la partie asynchrone de la manière suivante :

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

Enfin, nous mettons en place un gestionnaire d'événements et un système de notification communs.

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

D'autres abstractions sont possibles dans la fonction d'exécution de chaque commande. Mais nous n'en parlerons pas maintenant.

Voici ce que ce modèle fait et ne fait pas :

  1. Il permet à toute tâche terminée de mettre à jour le nouveau contexte sur le fil qu'il a été défini pour la première fois dans la classe MultiCommand.
  2. Cela suppose qu'aucun état basé sur le flux de travail n'est nécessaire. Le message indiquait simplement qu'un ensemble de tâches devaient être exécutées de manière asynchrone plutôt que de manière ordonnée et asynchrone.
  3. Aucun currencymanager n'est nécessaire car nous comptons sur la fermeture/accomplissement de la tâche asynchrone de chaque commande pour retourner le nouveau contexte sur le thread où il a été créé !

Si vous avez besoin de la concurrence, cela implique que l'état du contexte est important, cette conception est similaire à celle-ci mais différente. Cette conception est facilement mise en œuvre en utilisant des fonctions et des rappels pour la fermeture.

0voto

usr Points 74796

Tant que chaque contexte n'est utilisé qu'à partir d'un seul thread simultanément, il n'y a aucun problème à l'utiliser à partir de plusieurs threads.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X