Bienvenue à Blogs CodeS-SourceS Identification | Inscription | Aide

Abonnements

Comment faire du séquentiel avec de l’asynchrone ?

L’utilisation de services asynchrones est souvent très utile :

  • cela évite de bloquer l’application le temps du traitement
  • permet d’annuler un traitement en cours

Cependant, l’utilisation du mode asynchrone complexifie les algorithmes qui deviennent vite moins intuitif.

Malgré cette complexité, c’est une approche de plus en plus utilisée.

Dans certains cas, le mode synchrone n’est même pas possible (par exemple, Silverlight ne le permet pas).

Imaginons l’exercice suivant :

On souhaite appeler un service une et une seule fois, mettre son résultat en cache et ensuite utiliser ce cache.

Dans le cas d’un service synchrone, cela ne pose aucun soucis :

static class Service
{
    private static string _value;
    public static string Value
    {
        get 
        {
            if (_value == null)
                using (ServiceClient service = new ServiceClient())
                {
                    _value = service.GetData(0);
                }
            return _value; 
        }
    }
}

Pour l’appeler, on pourra ensuite tout simplement faire ceci (dans le cas d’une application Console) :

Console.WriteLine(Service.Value);

Jusque là rien de compliqué.

// Afin d’éviter les remarques, je doublerai systématiquement mon code dans ce post : avec et sans lambda expressions.

Maintenant supposons que l’on veuille faire la même chose en asynchrone.

Pour cela, nous allons utiliser une logique basée sur des évènements :

public static class Service
{
    public static void Init()
    {
        using (ServiceClient service = new ServiceClient())
        {
            service.GetDataCompleted += (sender, e) =>
            {
                if (e.Cancelled)
                    return;
                if (e.Error != null)
                    throw e.Error;
                Value = e.Result;
                if (ValueInitialized != null)
                    ValueInitialized();
            };
            service.GetDataAsync(0);
        }
   
  
    public static string Value { get; private set; } 

   
public static event Action ValueInitialized;
}

public static class Service
{
    public static void Init()
    {
        using (ServiceClient service = new ServiceClient())
        {
            service.GetDataCompleted += service_GetDataCompleted;
            service.GetDataAsync(0);
        }
   

    private static void service_GetDataCompleted(object sender, GetDataCompletedEventArgs e)
    {
        if (e.Cancelled)
            return;
        if (e.Error != null)
            throw e.Error;
        Value = e.Result; 
        if (ValueInitialized != null)
            ValueInitialized();
    }

 

    public static string Value { get; private set; } 
  
    public static event Action ValueInitialized;
}

Quant-à l’appel, il sera le suivant :

Service.ValueInitialized += () => Console.WriteLine(Service.Value);
Service.Init();

Service.ValueInitialized += Service_ValueInitialized;
Service.Init();

 

private static void Service_ValueInitialized()
{
    Console.WriteLine(Service.Value);
}

Maintenant, imaginons que dans notre code, nous voulons récupérer la valeur de Service.Value sans savoir si elle a déjà été initialisée ou non. Comment faire cela ?

Premier point, il va falloir déterminer si Value a déjà été initialisée ou non.

Pour cela, nous allons rajouter une propriété booléenne IsInitialized :

public static class Service
{
    public static void Init()
   
        if (IsInitialized)
        {
            if (ValueInitialized != null
                ValueInitialized(); 
           
return;
        }
        using (ServiceClient service = new ServiceClient())
        {
            service.GetDataCompleted += (sender, e) =>
            {
                if (e.Cancelled)
                    return;
                if (e.Error != null)
                    throw e.Error;
                Value = e.Result; 
                IsInitialized = true;
                if (ValueInitialized != null)
                    ValueInitialized();
            };
            service.GetDataAsync(0);
        }
   
  
    public static string Value { get; private set; } 

    public static bool IsInitialized { get; private set; } 

   
public static event Action ValueInitialized;
}

public static class Service
{
    public static void Init()
   
        if (IsInitialized)
        {
            if (ValueInitialized != null
                ValueInitialized(); 
           
return;
        }
        using (ServiceClient service = new ServiceClient())
        {
            service.GetDataCompleted += service_GetDataCompleted;
            service.GetDataAsync(0);
        }
   

    private static void service_GetDataCompleted(object sender, GetDataCompletedEventArgs e)
    {
        if (e.Cancelled)
            return;
        if (e.Error != null)
            throw e.Error;
        Value = e.Result; 

        IsInitialized = true
        if (ValueInitialized != null)
            ValueInitialized();
    }


    public static string Value { get; private set; } 

    public static bool IsInitialized { get; private set; }
  
    public static event Action ValueInitialized;
}

Si on revient sur mes spécifications initiales, il faut s’assurer que le service n’est appelé qu’une seule fois. Ce n’est pas le cas ici à cause des problématiques multi-threadées.

Pour répondre à ce besoin, nous allons faire l’appel au service dans un constructeur statique :

public static class Service

