Aktuelle Posts

Parallel Computing: Möglichkeiten der Parallelisierung erkennen und beurteilen


by Marc André Zhou April 06, 2011 03:51
Nach der Verschiebung der Hardwareentwicklung im CPU-Bereich steht der Entwickler vor einer neuen technischen Herausforderung. Heutige Prozessoren (CPUs) besitzen in der Regel mehr als einen Kern (Core), wobei möglichst alle genutzt werden sollten. Die Nutzung der Kerne geschieht allerdings nicht automatisch, sondern muss durch den Entwickler explizit eingefordert werden. Artikel erschienen im dot.net Magazin.

Tags:

.NET | Artikel | C# | Parallel FX | Parallel Task Library | PLINQ | Threading

Thread Priority und Task Parallel Library: Ist das möglich?


by Marc André Zhou March 22, 2011 01:57

Auf der letzten BASTA! im Februar wurde mir die Frage gestellt: Kann ich die Priorität (Priority) eines Tasks festlegen?

Diese Frage möchte ich nun in dem folgenden Post ausführlich beantworten. Wie ich bereits sagte, kann die Priorität beeinflusst werden. Aber zunächst der Reihe nach.

Task und Thread
Konkret geht es darum die Priorität eines Threads zu ändern der einen Task ausführt. Die Priorität eines Tasks kann nicht festgelegt werden. Ein Task Objekt kann höchstens eine Information über die gewünschte Priorität mitführen. Das Task Objekt selbst ist ja bekanntlich nicht ausführungsfähig, sondern benötigt immer einen Thread. D. h. jeder Task wird früher oder später an einen Thread gebunden. An dieser Stelle muss nun eingegriffen werden und ein Thread mit gewünschter Priorität an den Task gebunden werden. Dies ist mittels eigenen Task Scheduler möglich.

Implementierung eines eigenen Task Scheduler
Durch die Umsetzung eines eigenen Task Scheduler besteht die Möglichkeit, auf die Einstellungen der (Ausführungs-) Threads Einfluss zu nehmen. Innerhalb eines eigenen Task Schedulers werden die verfügbaren Threads mit den gewünschten Werten (Apartment State, Priority usw.) angelegt. Anschließend kann der umgesetzte Task Scheduler dazu verwendet werden, die Tasks auszuführen.

Wie sieht so was konkret aus?
Ein eigener Task Scheduler wird von der Klasse TaskScheduler aus dem Namensraum System.Threading.Tasks abgeleitet. Die wichtigsten zu implementierenden Methoden sind:

  • QueueTask
  • GetScheduledTasks
  • TryExecuteTaskInline

Der Methode QueueTask wird ein neuer Task zu Ausführung übergeben. Dieser wird dann zunächst einer Warteschlange hinzugefügt und verarbeitet, sobald Ressourcen verfügbar werden. Die Methode GetScheduledTasks gibt eine Liste der aktuell eingeplanten Task Objekte zurück. Die Methode TryExecuteTaskInline versucht einen Task auf dem aktuellen Thread auszuführen.

Nachfolgend werden die wichtigsten Implementierungen erläutert:

public class TaskThreadPriority : TaskScheduler, IDisposable
{
  private ThreadPriority priority = ThreadPriority.Normal;
  private ApartmentState apState = ApartmentState.MTA;

  private Lazy<Thread[]> availableThreads = null;
  private BlockingCollection<Task> queuedTasks;

  [ThreadStatic]
  private static bool currentThreadIsActive;
   
  private int degreeOfParallelism = 1;

  public TaskThreadPriority(ThreadPriority priority, int degreeOfParallelism)
  {
    this.degreeOfParallelism = degreeOfParallelism;
    this.priority = priority;
    queuedTasks = new BlockingCollection<Task>();

    InitializeScheduler(this.degreeOfParallelism, this.priority);
  }

  private void InitializeScheduler(int parallelismLevel, ThreadPriority priority)
  {
    // Set up threads
    availableThreads = new Lazy<Thread[]>(() =>
      {
        Thread[] threads = new Thread[parallelismLevel];
        for (int i = 0; i < threads.Length; i++)
        {
          threads[i] = new Thread(ProcessingRequests);
          threads[i].IsBackground = true;
          threads[i].Priority = priority;
          threads[i].SetApartmentState(apState);
          threads[i].Start();
        }
        return threads;
      });
  }
  ...
}
 

