Matthieu Mezil
L’objet, c’est beau
Le développement orienté objet peut être apparenté, osons le mot, à une forme d’art. Cependant, avant de devenir un « artiste » de l’objet, il y a plusieurs notions à maîtriser. A travers cet article, nous allons en aborder quelques-unes.
Par Matthieu Mezil publié le 27/04/2009 à 07:10, lu 2232 fois, 10 pages
 8 | Multi-threading : cancel
Rajoutons une dernière demande fonctionnelle : avoir la possibilité d’annuler les traitements en cours d’exécution. Pour cela, le premier point va consister à rajouter une méthode Cancel et une propriété Cancelled en lecture seule sur l’interface IStringsTransform :

public interface IStringsTransform

{

    IEnumerable<string> Do(IEnumerable<string> lines);

    void Cancel();

    bool Cancelled { get; }

}

Afin de factoriser au maximum le code, la classe StringsTransformBase va refaire son apparition :

public abstract class StringsTransformBase : IStringsTransform

{

    public abstract IEnumerable<string> Do(IEnumerable<string> lines);

 

    public void Cancel()

    {

        Cancelled = true;

    }

 

    public bool Cancelled { get; private set; }

}

 

public abstract class PerStringTransformBase : StringsTransformBase

{

    public override IEnumerable<string> Do(IEnumerable<string> lines)

    {

        Init();

        foreach (string line in lines)

        {

            if (Cancelled)

                yield break;

            yield return DoAction(line);

        }

    }

 

    protected abstract string DoAction(string line);

 

    protected virtual void Init()

    {

    }

}

 

public class CompositeStringsTransform : StringsTransformBase

{

    private List<IStringsTransform> _transforms = new List<IStringsTransform>();

 

    public CompositeStringsTransform(IEnumerable<IStringsTransformInstanciator> transforms)

    {

        _transforms.AddRange(transforms.Select(s => s.CreateInstance()));

    }

    public CompositeStringsTransform(params IStringsTransformInstanciator[] transforms)

        : this((IEnumerable<IStringsTransformInstanciator>)transforms)

    {

    }

 

    public List<IStringsTransform> Transforms

    {

        get { return _transforms; }

    }

 

    public override IEnumerable<string> Do(IEnumerable<string> lines)

    {

        foreach (var transform in Transforms)

        {

            if (Cancelled)

                break;

            lines = transform.Do(lines);

        }

        return lines;

    }

 

    public override string ToString()

    {

        var sb = new StringBuilder();

        foreach (var transform in Transforms)

        {

            sb.Append(" & ");

            sb.Append(transform);

        }

        return sb.ToString().Substring(3);

    }

}

Côte UI, nous allons rajouter deux ListBox : une pour les traitements en cours et une pour les traitements effectués.
L’idée est comme précédemment de remplir la ListBox avec des ITransform afin de simplifier leur identification pour pouvoir les annuler.
En réalité, nous n’allons pas utiliser directement l’interface ITransform mais une classe créé pour l’occasion : FileTransformInfo.

public class FileTransformInfo

{

    internal FileTransformInfo()

    {

    }

 

    public IStringsTransform Transform { get; internal set; }

    public string InFilePath { get; internal set; }

    public string OutFilePath { get; internal set; }

 

    public override string ToString()

    {

        return string.Concat(Transform, " (", InFilePath, "  -->  ", OutFilePath, ")");

    }

}

Nous allons ensuite modifier la classe FileTransform pour que la méthode Transform retourne un FileTransformInfo et intégre l’annulation tout en évitant de créer un thread pour rien :

public static class FileTransform

{

    private static Dictionary<string, BackgroundWorker> _transformsRunning = new Dictionary<string, BackgroundWorker>();

    private static object _lockObject = new object();

 

    public static FileTransformInfo Transform(string inFilePath, string outFilePath,

        IEnumerable<IStringsTransformInstanciator> transforms, Action<string> showError,

        Func<string, bool> showErrorWithRetry, Action<bool, FileTransformInfo> transformDone)