    static Service()
    {
        using (ServiceClient service = new ServiceClient())
        {
            service.GetDataCompleted += (sender, e) =>
            {
                if (e.Cancelled)
                    return;
                if (e.Error != null)
                    throw e.Error;
                Value = e.Result;

                         IsInitialized = true;
                         if (ValueInitialized != null)
                    ValueInitialized();
            };
            service.GetDataAsync(0);
        }
   
  
    public static string Value { get; private set; } 

    public static bool IsInitialized { get; private set; }


   
public static event Action ValueInitialized;
}

public static class Service
{
    static Service()
    {
        using (ServiceClient service = new ServiceClient())
        {
            service.GetDataCompleted += service_GetDataCompleted;
            service.GetDataAsync(0);
        }
   

    private static void service_GetDataCompleted(object sender, GetDataCompletedEventArgs e)
    {
        if (e.Cancelled)
            return;
        if (e.Error != null)
            throw e.Error;
        Value = e.Result;  
        IsInitialized = true;  
        if (ValueInitialized != null)
            ValueInitialized();
    }
 
 
    public static string Value { get; private set; } 

    public static bool IsInitialized { get; private set; }  
 
    public static event Action ValueInitialized;
}

Pour récupérer la valeur, on pourra désormais utiliser le code suivant :

Action valueInitialized = () => Console.WriteLine(Service.Value);
if (Service.IsInitialized)
    valueInitialized();
else
    Service.ValueInitialized += valueInitialized;

if (Service.IsInitialized)
    Service_ValueInitialized();
else
    Service.ValueInitialized += Service_ValueInitialized;

private static void Service_ValueInitialized()
{
    Console.WriteLine(Service.Value);
}

Cependant, cela n’est pas encore satisfaisant. En effet, à cause de la problématique de multi-threading apportée par l’asynchronisme, on peut se retrouver dans le cas suivant : le thread principal est en cours d’exécution, il fait le test sur IsInitialized (qui vaut false) puis est suspendu. Ensuite, c’est au tour du thread asynchrone de s’exécuter. Il affecte IsInitialized à true et déclenche l’évènement ValueInitialized. Le thread principal reprend la main, s’abonne à ValueInitialized mais il est trop tard…

Aussi il faut gérer ce cas :

public static class Service

