Utilisation des "Task" en 5 min

1. Quelques patterns courants

Lancer l’exécution d’une tâche

Une tâche est une opération asynchrone qui se lance de la façon suivante (à partir du framework 4.5):

Task t = Task.Run(() => { ... Code à exécuter...});

Une autre syntaxe en utilisant la "Task.Factory":

Task t = Task.Factory.StartNew(() => { ... Code à exécuter...});

Sur les versions antérieures au framework 4.5:

Task t = Task.Factory.StartNew(() => { ... Code à exécuter... });
Task.Run() et Task.Factory.StartNew() se sont pas strictement équivalents

Les 2 notations ne sont pas strictement équivalentes. La notation Task.Run() est équivalente à:

Task.Factory.StartNew(() => 
    {
        // Code de la tâche
    },
    CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);

Plus de détails sur l’option TaskCreationOptions.DenyChildAttach dans Notion de tâche parente.

Attendre la fin de l’exécution de la tâche

Equivalent à JOIN pour les threads:

t.Wait();

Attendre la fin de l’exécution de plusieurs tâches:

Si on a une liste de tâche:

Task[] tasks = new Task[3]; 
tasks[0] = Task.Run(() => { ... Some work;});
tasks[1] = Task.Run(() => { ... Some work;}); 
tasks[2] = Task.Run(() => { ... Some work;}); 

Pour attendre la fin de l’exécution de toutes les tâches:

Task.WaitAll(tasks);

Attendre la fin de l’exécution de n’importe quelle tâche dans une liste de tâches:

Si on prends l’exemple précédent, pour attendre la fin de l’exécution de n’importe quelle tâche dans une liste de tâches, il suffit de faire:

int i = Task.WaitAny(tasks);

Le type de retour est le type générique qui définit la tâche: Task<int>.

Cette fonction permet par exemple d’effectuer un traitement à chaque fois qu’une tâche a terminé son exécution:

while (tasks.Length > 0) 
{ 
  int i = Task.WaitAny(tasks); 
  // etc... 
} 

Les appels à Wait(), WaitAll() et WaitAny() sont bloquants.

Récupérer le résultat d’une tâche

Task t = Task.Run(() => { return 42;}); 
int taskResult = t.Result; 

Effectuer un travail après l’exécution de la tâche (continuation):

On peut exécuter un bout de code à la fin de l’exécution d’une tâche. Ce bout de code s’appelle une continuation. On peut effectuer autant de continuation que l’on veut. Pour effectuer une continuation, il suffit de faire appel à la fonction ContinueWith():

Task t = Task.Run(() => { return 42;}) 
     .ContinueWith((i) => { return i.Result*2;}); 
int taskResult = t.Result; 

Dans le cas de l’exemple, "I" est un entier à cause du type générique précisé dans la définition de la tâche Task<int>.

Retour de la fonction ContinueWith

Le retour de la fonction ContinueWith est la tâche "t" qui n’est pas la même instance que la tâche renvoyée par Task.Run().
Si on fait:

Task t = Task.Run(() => { return 42;});
t.ContinueWith((i) => { return i.Result*2;}); 
int taskResult = t.Result; 

Le résultat t.Result contiendra le résultat de Task.Run() et non de ContinueWith(). Pour avoir le résultat de ContinueWith, il faut faire:

Task t = Task.Run(() => { return 42;}) 
t = t.ContinueWith((i) => { return i.Result*2;}); 
int taskResult = t.Result; 

ATTENTION: en utilisant t.Result après un ContinueWith() comme dans l’exemple précédent, l’exécution est bloquante, c’est-à-dire qu’on attend la fin de l’exécution de la tâche. Il n’est pas nécessaire d’utiliser Task.Wait()

Options TaskCreationOptions.RunContinuationsAsynchronously et TaskContinuationOptions.RunContinuationsAsynchronously

Comme indiqué plus haut, les "continuations" sont exécutées, par défaut, de façon synchrone dans le même thread qui passe la tâche initiale dans son état final. Si la tâche initiale est terminée au moment de la création de la "continuation", elle sera exécutée, par défaut, dans le thread qui a créé la "continuation".

