Plan du site  
pixel
pixel

Articles - Étudiants SUPINFO

Multithreads in C# (Partie2)

Par Riadh ADOUANI Publié le 12/10/2017 à 18:10:30 Noter cet article:
(0 votes)
En attente de relecture par le comité de lecture

Introduction

Comme on a vu dans la première partie de cet article, l’utilisation des threads et de thread pool est bien mais elle présente des inconvénients à titre d’exemple Il n'y a pas une méthode pour déterminer si le thread a terminé son job ou non et sa valeur de retour.

C'est pourquoi le .NET Framework introduit le concept du Tâche, qui est un objet qui représente un « Job » qui devrait être fait. La tâche peut vous dire si le travail a été terminé et si l'opération renvoie un résultat, la tâche vous donne le résultat.

Un planificateur de tâches est chargé de démarrer la tâche et de la gérer. Par défaut, le planificateur de tâches utilise des threads du pool de threads pour exécuter la tâche.

La classe Task

Les tâches peuvent être utilisées pour rendre votre application plus réactive. Si le thread qui gère l'interface utilisateur décharge le travail sur un autre thread du pool de threads, il peut continuer à traiter les événements utilisateur et s'assurer que l'application peut toujours être utilisée. Mais cela n'aide pas avec l'évolutivité. Si un thread reçoit une requête Web et qu'il commence une nouvelle tâche, il consomme simplement un autre thread du pool de threads pendant que le thread d'origine attend les résultats.

Exécuter une tâche sur un autre thread n'a de sens que si vous voulez garder le thread interface de l'utilisateur libre pour un autre travail ou si vous voulez paralléliser votre travail sur plusieurs processeurs.

Les exemples suivants montrent comment démarrer une nouvelle tâche et attendez qu'elle soit terminée.

using System;
using System.Linq;
using System.Threading.Tasks;

namespace Listings1
{
    class Program
    {
        static void Main(string[] args)
        {
            Task userTask = new Task(someWorkToDo);
            userTask.Start();
            userTask.Wait();
        }

        private static void someWorkToDo()
        {
            foreach (var value in Enumerable.Range(0,1000))
            {
                Console.WriteLine(value);
            }   
        }
    }
}

        

using System;
using System.Linq;
using System.Threading.Tasks;

namespace Listings2
{
    class Program
    {
        static void Main(string[] args)
        {
            Task userTask = Task.Run(new Action(someWorkToDo));
            userTask.Wait();
        }

        private static void someWorkToDo()
        {
            foreach (var value in Enumerable.Range(0,1000))
            {
                Console.WriteLine(value);
            }   
        }
    }
}

        

using System;
using System.Linq;
using System.Threading.Tasks;

namespace Listings3
{
    class Program
    {
        static void Main(string[] args)
        {
            Task userTask = Task.Run(() =>
            {
                foreach (var value in Enumerable.Range(0, 1000))
                {
                    Console.WriteLine(value);
                }
            });
            userTask.Wait();
        }
    }
}
}
        

Ces exemples créent une nouvelle tâche et la lance immédiatement. La méthode Wait() est l’équivalente de la méthode Join() sur les threadsl. Le CLR attend que la tâche soit terminée avant de quitter l'application.

À côté de la classe Task, le .NET Framework a également définit la classe Task <T> que vous pouvez utiliser si une tâche doit renvoyer une valeur. Les exemples suivants montrent comment cela fonctionne.

using System;
using System.Linq;
using System.Threading.Tasks;

namespace Listings4
{
    class Program
    {
        static void Main(string[] args)
        {
            Task<ulong> userTask = Task.Run(() =>
            {
                ulong somme = 0;
                foreach (var value in Enumerable.Range(0, 1000))
                {
                    somme += (ulong)value;
                }
                return somme;
            });
            Console.WriteLine($"Sum of first 1000 numbers is {userTask.Result}");
        }
    }
}

using System;
using System.Linq;
using System.Threading.Tasks;