Dem Konstruktor können die wesentlichen Informationen, die zur Anlage des eigenen Task Schedulers notwendig sind, übergeben werden. Der Parameter priority legt die gewünschte Priorität für die späteren Threads fest und über den Parameter degreeOfParallelism kann die maximale Anzahl aktiver Thread eingestellt werden.

Die private Methode InitializeScheduler bereitet den Scheduler für die Ausführung vor. Innerhalb der Methode werden die gewünschte Anzahl Threads angelegt. Als Thread-Delegate wird die Methode ProcessingRequest mit folgender Implementierung übergeben:

private void ProcessingRequests()
{
  foreach (var task in queuedTasks.GetConsumingEnumerable())
  {               
    currentThreadIsActive = true;
    base.TryExecuteTask(task);
    currentThreadIsActive = false;
  }
}

Die Methode ProcessingRequests ist für die Verarbeitung anstehender Task-Objekte zuständig. Neue Tasks Objekte werden über die Methode QueueTask der Warteschlange hinzugefügt. Sobald ein wartender Task aus der Warteschlange entnommen wurde, wird dieser der TryExecuteTask Methode zur Ausführung übergeben.

protected override void QueueTask(Task task)
{
  var forceIgnore = availableThreads.Value;
  lock (queuedTasks)
  {
    queuedTasks.Add(task);
  }           
}

Als Warteschlange wird eine BlockingCollection verwendet. Diese ist zu finden in dem Namensraum System.Collections.Concurrent und gehöhrt zu den neuen - unter .NET 4.0 eingeführten - thread-safe Collections. D. h. ein separater Schutz gegen gleichzeitige/konkurrierende Zugriffe ist out-of-the-box gewährleistet. Der eigene Scheduler setzt mithilfe der BlockingCollection das typische Design-Pattern Producer/Consumer um. Der Producer ist in diesem Fall die Anwendung, die den Scheduler verwendet. Die Consumer sind die einzelnen Threads, die anstehende Tasks verarbeiten.

Verwendung des Custom Task Schedulers

Die Verwendung des eigenen Schedulers ist relativ einfach, wie das nachfolgende Beispiel verdeutlicht:

public void UsingTasksCustomScheduler()
{
    CancellationTokenSource cts = new CancellationTokenSource();
    CancellationToken token = cts.Token;

    TaskThreadPriority taskPrio = new TaskThreadPriority(ThreadPriority.Lowest, 8);

    Task[] tasks = new Task[WORKLOAD];
    int i = 0;
    foreach (List<int> list in allLists)
    {
        tasks[i] = Task.Factory.StartNew(() => { list.Sort(); },
        token, TaskCreationOptions.None, taskPrio);
        i++;
    }
    Task.WaitAll(tasks);
}

Es muss lediglich eine Instanz des eigenen Task Schedulers erstellt werden und diese kann dann z. B. der Task.Factory.StartNew Methode übergeben werden.

Die gesamte Implementierung des Schedulers ist im Anhang zu finden.

HINWEIS:
Die hier gezeigte und zur Verfügung gestellte Implementierung dient lediglich der Demonstration und eignet sich nicht für den Einsatz in einer produktiven Umgebung. Innerhalb der Dispose-Methode muss z. B. noch sichergestellt werden, dass alle Threads ordnungsgemäß beendet wurden.

 

TaskThreadPriority.cs (2,80 kb)

Tags: ,

.NET | C# | Parallel FX | Parallel Task Library | Threading

BASTA! TV: Ist Parallel Computing in .NET wirklich schon Daily Business?


by Marc André Zhou May 09, 2010 09:31

Mit .NET 4.0 haben Entwickler neue Möglichkeiten in Hinsicht auf die parallele Programmierung, die in Zukunft immer wichtiger werden wird. Bis vor kurzem konnte man sich darauf verlassen, dass die Geschwindigkeit von Programmen durch den Einsatz einer neuen CPU-Generation verbessert werden kann, doch dies gehört nun der Vergangenheit an. Jetzt hat man die Möglichkeit durch den Einsatz mehrerer CPU-Kerne in den PCs die Leistung zu steigern, doch um mehrere CPU-Kerne effektiv nutzen zu können, ist die parallele Programmierung von Nöten.

Auf der BASTA! Spring 2010 hat Katharina Friedrich, Redakteurin des dot.NET Magazins, die Möglichkeit genutzt, mit Marc André Zhou, Logica Deutschland GmbH & Co. KG, unter anderem über das Thema Parallel Computing zu sprechen.

Zum Interview >

Tags: ,