A partir du framework 4.6, on peut indiquer qu’une "continuation" doit s’exécuter de façon asynchrone:

  • A la création de la tâche initiale: en utilisant l’option TaskCreationOptions.RunContinuationsAsynchronously, on peut indiquer que les "continuations" se déclenchant à la suite de cette tâche seront exécutées de façon asynchrone. Cette option peut être utilisée avec les méthodes de création de "Task": Task.Factory.StartNew(), les constructeurs Task() ou le constructeur TaskCompletionSource().
  • A la création de la "continuation": en utilisant TaskContinuationOptions.RunContinuationsAsynchronously, on peut indiquer que la "continuation" sera exécutée de façon asynchrone. Cette option peut être utilisée par les méthodes de création d’une continuation comme Task.ContinueWith().

Notion de tâche parente

Lorsqu’on définit une tâche dans une tâche:

  • La tâche initiale est la tâche parente et
  • La tâche dans la première tâche est la tâche enfant.

On distingue les tâches enfant détachées, c’est le type par défaut si on ne précise rien et les tâches enfant attachées.

Tâche enfant détachée

Une tâche enfant détachée se définit de cette façon:

var parent = Task.Factory.StartNew(() => {
  // Code tâche parente

  var child = Task.Factory.StartNew(() => {
    // Code tâche enfant
  });
});

Pas d’attente de la tâche enfant par défaut
Lorsque la tâche enfant est détachée, la tâche parente n’attends pas la fin de l’exécution de la tâche enfant pour passer dans un des états terminés (par exemple TaskStatus.RanToCompletion). Il faut donc utiliser Wait() pour attendre la fin de la l’exécution de la tâche enfant.

Pas de propagations des exceptions
La tâche parente ne propagent pas les éventuelles exceptions aux tâches enfant. De même, la tâche enfant ne propage pas ses éventuelles exceptions à la tâche parente. Il faut que la tâche parente utilise une “continuation” ou vérifie l’état du statut de la tâche enfant pour savoir si une exception a été lancée.

En cas d’annulation en utilisant un CancellationToken

Si on utilise le même CancellationToken entre la tâche parente et la tâche enfant:

  • Si on annule au niveau de la tâche enfant: pas de propagation vers la tâche parente.
  • Si on annule au niveau de la tâche parente: si l’exécution de la tâche enfant a déjà commencé, son exécution ne sera pas stoppé. En revanche, si l’exécution de la tâche enfant n’a pas débuté, elle ne sera pas lancée.

Tâche enfant attachée

Contrairement au type de tâche enfant par défaut, il faut rajouter l’option AttachedToParent pour qu’une tâche enfant soit attachée à la tâche parente:

var parent = Task.Factory.StartNew(() => {
  // Code tâche parente

  var child = Task.Factory.StartNew(() => {
    // Code tâche enfant
  }, TaskCreationOptions.AttachedToParent);
});

Le statut de la tâche parente dépends de ceux des tâches enfant.

Attente de la tâche enfant
Il n’est pas nécessaire d’utiliser Wait() pour attendre la fin de l’exécution de la tâche. Par défaut la tâche parente attends la fin de l’exécution de ses enfants.

Propagation des exceptions
Les exceptions qui surviennent éventuellement dans la tâche enfant sont propagées à la tâche parente et affectera le statut final de la tâche parente.

En cas d’annulation en utilisant un CancellationToken
Si on utilise le même CancellationToken entre la tâche parente et la tâche enfant attachée, en cas d’annulation dans la tâche enfant, elle sera propagée dans la tâche parente et elle affectera le résultat de la tâche parente.

Attention au démarrage de la tâche parente:

Si on utilise Task.Run(), il n’est pas possible d’utiliser des tâches enfant attachées. Les tâches enfant sont systématiquement détachées.
En revanche, Task.Factory.StartNew() permet de créer des tâches enfant attachées.

L’équivalent de:

Task.Run(() => 
    {
        // Code de la tâche
    });

est:

Task.Factory.StartNew(() => 
    {
        // Code de la tâche
    },
    CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);

Plus de détails sur MSDN.

Utilisation de l’option de création “LongRunning”

Par défaut, le processus dans lequel est exécuté une tâche n’attends, pas la fin de son exécution pour sortir. Pour que le processus attende la fin de l’exécution d’une tâche avant de sortir, il faut utiliser l’option TaskCreationOptions.LongRunning.