namespace Listings5
{
    class Program
    {
        static void Main(string[] args)
        {
            Task<ulong> userTask = Task.Run(new Func<ulong>(doSomeWork));
            Console.WriteLine($"Sum of first 1000 numbers is {userTask.Result}");
            Console.ReadKey();
        }

        private static ulong doSomeWork()
        {           
            ulong somme = 0;
            foreach (var value in Enumerable.Range(0, 1000))
            {
                somme += (ulong)value;
            }
            return somme;         
        }
    }
}

Tenter de lire la propriété Result sur une tâche forcera le thread à attendre que la tâche soit terminée avant de continuer. Tant que la tâche n'a pas fini, il est impossible de donner le résultat. Si la tâche n'est pas terminée, cet appel bloquera le thread en cours.

En raison de la nature orientée objet de l'objet Task, vous pouvez ajouter une tâche de continuation. Cela signifie que vous voulez qu'une autre opération soit exécutée dès que la tâche est terminée.

L’exemple suivant montre un exemple de création d'une telle continuation.

using System;
using System.Linq;
using System.Threading.Tasks;

namespace Listings6
{
    class Program
    {
        static void Main(string[] args)
        {
            Task<bool> userTask = Task.Run(new Func<ulong>(doSomeWork))
                .ContinueWith((t) =>
                {
                    return t.Result % 2 == 0 ? true : false;
                    
                });
            var result = userTask.Result ? "even" : "odd";
            Console.WriteLine($"Sum of first 1000 numbers is {result}");
            Console.ReadKey();
        }

        private static ulong doSomeWork()
        {           
            ulong somme = 0;
            foreach (var value in Enumerable.Range(0, 1000))
            {
                somme += (ulong)value;
            }
            return somme;         
        }
    }
}

La méthode ContinueWith est surchargé que vous pouvez utiliser pour configurer lorsque la suite sera exécutée. De cette façon, vous pouvez ajouter différentes méthodes de continuation qui s'exécuteront lorsqu'une exception se produira, la tâche sera annulée ou la tâche terminée. L’exemple suivant montre comment faire ceci

using System;
using System.Linq;
using System.Threading.Tasks;

namespace Listings7
{
    class Program
    {
        static void Main(string[] args)
        {
            Task<ulong> userTask = Task.Run(new Func<ulong>(doSomeWork));           
            var compTask = userTask.ContinueWith((t) =>
            {
                if (t.Result % 2 == 0)
                    Console.WriteLine("Sum of first 1000 numbers is even");
                else
                    Console.WriteLine("Sum of first 1000 numbers is odd");
            }, TaskContinuationOptions.OnlyOnRanToCompletion);
            try
            {
                userTask.ContinueWith((t) =>
                {
                    Console.WriteLine("The task has been cancelled..");
                    return new TaskCanceledException();
                }, TaskContinuationOptions.OnlyOnCanceled);
            }
            catch(TaskCanceledException ex)
            {
                Console.WriteLine(ex.Message);
            }
            Console.ReadKey();
        }

        private static ulong doSomeWork()
        {           
            ulong somme = 0;
            foreach (var value in Enumerable.Range(0, 1000))
            {
                somme += (ulong)value;
            }
            return somme;         
        }
    }
}

Un autre concept aussi est introduit dans les tâches est la décomposition. Une Tâche peut également comporter plusieurs Tâches enfant. La tâche parente finit lorsque toutes les tâches enfant sont terminées. L’exemple suivant montre comment cela fonctionne.

using System;
using System.Linq;
using System.Threading.Tasks;

namespace Listings8
{
    class Program
    {
        static void Main(string[] args)
        {

            var userTask = Task<ulong[]>.Run(new Func<ulong[]>(doSomeWork));
            userTask.ContinueWith(t =>
            {
                foreach (var value in Enumerable.Range(0, 10))
                {
                    Console.WriteLine($"{value}! = {t.Result[value]}");
                }
            });         
            Console.ReadKey();
        }

