Multithreading in C#.NET



Ähnliche Dokumente
Suche schlecht beschriftete Bilder mit Eigenen Abfragen

Datensicherung. Beschreibung der Datensicherung

Objektorientierte Programmierung für Anfänger am Beispiel PHP

Multi-Threading. Ralf Abramowitsch Vector Informatik GmbH

Systeme 1. Kapitel 6. Nebenläufigkeit und wechselseitiger Ausschluss

Übung: Verwendung von Java-Threads

TTS - TinyTimeSystem. Unterrichtsprojekt BIBI

icloud nicht neu, aber doch irgendwie anders

Softwaretests in Visual Studio 2010 Ultimate Vergleich mit Java-Testwerkzeugen. Alexander Schunk Marcel Teuber Henry Trobisch

Lineargleichungssysteme: Additions-/ Subtraktionsverfahren

Outlook. sysplus.ch outlook - mail-grundlagen Seite 1/8. Mail-Grundlagen. Posteingang

Architektur Verteilter Systeme Teil 2: Prozesse und Threads

! " # $ " % & Nicki Wruck worldwidewruck

Grundlagen verteilter Systeme

Professionelle Seminare im Bereich MS-Office

Handbuch Fischertechnik-Einzelteiltabelle V3.7.3

Java: Vererbung. Teil 3: super()

Urlaubsregel in David

Ihre Interessentendatensätze bei inobroker. 1. Interessentendatensätze

In 12 Schritten zum mobilen PC mit Paragon Drive Copy 11 und Microsoft Windows Virtual PC

40-Tage-Wunder- Kurs. Umarme, was Du nicht ändern kannst.

Binäre Bäume. 1. Allgemeines. 2. Funktionsweise. 2.1 Eintragen

Inhalt. 1 Einleitung AUTOMATISCHE DATENSICHERUNG AUF EINEN CLOUDSPEICHER

Monitore. Klicken bearbeiten

Programmierkurs Java

Fachbericht zum Thema: Anforderungen an ein Datenbanksystem

Berechnungen in Access Teil I

Nach der Installation kann es auch schon losgehen. Für unseren Port Scanner erstellen wir zunächst ein neues Projekt:

Delegatesund Ereignisse

ecaros2 Installer procar informatik AG 1 Stand: FS 09/2012 Eschenweg Weiterstadt

Folgeanleitung für Klassenlehrer

Datenübernahme von HKO 5.9 zur. Advolux Kanzleisoftware

Der PDF-Druck. EDIORG Software GmbH, Linz

Bedienungsanleitung für den SecureCourier

2. Einrichtung der ODBC-Schnittstelle aus orgamax (für 32-bit-Anwendungen)

Sie befinden sich hier: WebHosting-FAQ -Clients - Einrichtung und Konfiguration Outlook Express Artikel #1

DELFI. Benutzeranleitung Dateiversand für unsere Kunden. Grontmij GmbH. Postfach Bremen. Friedrich-Mißler-Straße Bremen

schnell und portofrei erhältlich bei beck-shop.de DIE FACHBUCHHANDLUNG mitp/bhv

ACHTUNG: Voraussetzungen für die Nutzung der Funktion s-exposé sind:

Hilfedatei der Oden$-Börse Stand Juni 2014

2A Basistechniken: Weitere Aufgaben

Einrichtung des Cisco VPN Clients (IPSEC) in Windows7

DOKUMENTATION VOGELZUCHT 2015 PLUS

Kontoname ist Mailanschrift Maximale Mailboxgrösse: Maximale Nachrichtengrösse: Haltezeit der Nachrichten:

Anwenderdokumentation AccountPlus GWUPSTAT.EXE

Eine Anleitung, wie Sie Mozilla Thunderbird 2 installieren und konfigurieren können. Installation Erstkonfiguration... 4

Objektorientierte Programmierung

Windows 8 Lizenzierung in Szenarien

Online-Prüfungs-ABC. ABC Vertriebsberatung GmbH Bahnhofstraße Neckargemünd

infach Geld FBV Ihr Weg zum finanzellen Erfolg Florian Mock

Erstellen einer digitalen Signatur für Adobe-Formulare

Tapps mit XP-Mode unter Windows 7 64 bit (V2.0)

Einrichtung eines -Kontos bei MS Office Outlook 2007 (Windows) Stand: 03/2011

Outlook Express: Einrichtung Account

Lizenzen auschecken. Was ist zu tun?

Stellen Sie bitte den Cursor in die Spalte B2 und rufen die Funktion Sverweis auf. Es öffnet sich folgendes Dialogfenster

-Konten für Studierende und Zugriffswege auf die Mail-Systeme der Hochschule Rhein-Waal

TechNote: Exchange Journaling aktivieren

VERWALTUNG. Postfächer, Autoresponder, Weiterleitungen, Aliases. Bachstraße 47, 3580 Mödring

Software Engineering Klassendiagramme Assoziationen

Tipps und Tricks zu Netop Vision und Vision Pro

Synchronisation in Java. Invisible Web

Kommunikations-Management

Speicher in der Cloud

Ordner Berechtigung vergeben Zugriffsrechte unter Windows einrichten

Folgeanleitung für Fachlehrer