TaskCreationOptions.LongRunning va aussi imposer d’exécuter la tâche un thread particulier.

Par exemple:

var cancellationTokenSource = new CancellationTokenSource();
Task.Factory.StartNew(() => { ... Code à exécuter...},
  TaskCreationOptions.LongRunning,
  cancellationTokenSource.Token);

Il est fortement conseillé d’utiliser un CancellationToken dans le cas où on utilise l’option LongRunning.

FromException(), FromCanceled() et FromResult()

A partir du framework 4.6, on peut créer directement une "Task" avec un statut particulier:

  • Si on souhaite une "Task" avec le statut TaskStatus.RanToCompletion, on peut utiliser Task.FromResult() en indiquant directement le résultat,
  • Si on souhaite une "Task" avec le statut TaskStatus.Canceled, on peut utiliser Task.FromCanceled() avec un CancellationToken,
  • et si on souhaite une "Task" avec le statut TaskStatus.Faulted, on peut utiliser Task.FromException() avec une exception.

L’intérêt de ces nouvelles méthodes est d’effectuer un traitement synchrone lorsqu’on est obligé d’utiliser d’avoir une signature de méthode asynchrone. Par exemple, si une interface impose une signature asynchrone du type:

public Task<TResult> ExecuteAsync()
{
    // ...
}

Toutefois dans l’implémentation, au lieu de lancer une exécution asynchrone, si on peut vouloir affecter directement le résultat (parce qu’on le possède déjà) et avoir une "Task" dont le résultat est déjà affectée. Dans ce cas:

  • Avant le framework 4.6, on peut utiliser la classe TaskCompletionSource,
  • A partir du framework 4.6, on peut utiliser directement les fonctions FromResult(), FromCanceled() ou FromException().

Par exemple:

public Task<int> GetResultAsync(CancellationToken cancellationToken)
{
    if (cancellationToken.IsCancellationRequested)
    {
        return Task.FromCanceled<int>(cancellationToken);
    }

    try
    {
        return Task.FromResult(GetResultSync());
    }
    catch (Exception exception)
    {
        return Task.FromException(exception);
    }
}

public int GetResultSync()
{
    // ...
}

2. Gestion des exceptions

Sans gestion explicite, les exceptions qui surviennent dans les "Task" ne seront jamais remontées. La "Task" stoppera son exécution et l’exception passera inaperçue.
Une façon simple de gérer les exceptions qui peuvent survenir dans une "Task" est d’utiliser une "continuation":

Task task =  Task.Factory.StartNew(() => DoPrintConfigPage(serial))
    .ContinueWith(tsk => {
        logger.ErrorFormat("Exception occured: {0}", tsk.Exception.ToString());
    }, TaskContinuationOptions.OnlyOnFaulted);
Attention au retour de ContinueWith

Dans l’exemple précédent, task contient l’instance de la continuation et non l’instance de la "Task" qui est lancée par Task.Factory.StartNew()

AggregateException

Dans l’exemple précédent, tsk.Exception est une instance de "AggregateException". Cette exception contient d’autres exceptions accessibles avec tsk.Exception.InnerExceptions (AggregateException.InnerExceptions).

CancellationToken

Cet objet permet de signaler à une ou plusieurs tâches d’annuler leur exécution. Mais attention, à l’intérieur de la tâche, il faut explicitement vérifier qu’une annulation est requise sinon la tâche ne s’arrêtera jamais.
Une instance de "CancellationToken" est liée à une "CancellationTokenSource":

var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;

La demande d’annulation se fait au niveau de "CancellationTokenSource":

tokenSource.Cancel();

Ainsi, on peut utiliser le "CancellationToken" au moment de lancer la tâche:

var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
var task = Task.Factory.StartNew(() => {
  if (token.IsCancellationRequested)
  {...}
}, token);
L’utilisation de "CancellationToken" doit respecter certaines règles:
  • Si une tâche est en cours d’exécution, son statut est TaskStatus.Running. En cas d’annulation, pour que la tâche passe au statut TaskStatus.Canceled, il faut qu’une exception de type OperationCanceledException soit lancée à l’intérieur de la tâche (ne pas oublier d’inclure le token lors du lancement de l’exception: throw new OperationCanceledException(token)).
    Une façon de lancer directement cette exception est d’exécuter: token.ThrowIfCancellationRequested() (l’exception sera lancée seulement si l’annulation a été signalée).
  • Si une annulation est requise (i.e. si tokenSource.Cancel() a été exécutée), toutes les utilisations suivantes de l’instance de la "CancellationToken" vont conduire à une OperationCanceledException.

