Windows 8 et async/await : Attention aux accès fichiers

 

 

Windows 8 et async/await : Attention aux accès fichiers

Rate This
  • Comments 3

Grâce à async/await, l’écriture de code asynchrone devient un jeu d’enfant…à tel point que l’on oublie parfois d’y protéger l’accès aux ressources critiques. Ce genre d’oubli provoque des erreurs d’exécution totalement aléatoires dans les applications mais la bonne nouvelle c’est que c’est très facile à corriger, surtout une fois que l’on a compris pourquoi Smile.

Un exemple typique dans les applications Windows 8 : l’accès aux fichiers.

On les utilise à plus forte raison dans les applications Metro, comme par-exemple pour :

  • le fichier servant de cache off-line
  • le fichier de sauvegarde du contexte courant de l’application
  • le fichier contenant les données applicatives

Plusieurs clients m’ont remonté le fait que des exceptions sont déclenchées lors des écritures fichier, or le plus souvent, le problème provient de l’écriture du code asynchrone correspondant.

Une démo vaut mieux que de longs discours, je vais donc illustrer ces propos pas à pas.

D’abord, voyons le bout de code que je vais utiliser : un simple bouton dans l’appbar qui déclenche un accès fichier en écriture.

image

        
    private void Save_Click(object sender, RoutedEventArgs e)
    {
       SaveAsync();
    }

    // Write data to a file
    async void SaveAsync()
    {
       StorageFolder localFolder = Windows.Storage.ApplicationData.Current.LocalFolder;

       var sampleFile = await localFolder.CreateFileAsync("dataFile.txt", CreationCollisionOption.OpenIfExists);
       var ws = await sampleFile.OpenStreamForWriteAsync();
 
       // Ecriture dans mon fichier, blablabla…
       ws.Dispose();
    }
 

Au lancement de l’application, si l’on appuie plusieurs fois d’affilée sur le bouton “Save” en principe il n’y a pas de souci particulier car la méthode SaveAsync s’exécute dans un délai très court et a donc le temps de se terminer avant qu’on l’a réexécute.

A présent, simulons un accès fichier plus long (5 secondes) entre l’ouverture du fichier en écriture et sa fermeture:       

       // Write data to a file
   async void SaveAsync()
   {
       StorageFolder localFolder = Windows.Storage.ApplicationData.Current.LocalFolder;

       var sampleFile = await localFolder.CreateFileAsync("dataFile.txt", CreationCollisionOption.OpenIfExists);
       var ws = await sampleFile.OpenStreamForWriteAsync();

       await Task.Delay(5000);
       ws.Dispose();
   }
 
 Cette fois, si je clique 2 fois sur le bouton en moins de 5 secondes, une exception sera levée:
image
Pourquoi ? Parce que je dois garantir un accès exclusif au fichier ! Et pour l’instant ce n’est pas le cas.
1er réflexe : je désactive mon bouton le temps de réaliser la sauvegarde, sur l’evt click.  

Protection par l’UI

A votre avis, quelle est la différence entre ces 2 snippets : 

Snippet 1 :

        private void Save_Click(object sender, RoutedEventArgs e)
        {
            btSave.IsEnabled = false;
            SaveAsync();
            btSave.IsEnabled = true;
        }

        // Write data to a file
        async void SaveAsync()
        {

            StorageFolder localFolder = Windows.Storage.ApplicationData.Current.LocalFolder;

            var sampleFile = await localFolder.CreateFileAsync("dataFile.txt", CreationCollisionOption.OpenIfExists);
            var ws = await sampleFile.OpenStreamForWriteAsync();

            await Task.Delay(5000);            
            ws.Dispose();
        }

Snippet 2 :

        private void Save_Click(object sender, RoutedEventArgs e)
        {
            SaveAsync();

        }

        // Write data to a file
        async void SaveAsync()
        {
            btSave.IsEnabled = false;

           StorageFolder localFolder = Windows.Storage.ApplicationData.Current.LocalFolder;

            var sampleFile = await localFolder.CreateFileAsync("dataFile.txt", CreationCollisionOption.OpenIfExists);
            var ws = await sampleFile.OpenStreamForWriteAsync();

            await Task.Delay(5000);
            ws.Dispose();

        btSave.IsEnabled = true;
    }

On pourrait penser que l’exécution de ces snippets sera équivalente, mais ce n’est pas le cas.

La commande await permet de synchroniser un appel asynchrone avec les lignes de code qui la suivent, mais il ne faut pas oublier que la méthode appelante – elle – ne sera pas bloquée, reprendra la main et continuera à s’exécuter. C’est d’ailleurs ce qui permet au thread de l’UI de ne pas être bloqué pendant ce temps.

Cela signifie que l’on pourra déclencher plusieurs sauvegardes simultanées en appuyant sur le bouton “Save” dans le cas du 1er snippet, alors que dans le second, le bouton de sauvegarde sera désactivé tant que l’appel asynchrone ne se sera pas terminé.

Ainsi le premier snippet est totalement inefficace pour protéger l’accès au fichier !

Donc, rappelez-vous que lors d’un appel asynchrone avec async/await, on retourne immédiatement dans la fonction appelante sans attendre la fin de l’appel asynchrone !

Le snippet 2 peut être suffisant dans de nombreux cas, mais il ne faut pas oublier que vous empêchez ici les accès concurrents au bouton btSave, et non pas les accès concurrents au fichier !