Einrichten eines Postfachs mit Outlook Express / Outlook bis Version 2000

1&1 Webhosting FAQ Outlook Express

TeamSpeak3 Einrichten

Das Handbuch zu KNetAttach. Orville Bennett Übersetzung: Thomas Bögel

Technische Dokumentation SilentStatistikTool

Verarbeitung der Eingangsmeldungen in einem Callcenter

Installationsanleitung für CashPro im Mehrbenutzerzugriff/Netzwerkbetrieb

Sie wollen Was heißt das? Grundvoraussetzung ist ein Bild oder mehrere Bilder vom Wechseldatenträger

In 15 einfachen Schritten zum mobilen PC mit Paragon Drive Copy 10 und Microsoft Windows Virtual PC

MCRServlet Table of contents

Der Kalender im ipad

Hilfe zur Urlaubsplanung und Zeiterfassung

Drucken aus der Anwendung

Second Steps in eport 2.0 So ordern Sie Credits und Berichte

SJ OFFICE - Update 3.0

Diese Ansicht erhalten Sie nach der erfolgreichen Anmeldung bei Wordpress.

Neue Funktionen im GUI für PC-DMIS V3.x 4.x Seite 1 von 8

Zwischenablage (Bilder, Texte,...)

E Mail Versand mit der Schild NRW Formularverwaltung

Benutzerhandbuch. Leitfaden zur Benutzung der Anwendung für sicheren Dateitransfer.

Folgende Einstellungen sind notwendig, damit die Kommunikation zwischen Server und Client funktioniert:

5.2 Neue Projekte erstellen

Ein Hinweis vorab: Mailkonfiguration am Beispiel von Thunderbird

COMPUTERIA VOM SERIENBRIEFE UND ETIKETTENDRUCK

Einrichtung eines -Kontos bei Mac OS X Mail Stand: 03/2011

RT Request Tracker. Benutzerhandbuch V2.0. Inhalte

EINFACHES HAUSHALT- KASSABUCH

Im Folgenden wird Ihnen an einem Beispiel erklärt, wie Sie Excel-Anlagen und Excel-Vorlagen erstellen können.

Mit der Maus im Menü links auf den Menüpunkt 'Seiten' gehen und auf 'Erstellen klicken.

Bedienungsanleitung. Stand: Copyright 2011 by GEVITAS GmbH

Überblick. Lineares Suchen

Sie befinden sich hier: WebHosting-FAQ & Unified Messaging -Clients - Einrichtung und Konfiguration Outlook Express Artikel #1

Enigmail Konfiguration

Transkript:

Multithreading in C#.NET Proseminar Objektorientiertes Programmieren mit.net und C# Holger Stöcker [stoecker@in.tum.de] 19. Dezember 2010

Inhaltsverzeichnis 1 Einführung 3 2 Grundlagen 3 2.1 Was ist Multithreading?........................... 3 2.2 Was bietet der Einsatz von Multithreading?................ 3 2.3 Was ist ein Thread?............................. 4 3 Threadzustände 4 4 Die Thread -Klasse in C#.NET 5 5 Synchronisation von Threads 7 5.1 Beispiel eines unsychronisierten Programmes............... 8 5.2 Synchronisation mit Monitor oder lock-statement............. 9 5.3 Synchronisation mit ReaderWriterLockSlim................ 10 5.4 Synchronisation mit MethodImpl-Attribut................. 11 6 Der Einsatz von Threadpools und BackgroundWorker-Threads 12 6.1 Threadpools................................. 12 6.2 BackgroundWorker-Threads........................ 13 7 Programmierbeispiel: Simulierter Mailserver 13 7.1 Die SingleThread-Variante......................... 13 7.2 Die MultiThread-Variante.......................... 15 7.3 Konkreter Vergleich der beiden Varianten................. 16 8 Abschließende Zusammenfassung 16 9 Anhang 17 9.1 Quellcode und Binärdateien des Mailserver-Beispiels........... 17 9.2 Literaturverzeichnis............................. 17 9.3 Abbildungsverzeichnis........................... 17