Par exemple si on implémente:

var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
var task = Task.Factory.StartNew(() => {
  if (token.IsCancellationRequested)
  {...}
}, token);
tokenSource.Cancel();
task.Wait(token);

La ligne task.Wait(token) lancera une OperationCanceledException. Ainsi les utilisations extérieures à la tâche de token pouvant être exécutées après l’annulation doivent être entourées d’un bloc try...catch (OperationCanceledException) {}.

Utilisation d’une continuation

Si on utilise un "CancellationToken", on peut utiliser une continuation pour notifier l’annulation de la tâche:

var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
var task = Task.Factory.StartNew(() => {
  while (true)
    {
      token.ThrowIfCancellationRequested();
       ...
    }
}, token);
var continuationTask = task.ContinueWith(tsk => {
        logger.InfoFormat("Task canceled !");
    }, TaskContinuationOptions.OnlyOnCanceled);
tokenSource.Cancel();
task.Wait();
ATTENTION: exécution de la continuation

Pour que la continuation soit exécutée, il faut que la tâche soit dans le statut TaskStatus.Canceled c’est-à-dire que la ligne token.ThrowIfCancellationRequested() soit exécutée.

Exemple d’une implémentation utilisant des continuations pour l’annulation et pour une exception:

var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
var task = Task.Factory.StartNew(() => {
  while (true)
    {
      token.ThrowIfCancellationRequested();
       ...
    }
}, token);
var exceptionTask = task.ContinueWith(tsk => {
        logger.InfoFormat("Task canceled !");
    }, TaskContinuationOptions.OnlyOnCanceled);
var continuationTask = task.ContinueWith(tsk => {
        logger.ErrorFormat("Exception occured: {0}", tsk.Exception.ToString());
    }, TaskContinuationOptions.OnlyOnFaulted);

3. TaskScheduler

Cet objet permet de manipuler un ensemble de tâches comme le threadpool pour les threads. Ainsi on peut ajouter des tâches dans une file d’attente QueueTask(), enlever une tâche de la file d’attente TryDequeue() etc…

Task et thread principal

Il est possible de démarrer une tâche associée au contexte de synchronisation du thread principal en particulier si on veut intervenir sur l’interface graphique pour effectuer certains traitements. En effet il n’est pas possible d’interagir avec des objets définis dans le thread principal puisque le contexte de synchronisation n’est pas le même: SynchronizationContext.

Pour obtenir une tâche associé au contexte de synchronisation du thread principal:

TaskScheduler.FromCurrentSynchronizationContext();

Problème lecteur/écrivain

Depuis le framework 4.5, TaskScheduler permet de répondre au problème lecteur/écrivain avec ConcurrentExclusiveSchedulerPair.

Cette classe fournit une paire d’écrivains et de lecteurs. Les lecteurs peuvent s’exécuter de façon non exclusives, les écrivains s’exécutent de façon exclusives. Les écrivains ont la priorité par rapport aux lecteurs.

var pair = new ConcurrentExclusiveSchedulerPair(TaskScheduler.Default); 
var writers = new Task.Factory(pair.ExclusiveScheduler); 
var readers = new Task.Factory(pair.ConcurrentScheduler);

writers est un objet qui permet de créer des tâches d’écrivain et reader permet de créer des tâches de lecteurs.

4. Exécution asynchrone

Les continuations permettent d’exécuter des tâches de façon asynchrone mais il est possible d’effectuer des traitements asynchrones en utilisant la Task.Factory ().

Cette classe permet de définir un délégué qui sera exécutée à la fin de l’exécution en utilisant FromAsync().

Par exemple dans le cadre de la lecture d’un fichier:

FileInfo fi = new FileInfo(path);  
byte[] data = null;  
data = new byte[fi.Length];  
FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, data.Length, true);  

//Task returns the number of bytes read  
Task task = Task.Factory.FromAsync( fs.BeginRead, fs.EndRead, data, 0, data.Length, null); 
Appel non bloquant