    static Service()
    {
        using (ServiceClient service = new ServiceClient())
        {
            service.GetDataCompleted += (sender, e) =>
            {
                if (e.Cancelled)
                    return;
                if (e.Error != null)
                    throw e.Error;
                Value = e.Result; 
                lock (LockObject)
                {
                    IsInitialized = true;
                    if (ValueInitialized != null)
                        ValueInitialized();

                }

            };
            service.GetDataAsync(0);
        }
   


   
private static object _lockObject = new object();
    public static object LockObject
    {
        get { return _lockObject; }
    }

                    
    public static string Value { get; private set; } 

    public static bool IsInitialized { get; private set; }


   
public static event Action ValueInitialized;
}

public static class Service
{
    static Service()
    {
        using (ServiceClient service = new ServiceClient())
        {
            service.GetDataCompleted += service_GetDataCompleted;
            service.GetDataAsync(0);
        }
   

    private static void service_GetDataCompleted(object sender, GetDataCompletedEventArgs e)
    {
        if (e.Cancelled)
            return;
        if (e.Error != null)
            throw e.Error;
        Value = e.Result;  
        lock (LockObject)
        {
            IsInitialized = true;
            if (ValueInitialized != null)
                ValueInitialized();
        }
   


   
private static object _lockObject = new object();
    public static object LockObject
    {
        get { return _lockObject; }
    }

 
    public static string Value { get; private set; }  

    public static bool IsInitialized { get; private set; }  
   
    public static event Action ValueInitialized;
}

Action valueInitialized = () => Console.WriteLine(Service.Value);
if (Service.IsInitialized)
    valueInitialized ();
lock (Service.LockObject)
{
    if (Service.IsInitialized)
        valueInitialized ();
    else
        Service.ValueInitialized += valueInitialized;
}

if (Service.IsInitialized)
    Service_ValueInitialized();
lock (Service.LockObject)
{
    if (Service.IsInitialized)
        Service_ValueInitialized();
    else
        Service.ValueInitialized += Service_ValueInitialized;
}

private static void Service_ValueInitialized()
{
    Console.WriteLine(Service.Value);
}

// Le double test sur Service.IsInitialized permet d’éviter de poser un lock inutile.

Cependant, du point de vue architecture, ce code n’est pas idéal. En effet, il est anormal à mon sens que les classes utilisant ma classe Service aient besoin de connaître son fonctionnement (afin d’utiliser la logique du lock). De plus, on risque de se retrouver avec beaucoup de code identique tout au long de l’application car il faudra gérer le lock à chaque fois qu’on voudra accéder à Service.Value. Or je pense que tout le monde sait qu’il vaut mieux factoriser, en l’occurence dans la classe Service.

public static class Service
{
    static Service()
    {
        using (ServiceClient service = new ServiceClient())
        {
            service.GetDataCompleted += (sender, e) =>
            {
                if (e.Cancelled)
                    return;
                if (e.Error != null)
                    throw e.Error;
                Value = e.Result;
                lock (_lockObject)
                {
                    IsInitialized = true;
                    if (ValueInitialized != null)
                        ValueInitialized();
                }
            };
            service.GetDataAsync(0);
        }
   
 

    private static object _lockObject = new object(); 
  
    public static string Value { get; private set; } 
  
    private static bool IsInitialized { get; set; } 
  
    public static void DoWhenInitialized(Action action)
   
        if (action == null)
            return;


        if (IsInitialized)
        {
            action();
            return;
        }

        lock (_lockObject)
        {
            if (IsInitialized)
                action();
            else
                ValueInitialized += action;
        }
   
  
    public static event Action ValueInitialized;
}

public static class Service
{
    static Service()
    {
        using (ServiceClient service = new ServiceClient())
        {
            service.GetDataCompleted += service_GetDataCompleted;
            service.GetDataAsync(0);
        }
    }

 

    private static void service_GetDataCompleted(object sender, GetDataCompletedEventArgs e)
    {
        if (e.Cancelled)
            return;
        if (e.Error != null)
            throw e.Error;
        Value = e.Result;
        lock (_lockObject)
        {
            IsInitialized = true;
            if (ValueInitialized != null)
                ValueInitialized();
        }
   

    private static object _lockObject = new object(); 
  
    public static string Value { get; private set; } 
  
    private static bool IsInitialized { get; set; }

    public static void DoWhenInitialized(Action action)
    {   
       
if (action == null)
            return;
 

        if (IsInitialized)
        {
            action();
            return;
       

        lock (_lockObject)
        {
            if (IsInitialized)
                action();
            else
                ValueInitialized += action;
        }
    }  
   
    public static event Action ValueInitialized;
}

L’appel lui se réduit maintenant à ceci :

Service.DoWhenInitialized(() => Console.WriteLine(Service.Value));

Service.DoWhenInitialized(Service_ValueInitialized);

private static void Service_ValueInitialized()
{
    Console.WriteLine(Service.Value);
}

Un dernier point particulièrement important : pensez à vous désabonner !

La version finale de mon code sera donc la suivante :

public static class Service
{
    static Service()
    {
        using (ServiceClient service = new ServiceClient())
        {
            EventHandler<GetDataCompletedEventArgs> serviceGetDataCompleted = null;
            serviceGetDataCompleted = (sender, e) =>
            {
                service.GetDataCompleted -= serviceGetDataCompleted;
                if (e.Cancelled)
                    return;
                if (e.Error != null)
                    throw e.Error;
                Value = e.Result;
                lock (_lockObject)
                {
                    IsInitialized = true;
                    if (ValueInitialized != null)
                        ValueInitialized();
                }
            };
            service.GetDataCompleted += serviceGetDataCompleted;
            service.GetDataAsync(0);
        }
    }   

    private static object _lockObject = new object(); 
  
    public static string Value { get; private set; } 
  
    private static bool IsInitialized { get; set; } 
  
    public static void DoWhenInitialized(Action action)
    {

        if (action == null)
            return;


        if (IsInitialized)
        {
            action(); 
            return;
        }

        lock (_lockObject)
        {
            if (IsInitialized)
                action();
            else
            {
                Action valueInitialized = null;
                valueInitialized = () =>
                    {
                        ValueInitialized -= valueInitialized;
                        action();
                    }; 
                ValueInitialized += valueInitialized;
            }
        }
    }  

    private static event Action ValueInitialized;
}

public static class Service
{
    static Service()
    {
        using (ServiceClient service = new ServiceClient())
        {
            EventHandler<GetDataCompletedEventArgs> serviceGetDataCompleted = null;
            serviceGetDataCompleted = (sender, e) =>
            {
                service.GetDataCompleted -= serviceGetDataCompleted;
                if (e.Cancelled)
                    return;
                if (e.Error != null)
                    throw e.Error;
                Value = e.Result;
                lock (_lockObject)
                {
                    IsInitialized = true;
                    if (ValueInitialized != null)
                        ValueInitialized();
                }
            };
            service.GetDataCompleted += serviceGetDataCompleted;
            service.GetDataAsync(0);
        }
   
 
    private static object _lockObject = new object(); 
 

    public static string Value { get; private set; } 
 
    private static bool IsInitialized { get; set; } 
 
    public static void DoWhenInitialized(Action action)
    {
        if (action == null)
            return;


        if (IsInitialized)
        {
            action(); 
            return;
       

        lock (_lockObject)
        {
            if (IsInitialized)
                action(); 
            else
                ValueInitialized += new ActionClass { Action = action }.Service_ValueInitialized; 
        } 
   
  
    private static event Action ValueInitialized; 
 
   
    private class ActionClass
    {
        public Action Action { get; set; } 
 
  
        public void Service_ValueInitialized()
        {
            ValueInitialized -= Service_ValueInitialized;
            if (Action != null)
                Action();
        }
    }

}

//Soit dit en passant, vous pouvez vous apercevoir, dans ce dernier exemple particulièrement, à quel point c’est pénible sans les lambdas…

Enjoy Smile

Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :

Publié jeudi 25 mars 2010 01:41 par Matthieu MEZIL

Classé sous : , ,

Commentaires

# re: Comment faire du séquentiel avec de l’asynchrone ? @ vendredi 2 avril 2010 17:23

Sympa :)

Par contre non seulement c'est lourd le code sans les lambdas, mais c'est encore plus lourd d'avoir une double ration de code dans ton post ^^

Adrien Siffermann

# re: Comment faire du séquentiel avec de l’asynchrone ? @ vendredi 2 avril 2010 20:19

et c'est encore plus lourd pour moi d'écrire un code sans lambda pour éviter les commentaires

Matthieu MEZIL

# re: Comment faire du séquentiel avec de l’asynchrone ? @ jeudi 6 mai 2010 11:13

excellent article, maiq que de passe t-il si le service ne répond jamais ? Peut-on introduire une notion de timeout pour la réponse ?

minsou

# re: Comment faire du séquentiel avec de l’asynchrone ? @ vendredi 7 mai 2010 21:28

Il suffit pour cela d'utiliser un DispatcherTimer.

Hope that helps

Matthieu

Matthieu MEZIL

Les commentaires anonymes sont désactivés

Les 10 derniers blogs postés

- Merci par Blog de Jérémy Jeanson le 10-01-2019, 20:47

- Office 365: Script PowerShell pour auditer l’usage des Office Groups de votre tenant par Blog Technique de Romelard Fabrice le 04-26-2019, 11:02

- Office 365: Script PowerShell pour auditer l’usage de Microsoft Teams de votre tenant par Blog Technique de Romelard Fabrice le 04-26-2019, 10:39

- Office 365: Script PowerShell pour auditer l’usage de OneDrive for Business de votre tenant par Blog Technique de Romelard Fabrice le 04-25-2019, 15:13

- Office 365: Script PowerShell pour auditer l’usage de SharePoint Online de votre tenant par Blog Technique de Romelard Fabrice le 02-27-2019, 13:39

- Office 365: Script PowerShell pour auditer l’usage d’Exchange Online de votre tenant par Blog Technique de Romelard Fabrice le 02-25-2019, 15:07

- Office 365: Script PowerShell pour auditer le contenu de son Office 365 Stream Portal par Blog Technique de Romelard Fabrice le 02-21-2019, 17:56

- Office 365: Script PowerShell pour auditer le contenu de son Office 365 Video Portal par Blog Technique de Romelard Fabrice le 02-18-2019, 18:56

- Office 365: Script PowerShell pour extraire les Audit Log basés sur des filtres fournis par Blog Technique de Romelard Fabrice le 01-28-2019, 16:13

- SharePoint Online: Script PowerShell pour désactiver l’Option IRM des sites SPO non autorisés par Blog Technique de Romelard Fabrice le 12-14-2018, 13:01