1 Einführung Wenn Sie sich heute einen neuen, modernen PC zulegen, enthält dieser sehr wahrscheinlich einen Mehrkernprozessor. Dieser ist in der Lage gleichzeitig auf jedem Kern ein eigenes Programm, häufig auch Prozess genannt, auszuführen. Was für uns heute als Selbstverständlichkeit gilt, war jedoch nicht immer so. Zu Zeiten von 16 bit Betriebssystemen wie MS-DOS [1], konnte immer nur ein Programm sequentiell ausgeführt werden. Diese Form der Prozessabarbeitung hatte erhebliche Nachteile, denn sobald sich ein einzelner Prozess aufhängte, lag der gesamte PC lahm. In dieser Seminararbeit möchte ich eine grundsätzliche Einführung in die Konzepte des Multithreadings machen. Dabei liegt der Schwerpunkt darauf, wie dieses Konzept ganz konkret in der Programmiersprache C#.NET umgesetzt werden kann. 2 Grundlagen 2.1 Was ist Multithreading? Oftmals werden die zwei Begriffe Multithreading und Multitasking nicht klar voneinander abgegrenzt. Multitasking bedeutet, dass mehrere Anwendungen Parallel ausgeführt werden können. Auf einem Prozessorkern können Tasks immer nur sequentiell ausgeführt werden, also immer nur ein Task gleichzeitig. Jedoch durch schnelles Wechseln der aktiven Anwendung auf dem Kern entsteht eine Scheinparallelität. Multithreading bedeutet, dass mehrere Threads innerhalb eines Prozesses parallel ausgeführt werden können, damit ein Prozess mehrere Dinge gleichzeitig ausführen kann. 2.2 Was bietet der Einsatz von Multithreading? Multithreading bringt an vielen Stellen Performance-Vorteile, denn in der Praxis gibt es sehr oft Threads, die z.b. auf das Beenden einer Methode oder auf andere Dinge warten müssen. In diesen Wartezeiten können nun problemlos andere Threads auf der CPU arbeiten, damit diese möglichst effektiv ausgelastet werden kann. Für manche Anwendungen ist es sogar erforderlich, dass mehrere Threads eingesetzt werden. In einer Applikation, in der es eine grafische Oberfläche gibt, kann jede Interaktion, die der Benutzer durchführt (z.b. mit der Maus auf eine Schaltfläche klicken) durch einen oder mehrere zusätzliche Threads durgeführt werden. Ein anderer möglicher Fall ist z.b. ein Mailserver. Würde dieser nur aus einem einzigen Thread bestehen, könnte er gleichzeitig immer nur eine Versende- oder Empfangstransaktion durchführen. Kommt es bei einer Transaktion zu einer Verzögerung, oder zu einem Fehler, müssen alle anderen E-Mails so lange warten, bis die erste E-Mail tatsächlich versendet, empfangen oder der Fehler behoben wurde. Um dieses Problem zu umgehen, kann für jede zu versendende oder zu empfangende E-Mail ein eigener Thread 3

erstellt werden. Verzögert sich dabei ein Transaktionsvorgang (z.b. aufgrund einer langsamen Netzwerkanbindung des Remote-Mailservers), kann während der Wartezeit einfach ein anderer Thread auf der CPU arbeiten und seine Aufgaben erledigen. In Kapitel 7 wird ein konkretes Code-Beispiel eines simulierten Mailservers vorgestellt, um dieses Problem und deren Lösung zu verdeutlichen. Der Einsatz von mehreren Threads innerhalb eines Prozesses bringt aber auch Probleme und Risiken mit sich. In der Praxis besteht sehr häufig die Anforderung, dass mehrere Threads gleichzeitig auf ein und dieselbe Ressource (z.b. ein Objekt) zugreifen möchten. Dazu müssen die verschiedenen Threads synchronisiert werden. Wie oben bereits erwähnt, kann auf einem Prozessorkern stets nur ein Thread ausgeführt werden. Wird ein Thread durch das Prinzip des Timesharings an einer Stelle unterbrochen, an der er z.b. den Wert einer Ressource ändern möchte, so kann es zu unerwarteten Ergebnissen kommen, wenn als nächstes ein anderer Thread die selbe Ressource verwenden möchte. Was sich genau hinter diesem Phänomen der Threadunsicherheit verbrigt und wie man dies verhindern kann, wird in Kapitel 5 näher erläutert. 2.3 Was ist ein Thread? In den vorhergehenden Kapiteln taucht des Öfteren der Begriff Thread auf. Doch was verbirgt sich eigentlich dahinter? Als Thread wird ein Ausführungsstrang oder eine Ausführungsreihenfolge innerhalb eines Prozesses bezeichnet. Jeder Thread bekommt während seiner Initialisierungsphase einen eigenen, unabhängigen Stapel (Stack) im Arbeitsspeicher zugewiesen [2]. Threads besitzen bestimmte Eigenschaften, wie z.b. Zustände. 3 Threadzustände Jeder Thread, der erzeugt wird, befindet sich immer in einem bestimmten Zustand. Die folgende Grafik soll einen kurzen Überblick sowie die Zusammenhänge der verschiedenen Threadzustände geben. 4

Abbildung 1: Die verschiedenen Threadzustände im Überblick In der folgenden Auflistung sind die einzelnen Zustände nochmals etwas detaillierter beschrieben: Ready: Ausführungsbereit. Standby: Nächster Thread der ausgeführt werden soll. Running: Thread, der gerade aktiv ist. Waiting: Thread, der (meist auf eine I/O-Ressource) wartet. Transition: Ausführungsbereit, aber Kernel Stack des Threads ist nicht im Speicher verfügbar ist. Terminated: Thread, der seine Arbeit beendet hat. Initialized: Zustand, während ein Thread erzeugt wird. 4 Die Thread -Klasse in C#.NET In.NET werden alle Threads durch die Klasse Thread im Namensraum System.Threading repräsentiert. Das folgende Codebeispiel zeigt, wie ganz einfach ein neuer Thread erzeugt werden kann: 5