        private static ulong[] doSomeWork()
        {
            var results = new ulong[10];
           
            for(int index=0;index<10;index++)
            {
                var innerThread = new Task<ulong>(() =>
                   {
                       ulong somme = 1;
                       foreach (var value in Enumerable.Range(1, index))
                       {
                           somme *= (ulong)value;
                       }
                       return somme;
                   }, TaskCreationOptions.AttachedToParent);
                innerThread.Start();
                results[index] = innerThread.Result;

            }
            return results;
        }
    }
}
////////////////////////////////////////////////////////////////////
0! = 1
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
6! = 720
7! = 5040
8! = 40320
9! = 362880

Dans l'exemple précédent, vous avez créé trois tâches avec les mêmes options. Pour faciliter le processus, vous pouvez utiliser la classe TaskFactory. La classe TaskFactory est créée avec une certaine configuration et peut ensuite être utilisée pour créer des tâches avec cette configuration L’exemple suivant montre comment vous pouvez simplifier l'exemple précédent.

using System;
using System.Linq;
using System.Threading.Tasks;

namespace Listings9
{
    class Program
    {

        static void Main(string[] args)
        {

            var userTask = Task.Run(new Func<ulong[]>(doSomeWork));
            userTask.ContinueWith(t =>
            {
                foreach (var value in Enumerable.Range(0, 10))
                {
                    Console.WriteLine($"{value}! = {t.Result[value]}");
                }
            });         
            Console.ReadKey();
        }

        private static ulong[] doSomeWork()
        {
            var results = new ulong[10];
            TaskFactory factory = new TaskFactory(TaskCreationOptions.AttachedToParent, TaskContinuationOptions.ExecuteSynchronously);
            for (int index=0;index<10;index++)
            {
                var innerThread = factory.StartNew(() => facto(index));                 
                results[index] = innerThread.Result;
            }
            return results;
        }

        private static ulong facto(int index)
        {
            ulong somme = 1;
            foreach (var value in Enumerable.Range(1, index))
            {
                somme *= (ulong)value;
            }
            return somme;
        }
    }
}

En plus d'appeler Wait() sur une seule tâche, vous pouvez également utiliser la méthode WaitAll() pour attendre la fin de plusieurs tâches avant de poursuivre l'exécution. L’exemple suivant montre comment utiliser ceci.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Listings10
{
    class Program
    {

        static void Main(string[] args)
        {
            var pool = new List<Task<ulong>>();
            foreach(var value in Enumerable.Range(1,5))
            {
                pool.Add(Task.Run(new Func<ulong>(()=>
                {
                    ulong somme = 1;
                    foreach (var index in Enumerable.Range(1, value))
                    {
                        somme *= (ulong)index;
                    }
                    return somme;
                })));                
            }
            Task.WaitAll(pool.ToArray());
            foreach (var value in pool)
            {
                Console.WriteLine(value.Result);
            }
            Console.ReadKey();
        }      
    }
}

Dans ce cas, les trois tâches sont exécutées simultanément et l'ensemble du cycle prend environ 1000 ms au lieu de 5000. A côté de WaitAll, vous avez également une méthode WhenAll que vous pouvez utiliser pour planifier une méthode de continuation après la fin de toutes les tâches.

Au lieu d'attendre que toutes les tâches soient terminées, vous pouvez également attendre que l'une des tâches soit terminée. Vous utilisez la méthode WaitAny pour cela.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Listings11
{
    class Program
    {

        static void Main(string[] args)
        {
            var pool = new List<Task<ulong>>();
            foreach(var value in Enumerable.Range(1,5))
            {
                pool.Add(Task.Run(new Func<ulong>(()=>
                {
                    ulong somme = 1;
                    foreach (var index in Enumerable.Range(1, value))
                    {
                        somme *= (ulong)index;
                    }
                    return somme;
                })));                
            }
            Task.WhenAll(pool.ToArray())
                .ContinueWith(t =>
                {
                    foreach (var value in t.Result)
                    {
                        Console.WriteLine(value);
                    }
                });
            
            Console.ReadKey();
        }      
    }
}

la classe Parallel