.NET | .NET Features | Buch | C# | Parallel FX | Threading

BASTA! TV: Parallel Computing


by Marc André Zhou February 27, 2010 11:46

Parallele Programmierung wird nicht nur für die .NET-Entwickler in Zukunft immer wichtiger werden, sondern allgemein an Bedeutung gewinnen. Steigerte man bisher die Leistungsfähigkeit von Anwendungen vor allem durch den Einsatz einer neuen Prozessorgeneration, so wird dies heute durch den gleichzeitigen Einsatz mehrerer CPU-Kerne gelöst. Grund genug, das Thema auf der BASTA! Spring 2009 genauer unter die Lupe zu nehmen. Marc André Zhou spricht in seiner Session über die Grundlagen der parallelen Programmierung in .NET 4.0 mit Parallel FX (Parallel Extension). Beginnend mit PLINQ über Tasks bis hin zu Future Objekt und Schleifen führt Zhou in die parallele Programmierung in .NET 4.0 ein. Zum Video ...

Tags: ,

.NET | .NET Features | C# | Parallel FX | Parallel Task Library | PLINQ | Threading

Axum (vormals: MAESTRO) – Eine domänenspezifische Sprache für parallele Programmierung unter .NET


by Marc André Zhou December 02, 2009 04:01
Die kommende .NET-Framework-Version 4.0 beinhaltet wesentliche Verbesserungen, u. a. in Bezug auf parallele Verarbeitung. Die Task Parallel Library sowie die PLINQ-Erweiterung unterstützen den Entwickler, um einfach und effektiv parallele Verarbeitung in eigene Anwendungen zu integrieren. Die neuen API-Erweiterungen ermöglichen zwar die einfache Nutzung von Parallelität, dennoch verbleiben typische Probleme wie Race Conditions und Deadlocks, die der Entwickler beachten und lösen muss. Lesen Sie mehr in der aktuellen Ausgabe des dot.net Magazins.

Tags: , ,

.NET | .NET Features | Artikel | C# | Parallel FX | Parallel Task Library | PLINQ | Threading

Don't use volatile to synchronize your thread code ...


by Marc André Zhou June 19, 2009 08:34

EXAMPLE CODE IS ON THE WAY .... 

The C# Language contains the keyword volatile and I think this keyword is obsolete. Beside the fact that the using of volatile is often wrong and not necessary, the volatile keyword is often misinterpreted.

The volatile keyword gives often the wrong feeling of secure multithread code. Sometimes the volatile keyword is used to guarantee that a shared variable is not accessible from more than one thread during the same time.

BUT: The volatile keyword is not able to do this kind of service. The only way to protect a shared variable is to use a synchronization object (Monitor,Mutex) or the special static methods from the Interlocked class. 

Why? I’ve a simple example to demonstrate the effect of the volatile keyword and an example to solve the problem. At the end I’m going to explain the volatile keyword and the case, when it is necessary to use volatile.

The example below shows the wrong use of the volatile keyword:

Two threads are started and both threads are operating with the shared variable counter. The shared variable counter is incremented by both threads. If  everything is working fine, the result of the counter variable should be: ITERATION (const.) * Threads (in this case 2).

BUT: The counter variable is different from the expected value. Why? The keyword volatile is not helpful to guarantee thread-safety. In this case, the ++ Operation is not an atomicity operation. The volatile keyword guarantees that the “newest” value is reading from the memory and the value won’t be stored in a cache (L1, L2) but the value can be stored inside a CPU register. 

What happens exactly?
The ++ Operation contains more than one instruction to complete the operation. The following Intermediate Language Code (IL) shows the necessary instruction for the ++ Operation.

As you can see, the operation needs three steps to complete:

  • First: Load variable value (counter)
  • Second: Do the calculation
  • Third: Write back the result

On each operation it is possible that the thread will be interrupted. For example: A thread reads the variable counter and then the thread is interrupted, the variable value (counter) is stored inside the TIB (Thread Information Block). When the thread comes back to the processor (running again), the thread is using the stored value from the TIB. When a different thread has changed the value in the meantime, the old thread has no knowledge about this change. The re-activated thread is working with an old value. Writes the thread the value back to the memory, he overwrites the changes … Memory corrupted.   

The volatile keyword is not enough and not necessary on a x86 Architecure based CPU with Cache Coherency (Strong Memory Model)

The only way to solve the problem is to use a lock or the Interlocked class methods.

Tags: , , , , ,

.NET | C# | Threading