1 using System.Threading; 2 3 class Program 4 { 5 private static void Main(string[] args) 6 { 7 Thread thread1 = new Thread(new ThreadStart(Execute)); 8 thread1.start(); 9 10 Thread thread2 = new Thread(new ParameterizedThreadStart( Execute_Param)); 11 thread1.start(new Object()); 12 } 13 14 public void Execute() 15 { 16 // DO SOMETHING... 17 } 18 19 public void Execute_Param(Object param) 20 { 21 // DO SOMETHING... 22 } 23 } Der Konstruktor von Thread erwartet als Parameter ein ThreadStart-Objekt, bzw. ein Objekt vom Typ ParameterizedThreadStart. Diesem Objekt wird dann als Parameter der Name einer Methode übergeben, welche der Thread ausführen soll. Mit der Start()-Methode wird der Thread schließlich tatsächlich gestartet. In diesem Moment wird der neue Thread erzeugt und er reiht sich in die Schlange der Threads ein, die darauf warten vom Prozesssteuerprogramm (Scheduler) Prozessorzeit zugewiesen zu bekommen. Im Falle eines parametrisierten Thread-Starts wird dem ParameterizedThreadStart-Objekt als Parameter der Name einer Methode übergeben, welche der Thread ausführen soll. Der Tatsächliche Parameter wird jedoch an die Start()-Methode übergeben. Er muss vom Typ Object sein. Neben Start() gibt es noch einige andere Methoden, die auf einem Objekt der Klasse Thread aufgerufen werden können. Einige von ihnen sind in der Praxis relevanter als andere. Ein besonderes Augenmerk möchte ich jedoch auf die Methode Sleep() richten. Als Parameter wird ein Integer übergeben, der die Anzahl an Millisekunden angibt, die der Thread schlafen gelegt werden soll. Ein schlafender Thread wird in den Zustand Waiting versetzt. Ist die Wartezeit verstrichen so reiht er sich wieder in die Warteschlange ein. Im Folgenden befindet sich eine Übersicht über die wichtigsten Methoden, die auf einem Objekt der Klasse Thread aufgerufen werden können: Start(): Startet einen Thread. Start(Object obj): Parametrisierter Start eines Threads. Sleep(int milliseconds): Legt Thread für eine bestimmte Zeit schlafen. 6

Sleep(Timeout.Infinite): Legt Thread auf unbestimmte Zeit schlafen. Interrupt(): Weckt schlafenden Thread auf. Suspend() : Hält einen anderen Thread an. Resume(): Setzt angehaltenen Thread fort. Abort(): Zwingt anderen Thread zum Beenden. Join(): Blockiert den aufrufenden Thread, bis der andere Thread terminiert ist. Objekte der Klasse Thread besitzen auch bestimmte Attribute. Die vier wichtigsten davon sind Priority, isbackground, isalive und ThreadState. Mit diesen lassen sich die Werte der Priorität, des Threadtyps (Hinter- oder Vordergrungthread), des Ausführungsstatus und des aktuellen Threadzustandes auslesen, bzw. teilweise auch verändern. 5 Synchronisation von Threads Jedes moderne Betriebssystem nutzt das Prinzip des Timesharings. Dies bedeutet, dass jeder Thread eine bestimmte Rechenzeit auf der CPU zugeteilt bekommt. Nach Ablauf dieser Zeit wird der Thread unterbrochen, ein anderer Thread kommt zur Ausführung und die unterbrochene Thread reiht sich wieder in die Warteschlange ein, um irgendwann seine Ausführung fortzusetzen. Wird nun ein Thread genau dann unterbrochen, während er eine gemeinsam genutzte Ressource (z.b. ein Objekt) verändern möchte, seinen Vorgang jedoch noch nicht abgeschlossen hat und nun ein anderer Thread ebenfalls auf die Ressource zugreifen möchte, kann es passieren, dass das Objekt in einem ungültigen Zustand hinterlassen wird. Eine Folge von Operationen, die notwendig ist, um einen Bearbeitungsvorgang einer Ressource vollständig abzuschließen, heißt atomare Einheit oder kritischer Bereich. Um sicherzustellen, dass eine solche atomare Einheit immer erst komplett ausgeführt wird, bevor ein anderer Thread wieder auf die gemeinsam genutzte Ressource zugreifen kann, gibt es unterschiedliche Möglichkeiten in.net. Einige davon sind in den folgenden Unterkapiteln anhand von kleinen Code-Beispielen genauer erklärt. 7

5.1 Beispiel eines unsychronisierten Programmes Die Grundproblematik, die durch den Zugriff mehrerer Threads auf ein und dieselbe Ressource entsteht, wird durch das folgende Beispiel erläutert: 1 class Program { 2 private static void Main(string[] args) { 3 ExecClass obj = new ExecClass(); 4 Thread thread1 = new Thread(new ThreadStart(obj.Execute)); 5 Thread thread2 = new Thread(new ThreadStart(obj.Execute)); 6 thread1.start(); 7 thread2.start(); 8 } 9 } Bei Programmstart wird ein neues Objekt der Klasse ExecClass erzeugt. Zusätzlich werden zwei Threads erstellt, jedem der beiden Threads wird die Methode Execute als Parameter übergeben und beide Threads werden gestartet. Die Klasse Program wird auch in allen anderen Unterkapiteln des Kapitels 5 als gegeben vorausgesetzt. 1 class ExecClass { 2 // global ressource 3 private int counter = 0; 4 5 public void Execute() { 6 for (int i = 0; i < 10; i++) { 7 counter++; 8 Console.WriteLine(counter); 9 } 10 } 11 } Die Klasse ExecClass enthält zum einen eine private Variable counter, die als gemeinsam genutzte Ressource verwendet werden soll und zum anderen die Methode Execute, die die counter-variable 10-mal inkrementiert und jeweils dessen Wert ausgibt. Nun testen wir unser Programm und erhalten folgende oder eine ähnliche Ausgabe: 1 2 2 3 3 1 4 5 5 6 6 7 7 8 8 9 9 10 10 4 Wir erkennen eindeutig, dass unser Programm nicht die erwartete Ausgabe der aufsteigend sortierten Zahlen von eins bis zehn zurückliefert, da die counter-variable ja Schritt für Schritt inkrementiert wird und direkt im Anschluss ausgegeben wird. Der in der Ausgabe ersichtlich gewordene Fehler ist dadurch entstanden, dass jeder Thread nur eine bestimmte 8

Zeit auf der CPU rechnen darf. Ist die Zeit abgelaufen, wird er unterbrochen und er muss sich wieder in die Warteschleife einordnen um später seine Ausführung fortzusetzen. Wird in unserem Beispiel ein Thread nun genau in dem Moment unterbrochen, in dem die Variable zwar bereits erhöht, der Wert jedoch noch nicht auf die Konsole geschrieben wurde, kommt es zu solchen Fehlern, wie sie in unserer Ausgabe zu sehen sind. 5.2 Synchronisation mit Monitor oder lock-statement Damit der eben beschriebene Fehler vermieden, bzw. verhindert wird, müssen alle Methoden, die die gemeinsam genutzte Ressource verändern wollen synchronisiert werden. Das bedeutet, solange sich ein Thread in einem Bearbeitungsprozess der Ressource befindet, müssen alle anderen Threads warten, bis der Vorgang angeschlossen ist und die Ressource wieder frei ist. Das folgende Beispiel zeigt den Einsatz der Monitor-Klasse als erste Möglichkeit in.net, mit der eine solche Synchronisation erreicht werden kann: 1 class ExecClass { 2 // global ressource 3 private int counter = 0; 4 5 public void Execute() { 6 for (int i = 0; i < 10; i++) { 7 Monitor.Enter(this); // lock object 8 counter++; 9 Console.WriteLine(counter); 10 Monitor.Exit(this); // unlock object 11 } 12 } 13 } Im Vergleich zum Beispiel der unsychronisierten Umsetzung, wurden lediglich zwei Zeilen hinzugefügt (Zeile 7 und 10). Es wird an diesen beiden Stellen direkt auf ein Monitor- Objekt zugegriffen. Dies ist möglich, da es für jedes beliebige Objektinstanz automatisch genau einen Monitor gibt. Es kann daher auch keine Instanz der Klasse Monitor erstellt werden. Mit den Methoden Enter() und Exit() wird der Zugriff auf das Objekt gesperrt, bzw. wieder freigegeben. Als Parameter wird in diesem Fall jeweils die aktuelle Instanz der Klasse ExecClass übergeben. Nun betrachten wir erneut die Ausgabe: 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9 10 10 9

Wir sehen, dass die Ausgabe nun stimmt und die Zahlen, wie erwartet, aufsteigend sortiert ausgegeben werden. Der Methodenaufruf erfolgt nun also synchron. Eine weitere Option, um Synchronisation zu erreichen, ist das Verwenden des lock-statements, wie es im folgenden Beispiel zum Einsatz kommt: 1 class ExecClass { 2 // global ressource 3 private int counter = 0; 4 5 public void Execute() { 6 for (int i = 0; i < 10; i++) { 7 lock(this) { // lock object 8 counter++; 9 Console.WriteLine(counter); 10 } // unlock object 11 } 12 } 13 } Auch hier gibt es nur eine kleine Änderung am Programm-Code, ebenfalls in den Zeilen 7 und 10. Das lock-statement verwendet intern ebenfalls die im vorherigen Beispiel erwähnte Monitor-Klasse und ist daher keine komplett neue Art der Synchronisation. Allerdings gibt es zwei entscheidende Vorteile. Zum einen wird der Bereich, der synchronisiert werden soll (der kritische Bereich) in einem sogenannten Wirkungsbereich (Scope) geschachtelt. Dies ist sinnvoll, denn wenn der Bereich nicht mit einer geschweiften Klammer wieder geschlossen wird, gibt es bereits zur Zeit des Kompilierens einen Fehler. Bei Verwendung des Monitors, kann es also einfach passieren, dass man den Aufruf der Exit()- Methode vergisst, oder dieser Aufruf aufgrund eines Programmfehlers nicht stattfindet. In beiden Fällen wird das gesperrte Objekt nicht mehr freigegeben. Als weiteren Vorteil ergibt sich die Tatsache, dass das lock-statement intern den kritischen Bereich automatisch in einen try-finally-block einbindet. Das bedeutet, dass falls es zu einem Programmfehler kommt, während ein Objekt gesperrt ist, wird im finally-teil die Sperre in jedem Fall wieder aufgehoben, damit andere Threads wieder auf des Objekt zugreifen können. Insgesamt bietet das Monitor-Konzept, bzw. das lock-statement eine schnelle und einfache Art und Weise, mit der Synchronisation erreicht werden kann. In einigen Fällen eignen sich jedoch andere von.net bereitgestelle Klassen und Methoden besser, wie z.b. die im nächsten Kapitel beschriebene ReaderWriterLockSlim-Methode. 5.3 Synchronisation mit ReaderWriterLockSlim Mit dem.net-framework 2.0 hat Microsoft die Klasse ReaderWriterLock eingeführt. Dies ist die Vorgängerklasse der neuen ReaderWriterLockSlim-Klasse, die mit dem.net- Framework 3.5 eingeführt wurde und von Microsoft für jede Neuentwicklung empfohlen wird, da diese Klasse eine deutlich höhere Leistung bringt und sie zudem einige Mechanismen enthält, die eine Vielzahl potentieller Deadlocks vermeiden kann [3]. Im Folgenden befindet sich ein kurzes Code-Beispiel: 10

1 class ExecClass { 2 // global ressource 3 private ReaderWriterLockSlim lockobj = new ReaderWriterLockSlim(); 4 private int counter = 0; 5 6 public void Execute() { 7 for (int i = 0; i < 10; i++) { 8 try { 9 lockobj.enterwritelock(); // lock object 10 counter++; 11 Console.WriteLine(counter); 12 } 13 finally { 14 lockobject.exitwritelock(); // unlock object 15 } 16 } 17 } 18 } In Zeile 3 wird ein neue Objekt mit dem Namen lockobj eingeführt. Es ist vom Typ ReaderWriterLockSlim und dient lediglich als Objekt, das gesperrt wird, sobald der kritische Bereich betreten wird. Der Sperr- bzw. Entsperrvorgang erfolgt mit den Methoden EnterWriteLock() und ExitWriteLock(). Interessant ist diese Form der Synchronisation, weil sie besonders für Szenarien geeignet ist, in denen es viele lesende Zugriffe und wenig schreibende Zugriffe auf die gemeinsam genutzte Ressource gibt. Denn es kann mit den Methoden EnterReadLock() und ExitReadLock() ein Schreibzugriff angefordert werden. Solange kein Schreibzugriff angefordert ist, kann von beliebig vielen Threads gleichzeitig gelesen werden. Sobald ein Schreibzugriff stattfindet, müssen die anderen lesenden oder auch anderen schreibenden Threads warten. Es können also gleichzeitig immer nur beliebig viele lesen oder genau einer schreiben. Besonders beim Zugriff auf Datenquellen wie Dateien oder Datenbanken ist ein solchen Konzept sehr sinnvoll und effizient. 5.4 Synchronisation mit MethodImpl-Attribut Wir betrachten zunächst ein kleines Beispiel: 1 using System.Runtime.CompilerServices; 2 3 class ExecClass { 4 // global ressource 5 private int counter = 0; 6 7 [MethodImpl(MethodImplOptions.Synchronized)] 8 public void Execute() { 9 for (int i = 0; i < 10; i++) { 10 counter++; 11 Console.WriteLine(counter); 12 } 13 } 14 } 11

Die benötigten Attribute befinden sich im Namespace System.Runtime.CompilerServices. Sobald eine Methode mit dem in Zeile 7 beschriebenen Attribut versehen wird, wird immer die gesamte Methode synchronisiert aufgerufen. Dies entspricht in etwa dem synchronized- Attribut in der Signatur einer Methode in Java. 6 Der Einsatz von Threadpools und BackgroundWorker-Threads Zwei andere sehr komfortable Konzepte in.net sind ThreadPools sowie BackgroundWorker- Threads. Ein Threadpool ist eine Threadauflistung, mit der verschiedene Aufgaben im Hintergrund ausgeführt werden können. Auf diese Weise kann der primäre Thread asynchron andere Aufgaben ausführen [4]. Die BackgroundWorker-Klasse ermöglicht das Ausführen eines Vorgangs auf einem separaten, dedizierten Thread [5]. 6.1 Threadpools Ein ThreadPool ist eine Menge von Threads, die bei jedem Prozess in.net automatisch zur Verfügung stehen. Das Erzeugen eines neuen Threads ist kostspielig, weil jeder Thread seinen eigenen Stapelspeicher (Stack) benötigt. Die Idee hinter dem ThreadPool ist, dass diese Threads permanent im Hintergrund existieren und darauf warten, als Delegate benutzt zu werden und asynchron und unabhängig vom Vordergrundthread Aufgaben zu erledigen. Einen solchen Thread kann man mit der Methode QueueUserWorkItem() anfordern. Diese Methode erwartet als Parameter eine Methode, die der ThreadPool-Thread aufrufen soll. Sobald ein neuer Arbeitsauftrag eingereicht wurde, kann er nicht mehr abgebrochen werden. Ist zur Zeit der Anforderung kein Thread im Pool frei, so wird mit der Ausführung so lange gewartet, bis ein Thread wieder frei ist. Die folgende Grafik veranschaulicht die Funktionsweise eines ThreadPools: Abbildung 2: Funktionsweise eines ThreadPools 12

6.2 BackgroundWorker-Threads Ein BackgroundWorker-Thread kommt in der Praxis sehr oft in Kombination mit einer grafischen Oberfläche (GUI) zum Einsatz. Ein typisches Szenario ist das Erledigen einer Aufgabe im Hintergrund, wie z.b. ein Kopier- oder Installationsvorgang. Damit die grafische Oberfläche weiterhin reagieren kann, wird die zu erledigende Arbeit einfach in einem BackgroundWorker-Thread ausgeführt. Im Vordergrund wird dann beispielsweise der aktuelle Fortschritt des Arbeitsvorganges in Form einer StatusBar ausgegeben. Mit dieser speziellen Klasse stehen dem Benutzer zusätzliche Optionen zur Verfügung. So kann der aktuelle Status und der Fortschritt des Arbeitsvorganges abgerufen und überprüft werden. Dem Programmierer stehen dazu unter anderem die Ereignisse DoWork, RunWoker- Completed und ProgressChanged zur Verfügung. Ein BackgroundWorker-Thread ist immer dann besonders sinnvoll, wenn nur wenige oder nur ein einzelner Hintergrund-Thread benötigt wird. Ein ThreadPool ist dagegen sinnvoller, wenn es viele kleine Aufgaben sind, die im Hintergrund erledigt werden sollen. 7 Programmierbeispiel: Simulierter Mailserver Zum Abschluss dieser Arbeit möchte ich ein konkretes Beispiel vorstellen, an dem ersichtlich wird, in welchen Fällen der Einsatz von Multithreading nicht nur sinnvoll, sondern auch absolut erforderlich ist. In Kapitel 2.2 habe ich die Problematik bei einem Mailserver erwähnt. Genau dieses Beispiel möchte ich nun in Form einer Simulation demonstrieren. 7.1 Die SingleThread-Variante Folgender Programm-Code zeigt eine abstrahierte Version eines Mailservers, der auf Tastatureingaben (simulierte eingehende E-Mails) wartet und diese dann verschickt. Der Sendevorgang wird durch ein Thread.Sleep() simuliert. Dabei soll die Dauer des Vorgangs zwischen 100 Millisekunden und 2 Sekunden liegen (hier durch einen Zufallsgenerator implementiert). In dieser ersten Variante wird der Wartevorgang auf eingehende E-Mails sowie der Sendevorgang über einen einzigen Thread realisiert. Es handelt sich also um die SingleThread-Variante: 13

1 class Program { 2 public static void Main(string[] args) { 3 Console.WriteLine("Listening for incoming messages..."); 4 while (true) { // loop for listening for incoming messages 5 string inputstring = Console.ReadLine(); 6 SendEmail(inputString); 7 } 8 } 9 10 // simulates sending an email 11 public static void SendEmail(object messageargs) { 12 int randomint = new Random().Next(100, 2000); 13 Thread.Sleep(randomInt); // simulated time for sending email 14 Console.WriteLine("Message ({0}) successful send in {1} ms.", messageargs, randomint); 15 } 16 } Betrachten wir nun was passiert, wenn wir der Reihe nach auf der Tastatur die Zahlen eins bis null eingeben: Abbildung 3: Simulierter Mailserver mit einem einzigen Thread Wir können erkennen, dass eine neue Nachricht immer erst wieder eingegeben werden kann, wenn die vorherige E-Mail versendet worden ist. Das hat den Nachteil, dass alle nahezu gleichzeitig eintreffenden E-Mails lediglich nacheinander eingelesen und versendet werden können. Dauert nun ein einzelner Sendevorgang nun unverhältnismäßig lange, so müssen alle anderen E-Mails warten, bis dieser Vorgang abgeschlossen ist. Dieses Verhalten ist für große E-Mail Dienstleister natürlich absolut unpraktikabel, deren Server vermutlich über 100 E-Mails pro Minute verarbeiten müssen. 14

7.2 Die MultiThread-Variante Wir verändern nun unser obiges Code-Beispiel und passen es so an, dass für jeden Sendevorgang ein eigener Thread erstellt wird. Dazu ersetzen wir die Zeile 6 mit folgenden zwei Zeilen: 1 Thread mailsender = new Thread(new ParameterizedThreadStart(SendEmail)); 2 mailsender.start(inputstring); Durch diese minimale Anpassung des Codes verändert sich die Funktionsweise drastisch. Wir betrachten wieder die mögliche Ausgabe, die entsteht, wenn wir wieder die Zahlen eins bis zehn aufsteigend eingeben: Abbildung 4: Simulierter Mailserver mit mehreren Threads Als erstes fällt auf, dass erst sechs E-Mails eingehen, bevor eine davon raus geschickt wurde. Weiterhin sehen wir, dass Nachricht Nr. 4 als erstes verschickt wurde, bzw. dass die Sendereihenfolge grunsätzlich nicht der Reihenfolge entspricht, in der die E-Mails beim Mailserver eingehen. Genau dieses Verhalten haben wir durch den Einsatz mehrerer Threads bewirkt, denn jede E-Mail, die eingeht kann in einem neuen Thread nahezu sofort versendet werden. Um dem Ganzen noch etwas mehr Praxis zu verleihen, wird im folgenden Unterkapitel ein konkreter Test mit Zeitmessung vorgenommen, der den Unterschied der beiden Varianten nochmals in Zahlen ausdrückt. 15

7.3 Konkreter Vergleich der beiden Varianten In diesem Test wird die Eingabe der zehn Nachrichten über ein externes Programm gelöst, damit das Eingehen der E-Mails in jedem Einzeltest nahezu gleich schnell erfolgt. Gemessen wurde nun die Zeit, die mit dem Eingehen der ersten E-Mail beginnt und mit dem Versenden der letzten E-Mail endet. Aufgrund der zufällig generierten Wartezeit, die das Versenden der E-Mails simuliert, wurden beide Varianten des Mailservers 5-mal getestet und ein Durchschnittswert gebildet. Die Messergebnisse sind in der folgenden Tabelle übersichtlich dargestellt: Versuch Single-Thread Multi-Thread 1 13,94 s 2,44 s 2 11,39 s 2,35 s 3 12,97 s 2,54 s 4 11,23 s 2,75 s 5 6,28 s 2,27 s 11,16 s 2,47 s Die Ergebnisse fallen, wie erwartet aus. Die MultiThread-Variante kann alle E-Mails deutlich schneller versenden, als die SingleThread-Variante, da bei dieser ja die E-Mails nur nacheinander eingehen und nacheinander versendet werden können. Der Unterschied ist jedoch beträchtlich. Im Schnitt braucht die SingleThread-Variante ca. 4,5x länger als die MultiThread-Variante. Dieses Beispiel verdeutlicht sehr eindeutig, dass das einsetzen mehrerer Threads in einigen Anwendungen erhebliche Leistungssteigerungen bringt. In unserem Beispiel wurde eine Verkürzung auf ca. 22% der ursprünglichen Laufzeit gemessen. 8 Abschließende Zusammenfassung Das Thema Multithreading hat in Zeiten des 21. Jahrhunderts eine hohe Relevanz. Einzelne Prozesse können unmittelbar vom Einsatz Multicore-fähiger Systeme profitieren und einen solchen Prozessor mit mehreren CPUs besser auslasten. Gerade bei Anwendung, die aufwändige Berechnungen druchführen oder viele Dinge gleichzeitig erledigen müssen, ist durch Multithreading ein Leistungsvorteil erreichbar. Der aktuelle Trend der Prozessorentwicklung geht noch viel stärker in Richtung Multicore-Systeme. Führende Prozessorhersteller haben bereits Prototypen mit über 100 Rechenkernen entwickelt. Es ist also zu erwarten, dass in den kommenden Jahren das Thema Multithreading von noch größerer Bedeutung sein wird. Auch bei allen positiven Aspekten und Leistungsoptimierungen, die erreicht werden können, sind die damit verbundenen Probleme und Risiken zu beachten. Anwendungen, die mehrere Threads verwenden, sind extrem fehleranfällig beim gleichzeitigen Zugriff auf eine Ressource. Auch bringen mehrerer Threads in einer Anwendung, die ohnehin keine Aufgaben parallel erledigen kann, keine Vorteile. Ob Multithreading genutzt werden soll oder nicht, muss letztendlich der Programmierer entscheiden. 16

9 Anhang 9.1 Quellcode und Binärdateien des Mailserver-Beispiels Die Quelldateien befinden sich in Form eines Microsoft Visual Studio 2010 Projekts in der Datei Mailserver src.zip. Die Birärdateien befinden sich in der Datei Mailserver bin.zip. Es ist sowohl die SingleThreadals auch die MultiThread-Variante darin enthalten. 9.2 Literaturverzeichnis Folgende Quellen wurden für das Erstellen der Seminararbeit verwendet: [1] Vorl. 6: Single- und Multitasking, [22.06.2006 - zuletzt geändert am: 22.06.2006], http://www.rvs.uni-bielefeld.de/lectures/techinf/ti2/download/vorl6.pdf [2] Thread (Informatik), [15.12.2010 - zuletzt geändert am: 15.12.2010], http://de.wikipedia.org/wiki/thread (Informatik) [3] ReaderWriterLock-Klasse, [16.12.2010 - zuletzt geändert am: 16.12.2010], http://msdn.microsoft.com/dede/library/system.threading.readerwriterlock.aspx [4] Gewusst wie: Verwenden von Threadpools, [18.12.2010 - zuletzt geändert am: 18.12.2010], http://msdn.microsoft.com/de-de/library/3dasc8as(v=vs.80).aspx [5] Gewusst wie: Verwenden von Threadpools, [18.12.2010 - zuletzt geändert am: 18.12.2010], http://msdn.microsoft.com/dede/library/system.componentmodel.backgroundworker.aspx 9.3 Abbildungsverzeichnis Folgende Abbildungen wurden in der Seminararbeit verwendet: [Abb. 1] [Abb. 2] Laurent Haan: Multithreading in C#, [26.11.2010 - zuletzt geändert am: 12.12.2009], http://www.codeplanet.eu/tutorials/csharp/64-multithreading-incsharp.html#prozesse Threads und Prioritaetsklassen Threads Thread Status Laurent Haan: Multithreading in C#, [26.11.2010 - zuletzt geändert am: 12.12.2009] http://www.codeplanet.eu/tutorials/csharp/64- multithreading-in-csharp.html#multithreading in DotNET ThreadPool und asynchrone Methoden ThreadPool 17