    {

        var transform = new CompositeStringsTransform(transforms);

        var result = new FileTransformInfo

        {

            Transform = transform,

            InFilePath = inFilePath,

            OutFilePath = outFilePath

        };

        if (!(File.Exists(inFilePath) || _transformsRunning.ContainsKey(inFilePath)))

            FileNotFoundShowError(inFilePath, showError);

        else if (!Directory.Exists(Path.GetDirectoryName(outFilePath)))

            DirectoryNotFoundShowError(outFilePath, showError);

        else

        {

            var bg = new BackgroundWorker();

            bg.DoWork += (s, e) =>

            {

                e.Result = false;

                while (true)

                {

                    try

                    {

                        FileUtil.WriteLines(outFilePath, transform.Do(FileUtil.GetLines(inFilePath)));

                        e.Result = !transform.Cancelled;

                    }

                    catch (FileNotFoundException)

                    {

                        FileNotFoundShowError(inFilePath, showError);

                    }

                    catch (DirectoryNotFoundException)

                    {

                        DirectoryNotFoundShowError(outFilePath, showError);

                    }

                    catch (IOException)

                    {

                        if (showErrorWithRetry(string.Format(ErrorResources.IOException, outFilePath)))

                            continue;

                    }

                    catch (Exception ex)

                    {

                        showError(string.Format(ErrorResources.FileNotFoundException,

                            string.Concat(ex.GetType(), " : ", ex.Message)));

                    }

                    break;

                }

                lock (_lockObject)

                {

                    _transformsRunning.Remove(outFilePath);

                }

            };

            bg.RunWorkerCompleted += (s, e) =>

            {

                TransformDone(transformDone, result, (bool)e.Result);

            };

 

            StartBGAndCache(result, bg, transformDone);

        }

        return result;

    }

 

    private static void TransformDone(Action<bool, FileTransformInfo> transformDone,

        FileTransformInfo fileTransformInfo, bool transformResult)

    {

        if (transformDone != null)

            transformDone(transformResult, fileTransformInfo);

    }

 

    private static void StartBGAndCache(FileTransformInfo fileTransformInfo, BackgroundWorker bg,

        Action<bool, FileTransformInfo> transformDone)

    {

        lock (_lockObject)

        {

            if (fileTransformInfo.Transform.Cancelled)

            {

                TransformDone(transformDone, fileTransformInfo, false);

                return;

            }

            var inFilePath = fileTransformInfo.InFilePath;

            var outFilePath = fileTransformInfo.OutFilePath;

            RunWorkerCompletedEventHandler runWorkerCompletedTryAgain = null;

            Action<BackgroundWorker> tryAgain = (bgTryAgain) =>

                {

                    StartBGAndCache(fileTransformInfo, bg, transformDone);

                    bgTryAgain.RunWorkerCompleted -= runWorkerCompletedTryAgain;

                };

            if (_transformsRunning.ContainsKey(inFilePath))

            {

                var bgTryAgain = _transformsRunning[inFilePath];

                runWorkerCompletedTryAgain = (s, e) => tryAgain(bgTryAgain);

                bgTryAgain.RunWorkerCompleted += runWorkerCompletedTryAgain;

            }

            else if (_transformsRunning.ContainsKey(outFilePath))

            {

                var bgTryAgain = _transformsRunning[outFilePath];

                runWorkerCompletedTryAgain = (s, e) => tryAgain(bgTryAgain);

                bgTryAgain.RunWorkerCompleted += runWorkerCompletedTryAgain;

            }

            else

            {

                _transformsRunning.Add(outFilePath, bg);

                bg.RunWorkerAsync();

            }

        }

    }

 

    private static void DirectoryNotFoundShowError(string directoryPath, Action<string> showError)

    {

        showError(string.Format(ErrorResources.DirectoryNotFoundException, Path.GetDirectoryName(directoryPath)));

    }

 

    private static void FileNotFoundShowError(string filePath, Action<string> showError)

    {

        showError(string.Format(ErrorResources.FileNotFoundException, filePath));

    }

}

Le fait que l’exécution d’un seul traitement implique l’utilisation d’un CompositeStringsTransform peut être considéré comme regrettable (ou non suivant les secanrii). Cependant, dans le cadre de cet article, nous allons considérer qu’il faut « corriger » cela. Ne connaissant pas le type réel de notre IEnumerable<IStringsTransformInstanciator>, il est déconseillé de passer par la méthode Count() pour savoir s’il n’y en a qu’un. En effet, pour calculer le Count(), il va falloir passer tous les éléments de la collection. Nous allons donc passer par la combinaison d’un Skip et d’un Any. De plus il faut également gérer le cas où il n’y aurait aucun traitement.
Afin de ne pas s’embêter avec ce dernier cas, nous allons créer une classe StringsEmptyTransform :