Très souvent, une sauvegarde intervient automatiquement sans que celle-ci soit commandée par l’UI : c’est d’autant plus le cas sur Windows 8 où le contexte courant est supposé être maintenu au suspend, à la fermeture de l’application ou tout simplement régulièrement au fil de l’utilisation de l’application.

Protection par le code métier

Dans notre exemple nous avons couplé le code métier à l’UI en accédant au bouton btSave dans la méthode SaveAsync.

Il est plus propre de protéger l’accès au fichier au niveau du code métier, tout en restant indépendant de l’UI c’est à dire directement dans la méthode SaveAsync mais sans interagir avec la Vue.

Justement, dans mon exemple j’ai besoin de déclencher une sauvegarde automatique lors du passage dans l’état “suspend” de mon application. Ce nouvel état - nouvellement introduit pour les applications Win8 Metro - intervient 10 secondes après qu’une application passe en arrière plan. Le système peut à tout moment arrêter une application qui se trouve dans l’état suspend, s’il est en manque de ressources. Pour le confort de l’utilisateur, il est conseillé de sauvegarder le contexte applicatif lors du passage en “suspend” d’une application Metro, ce qui permettra de rétablir ce contexte en cas de besoin quand l’application seront relancée.

Dans notre cas, l’appel du SaveAsync sur le passage dans l’état suspend se fait à l’aide du code suivant:

        public MainPage()
        {
            this.InitializeComponent();
            App.Current.Suspending += Current_Suspending;
        }

        void Current_Suspending(object sender, Windows.ApplicationModel.SuspendingEventArgs e)
        {
            SaveAsync();
        }

Si vous lancez votre application, que vous cliquez sur le bouton “Save” puis passez immédiatement en état suspend grâce au bouton associé en mode debug dans Visual Studio, vous obtiendrez une erreur d’exécution.

image

Pour y remédier je vais protéger l’accès à ma ressource critique à l’aide d’un sémaphore : l’objet SemaphoreSlim qui permet de mettre en place un verrou asynchrone (comme un objet Semaphore mais que l’on peut attendre avec un await).

Snippet 3 :       

        SemaphoreSlim _semSave = new SemaphoreSlim(1);

        private void Save_Click(object sender, RoutedEventArgs e)
        {
            btSave.IsEnabled = false;
            SaveAsync();
            btSave.IsEnabled = true;
        }        

        // Write data to a file
        async void SaveAsync()
        {

            await _semSave.WaitAsync();

            StorageFolder localFolder = Windows.Storage.ApplicationData.Current.LocalFolder;

            var sampleFile = await localFolder.CreateFileAsync("dataFile.txt", CreationCollisionOption.OpenIfExists);
            var ws = await sampleFile.OpenStreamForWriteAsync();

            await Task.Delay(5000);
            ws.Dispose();

            _semSave.Release();
        }

Avec ce snippet, l’accès au fichier est protégé, quel que soit l’origine de l’appel de la méthode SaveAsync.

Le code de désactivation du bouton “Save” reste pertinent : il faut un feedback visuel permettant à l’utilisateur de savoir quand la sauvegarde est terminée. Mais elle n’est pas suffisante en tant que telle et demeure complémentaire à la protection de la ressource critique.

Le hasard faisant bien les choses, Rudy Huyn (MVP Windows Phone) a publié au même moment un article sur ce thème à propos de Windows Phone. Je vous le recommande vivement !

 
 
Leave a Comment
  • Please add 7 and 3 and type the answer here:
  • Post
  • Article parfait.

    Bises.

  • Très bon article!

    Nénanmoins je ne vois pas comment l'introduction des sémaphores rend plus pertinents l'utilisation des IsEnabled=false/true au niveau de Save_Click()..

    Tu indiques à propos des appels asynchrones que "il ne faut pas oublier que la méthode appelante – elle – ne sera pas bloquée, reprendra la main et continuera à s’exécuter. C’est d’ailleurs ce qui permet au thread de l’UI de ne pas être bloqué pendant ce temps."

    Donc ça reste aussi valable pour la ligne "await _semSave.WaitAsync();", et dans ce cas on redonne la main à l'appellant, exactement comme dans la snippet 1, Non?

  • Bonjour PedroCivo,

    Les 2 méthodes sont complémentaires, mais indépendantes : le sémaphore permet de locker la sauvegarde du fichier dans tous les cas : qu'on le fasse depuis l'UI ou depuis le code métier (sauvegarde auto p.ex). Le IsEnable = false permet simplement de signaler à l'utilisateur qu'il ne peut pas cliquer sur le bouton pendant une sauvegarde en cours.

    NB : D'ailleurs pour bien faire, il faudrait désactiver le bouton au début de la méthode SaveAsync au niveau du code métier et pas au niveau UI sur le Save_Click qui n'est qu'un cas de sauvegarde parmi d'autres. Ainsi le bouton serait désactivé dans tous les cas de sauvegarde. Mais SaveAsync étant du code métier et pas du code UI, on pourrait alors interagir avec l'UI avec un mécanisme d' évènements signalant la sauvegarde en cours (Saving et Saved): ainsi on désactiverait le bouton pendant la sauvegarde et la réactiverait une fois la sauvegarde terminée.

Page 1 of 1 (3 items)
Page 1 of 4 (87 items) 1234