L'espace de noms System.Threading.Tasks contient également une autre classe qui peut être utilisée pour le traitement parallèle. La classe Parallel a quelques méthodes statiques (For, ForEach et Invoke) que vous pouvez utiliser pour paralléliser le travail.

Le parallélisme consiste à prendre une certaine tâche et à la diviser en un ensemble de tâches connexes qui peuvent être exécutées simultanément. Cela signifie également que vous ne devriez pas passer par votre code pour remplacer toutes vos boucles par des boucles parallèles. Vous devez utiliser la classe Parallel uniquement lorsque votre code n'a pas besoin d'être exécuté séquentiellement.

L'augmentation des performances avec le traitement parallèle se produit uniquement lorsque vous avez beaucoup de travail à faire qui peut être exécuté en parallèle. Pour les ensembles de travail plus petits ou pour le travail devant synchroniser l'accès aux ressources, l'utilisation de la classe Parallel peut nuire aux performances.

La meilleure façon de savoir si cela fonctionnera dans votre situation est de mesurer les résultats.

L’exemple suivant montre un exemple d'utilisation de Parallel.For et Parallel.ForEach.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Listings12
{
    class Program
    {

        static void Main(string[] args)
        {

            ParallelLoopResult pool = Parallel.For(1, 10, (value) =>
            {
                ulong somme = 1;
                foreach (var index in Enumerable.Range(1, value))
                {
                    somme *= (ulong)index;
                }
                Console.WriteLine($"{value}! = {somme}");
            });  
            
            Console.ReadKey();
        }      
    }
}

3! = 6
4! = 24
6! = 720
8! = 40320
2! = 2
7! = 5040
9! = 362880
1! = 1
5! = 120

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Listings13
{
    class Program
    {

        static void Main(string[] args)
        {

            ParallelLoopResult pool = Parallel.ForEach(Enumerable.Range(1,10), (value) =>
            {
                ulong somme = 1;
                foreach (var index in Enumerable.Range(1, value))
                {
                    somme *= (ulong)index;
                }
                Console.WriteLine($"{value}! = {somme}");
            });  
            
            Console.ReadKey();
        }      
    }
}

2! = 2
1! = 1
4! = 24
3! = 6
5! = 120
10! = 3628800
8! = 40320
9! = 362880
7! = 5040
6! = 720

Vous pouvez annuler la boucle en utilisant l'objet ParallelLoopState. Vous avez deux options pour faire ceci: Break ou Stop. Break garantit que toutes les itérations en cours d'exécution seront terminées. Stop juste termine tout. Le code suivant montre un exemple.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Listings14
{
    class Program
    {

        static void Main(string[] args)
        {

            ParallelLoopResult pool = Parallel.ForEach(Enumerable.Range(1,10), (int value, ParallelLoopState state) =>
            {                
                ulong somme = 1;

                foreach (var index in Enumerable.Range(1, value))
                {
                    somme *= (ulong)index;
                }

                if (somme % 2 == 0)
                    state.Break();
                Console.WriteLine($"{value}! = {somme}");
            });  
            
            Console.ReadKey();
        }      
    }
}

Lors de la rupture de la boucle parallèle, la variable résultat a une valeur IsCompleted de false et une valeur LowestBreakIteration est égale à value quand il a levé l’appel de Break. Lorsque vous utilisez la méthode Stop, la valeur LowestBreakIteration est null.

A propos de SUPINFO | Contacts & adresses | Enseigner à SUPINFO | Presse | Conditions d'utilisation & Copyright | Respect de la vie privée | Investir
Logo de la société Cisco, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société IBM, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Sun-Oracle, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Apple, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Sybase, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Novell, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Intel, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Accenture, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société SAP, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Prometric, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Toeic, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo du IT Academy Program par Microsoft, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management

SUPINFO International University
Ecole d'Informatique - IT School
École Supérieure d'Informatique de Paris, leader en France
La Grande Ecole de l'informatique, du numérique et du management
Fondée en 1965, reconnue par l'État. Titre Bac+5 certifié au niveau I.
SUPINFO International University is globally operated by EDUCINVEST Belgium - Avenue Louise, 534 - 1050 Brussels