Cet exemple permet de montrer comment on peut créer une tâche en utilisant FromAsync() mais il n’est pas bloquant.

Voir Exécution asynchrone avec “await” et “async” en 5 min pour avoir plus détails sur l’exécution asynchrone en utilisant async et await.

5. TaskCompletionSource

L’objet TaskCompletionSource encapsule une “task” sur laquelle il est possible d’effectuer tous les traitements possibles au même titre qu’une “task” classique. L’intérêt de cette “task” encapsulée est qu’elle ne possède pas de corps, il n’y a donc pas besoin d’une implémentation particulière pour l’utiliser et on bénéficie de tous les autres éléments d’une “task” classique comme les “continuations” ou la gestion des exceptions.

Le point d’entrée de cette “task” est l’objet TaskCompletionSource. On peut donc préciser le résultat de la “task” encapsulée ou affecter une exception en utilisant directement l’objet TaskCompletionSource. Pour rappel, lorsqu’on affecte un résultat à une “task”, son état change à TaskStatus.RanToCompletion. De même si on affecte une exception à une “task”, son état change pour passer en TaskStatus.Faulted. Enfin, annuler la “task” affecte l’état TaskStatus.Canceled. RanToCompletion, Faulted et Canceled sont des états finaux affectées lorsque la tâche est terminée.

L’intérêt de TaskCompletionSource peut être d’affecter une “continuation” à la “task” encapsulée et de maîtriser le démarrage de la continuation en affectant un état final à la “task” encapsulée en utilisant une des méthodes citées précédemment:

  • Affecter un résultat en faisant TaskCompletionSource.SetResult() pour que la “task” passe à l’état TaskStatus.RanToCompletion.
  • Affecter une exception en faisant TaskCompletionSource.SetException() pour que la “task” passe à l’état TaskStatus.Faulted.
  • Annuler la “task” en utilisant un objet CancellationToken passé en paramètre de TaskCompletionSource. La “task” passe alors, à l’état TaskStatus.Canceled.

Par exemple, si on utilise des “continuations”:

public class TaskUsage  
{  
  private readonly TaskCompletionSource<bool> taskCompletionSource = new TaskCompletionSource<bool>(); 
  private readonly Task canceledTask; 
  private readonly Task exceptionOccuredTask; 
 
  public TaskUsage() 
  {  
      this.canceledTask = this.taskCompletionSource.Task.ContinueWith( 
          tsk => Console.WriteLine("Canceled"), 
          TaskContinuationOptions.OnlyOnCanceled); 
      this.exceptionOccuredTask = this.taskCompletionSource.Task.ContinueWith( 
          tsk => Console.WriteLine("Exception occured: " + tsk.Exception.ToString()), 
          TaskContinuationOptions.OnlyOnFaulted); 
  } 
 
  public bool Wait(TimeSpan timeout) 
  { 
    bool executedWithinTimeout = this.taskCompletionSource.Task.Wait(timeout) 
   
    return executedWithinTimeout; 
  } 

  public void SetException(Exception e) 
  {
    this.taskCompletionSource.SetException(e); 
  } 
 
  public void Signal() 
  {
    this.taskCompletionSource.SetResult(true); 
  } 
}

Dans cet exemple, on affecte des “continuations” qui s’exécutent si on affecte un résultat ou si on affecte une exception.

Plus de détails sur MSDN.

FromException(), FromCanceled() et FromResult()

A partir du framework 4.6, on peut s’aider des fonctions FromException(), FromCanceled() et FromResult() qui permettent d’éviter d’utiliser la classe TaskCompletionSource.

Par exemple, l’équivalent de:

var taskCompletionSource = new TaskCompletionSource<TResult>();
taskCompletionSource.SetException(exception);
return taskCompletionSource.Task;


devient:

Task.FromException(exception)

Le résultat est directement une "Task" avec le statut TaskStatus.Faulted.
FromResult() et FromCanceled() permettent d'obtenir une "Task" avec un statut différent.

Pour plus de détails, on peut se reporter à FromException(), FromCanceled() et FromResult().

Task.CompletedTask

A partir du framework 4.6, on peut créer directement une "Task" avec le statut TaskStatus.RanToCompletion sans utiliser TaskCompletionSource en utilisant la propriété statique:

Task.CompletedTask

6. Les "closures"

Une "closure" est une fonction qui capture au moins une référence vers une variable libre (source wikipedia), par exemple:

var freeVariable = "Value from the outside";
Func<string,string> myFunc = funcVariable => 
{
  return funcVariable + freeVariable;
};

La variable freeVariable est une variable libre, elle est définie à l'extérieur du délégué et elle est passée dans le délégué par référence. Dans le cas d'une utilisation de la variable libre par un seul délégué, il n'y a pas de surprises sur sa valeur. En revanche, si on utilise la variable dans une boucle ou si on effectue des affectations à la variable libre dans plusieurs délégués, on peut avoir des surprises quant à sa valeur.

Utilisation d'une "closure" dans une boucle

Par exemple si on écrit:

List<Func<int>> actions = new List<Func<int>>();

int freeVariable = 0;
for (int freeVariable = 0; freeVariable < 5; freeVariable++)
{
    actions.Add(() => freeVariable * 2);
}

foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

On ne maitrise pas la valeur de la variable libre car elle est passée par référence dans le délégué. Ainsi, au moment de l'exécution du délégué, sa valeur correspondra à la dernière valeur de la variable, à savoir, 10 c'est-à-dire la dernière valeur de la référence.

Pour que la valeur soit spécifique à chaque boucle, il faut copier la variable dans une autre pour que la variable libre soit copiée par valeur:

for (int freeVariable = 0; freeVariable < 5; freeVariable++)
{
    int variableCopy = freeVariable;
    actions.Add(() => variableCopy * 2);
}

La valeur de variableCopy est spécifique à chaque itération puisque la valeur est copiée et qu'on utilise plus la référence de la variable libre.

Utilisation d'une variable dans plusieurs délégués

Par exemple si on écrit:

static void Main(string[] args)
{
  var definedDeleguate = DefineDeleguate();
  Console.WriteLine(definedDeleguate(2));
  Console.WriteLine(definedDeleguate(3));
}
 
public static Func<int,int> DefineDeleguate()
{
  var freeVariable = 1;
  Func<int, int> definedDeleguate = (funcVariable) =>
  {
    freeVariable = freeVariable + 1;
    return funcVariable + freeVariable;
  };
  return definedDeleguate;
}

Après exécution, on obtient 4 et 6. Au premier abord, on s'attendrait à 4 et 5 à cause de l'initialisation de freeVariable à 1. Cependant comme la valeur est passée au délégué par référence, la référence est gardée lors de la 2e exécution du délégué. Ainsi à la première exécution, la valeur de freeVariable est 2. Cette valeur persistera lors de la 2e exécution du délégué d'où le résultat 6.

Explication sur les "closures"

Sans rentrer dans les détails, il faut considérer une "closure" comme une classe à part entière dans laquelle se trouve la fonction correspondant au délégué et un ou plusieurs membres correspondant à une ou plusieurs variables libres.
Ainsi la vie de cette classe correspond à la vie du délégué. Sachant que la variable libre est passée par référence à la fonction, la valeur de la variable libre sera affectée durant la vie de la classe:

  • Dans une boucle, sa valeur est incrémentée pendant l'exécution de la boucle,
  • Si on exécute plusieurs fois le délégué, sa valeur sera modifiée et persistée à chaque exécution du délégué.

Les "closures" et les "tasks"

Les "tasks" utilisent des délégués et on peut être amené à utiliser des "closures". Ainsi, les mêmes problèmes peuvent se poser quant à la valeur des variables libres utilisées dans le délégué.

De la même façon, les variables libres sont passées par référence. En outre, sachant que l'exécution d'une "task" ne s'effectue rigoureusement au même moment d'une exécution à l'autre par rapport à l'enchaînement des instructions, on peut aussi avoir des surprises sur la valeur de variables libres quand elles sont utilisées dans le corps d'une "task".

Par exemple, si on écrit:

int freeVariable = 45; 
Task.Factory.StartNew( () =>
{
   freeVariable++;
}
);
// ...
Console.WriteLine(freeVariable);

On peut obtenir pour certaines exécution 45 et pour d'autres 46, suivant si la "task" a eu le temps de s'exécuter ou non.

Leave a Reply