internal class StringsNoTransform : StringsTransformBase

{

    public override IEnumerable<string> Do(IEnumerable<string> lines)

    {

        return lines;

    }

 

    public override string ToString()

    {

        return "";

    }

}

Puis nous allons modifier le code de FileTransform.Transform :

public static FileTransformInfo Transform(string inFilePath, string outFilePath,

    IEnumerable<IStringsTransformInstanciator> transforms, Action<string> showError,

    Func<string, bool> showErrorWithRetry, Action<bool, FileTransformInfo> transformDone)

{

    IStringsTransform transform;

    if (transforms.Skip(1).Any())

        transform = new CompositeStringsTransform(transforms);

    else

    {

        var transformInstanciator = transforms.FirstOrDefault();

        transform = transformInstanciator == null ? new StringsNoTransform() :

            transformInstanciator.CreateInstance();

    }

    var result = new FileTransformInfo

    {

        Transform = transform,

        InFilePath = inFilePath,

        OutFilePath = outFilePath

    };

    if (!(File.Exists(inFilePath) || _transformsRunning.ContainsKey(inFilePath)))

        FileNotFoundShowError(inFilePath, showError);

    else if (!Directory.Exists(Path.GetDirectoryName(outFilePath)))

        DirectoryNotFoundShowError(outFilePath, showError);

    else

    {

        var bg = new BackgroundWorker();

        bg.DoWork += (s, e) =>

        {

            e.Result = false;

            while (true)

            {

                try

                {

                    FileUtil.WriteLines(outFilePath, transform.Do(FileUtil.GetLines(inFilePath)));

                    e.Result = !transform.Cancelled;

                }

                catch (FileNotFoundException)

                {

                    FileNotFoundShowError(inFilePath, showError);

                }

                catch (DirectoryNotFoundException)

                {

                    DirectoryNotFoundShowError(outFilePath, showError);

                }

                catch (IOException)

                {

                    if (showErrorWithRetry(string.Format(ErrorResources.IOException, outFilePath)))

                        continue;

                }

                catch (Exception ex)

                {

                    showError(string.Format(ErrorResources.FileNotFoundException,

                        string.Concat(ex.GetType(), " : ", ex.Message)));

                }

                break;

            }

            lock (_lockObject)

            {

                _transformsRunning.Remove(outFilePath);

            }

        };

        bg.RunWorkerCompleted += (s, e) =>

        {

            TransformDone(transformDone, result, (bool)e.Result);

        };

 

        StartBGAndCache(result, bg, transformDone);

    }

    return result;

}

Maintenant, côté UI, le code associé à l’évènement runBtn.Click est le suivant :

private void runBtn_Click(object sender, EventArgs e)

{

    lock (_lockObject)

    {

        processingTransformsLB.Items.Add(FileTransform.Transform(inFilePathTB.Text, outFilePathTB.Text,

            transformsLB.Items.Cast<IStringsTransformInstanciator>(),

            message => MessageBox.Show(message, Resources.ErrorTitle, MessageBoxButtons.OK, MessageBoxIcon.Error),

            message =>

                MessageBox.Show(message, Resources.ErrorTitle, MessageBoxButtons.RetryCancel, MessageBoxIcon.Error)

                == DialogResult.Retry,

            (result, fileTransform) =>

            {

                lock (_lockObject)

                {

                    if (result)

                        transformsDoneLB.Items.Add(fileTransform);

                    processingTransformsLB.Items.Remove(fileTransform);

                }

            }));

    }

}

A quoi servent les deux locks et pourquoi les deux locks ne provoquent-ils pas un « dead lock » ?
Les deux locks permettent de s’assurer que l’on écrira bien dans la ListBox des traitements en cours avant de passer dans la ListBox des traitements terminés. Ceci est notamment important pour être sûr qu’il n’y aura pas de « résidus » de traitements effectués dans la ListBox des traitements en cours.
Il n’y aura pas de « dead lock » car comme précédemment, l’utilisation d’un délégué implique un code exécuté de façon différé.
 
» Démarrer une discussion
 
Discussion démarée par killscores le 28/04/2009 à 17:06, 3 commentaire(s).