Wann soll Parallel.ForEach verwendet werden und wann soll PLINQ verwendet werden?

Ursprünglicher Autor: Pamela Vagata
  • Übersetzung

Einleitung


In der Regel besteht der erste Schritt bei der Optimierung eines Programms für Mehrkerncomputer darin, die Möglichkeit zu ermitteln, den Algorithmus in parallel laufende Teile zu unterteilen. Wenn zur Behebung des Problems separate Elemente aus einem großen Datensatz parallel verarbeitet werden müssen, sind die ersten Kandidaten neue parallele Features in .NET Framework 4: Parallel.ForEach und Parallel LINQ ( PLINQ ).

Parallel.ForEach


Die Parallel-Klasse enthält die ForEach- Methode , eine Multithread-Version einer regulären foreach-Schleife in C #. Parallel.ForEach durchläuft wie reguläres foreach mehrere Daten, verwendet jedoch mehrere Threads. Eine der am häufigsten verwendeten Parallel.ForEach- Überladungen lautet wie folgt:

public static ParallelLoopResult ForEach(
			 IEnumerable source,
			 Action body)

Ienumerable gibt die Sequenz an, über die iteriert werden soll, und der Aktionshauptteil legt fest, dass der Delegat für jedes Element aufgerufen wird. Eine vollständige Liste der Parallel.ForEach-Überladungen finden Sie hier .

Plinq


In Verbindung mit Parallel.ForEach PLINQ ist ein Programmiermodell für parallele Datenoperationen . Der Benutzer definiert eine Operation aus einem Standardsatz von Operatoren, einschließlich Projektionen, Filtern, Aggregation usw. Wie Parallel.ForEach erreicht PLINQ Parallelität, indem die Eingabesequenz in Teile und Verarbeitungselemente in verschiedenen Threads aufgeteilt wird.
Der Artikel hebt die Unterschiede zwischen diesen beiden Ansätzen zur Parallelität hervor. Grundlegendes zu Verwendungsszenarien, in denen Parallel.ForEach anstelle von PLINQ verwendet werden sollte, und umgekehrt.

Unabhängige Operationen



Wenn Sie lange Berechnungen für die Elemente einer Sequenz durchführen müssen und die Ergebnisse unabhängig sind, sollten Sie Parallel.ForEach verwenden. PLinq wiederum ist für solche Operationen zu schwer. Darüber hinaus wird für Parallel.ForEach die maximale Anzahl von Threads angegeben, dh , wenn ThreadPool nur über wenige Ressourcen verfügt und weniger Threads als in ParallelOptions.MaxDegreeOfParallelism angegeben verfügbar sind , wird die optimale Anzahl von Threads verwendet, die bei der Ausführung zunehmen kann. Für PLINQ ist die Anzahl der auszuführenden Threads genau festgelegt.

Parallelbetrieb mit Erhalt der Datenreihenfolge


PLINQ um Ordnung zu halten


Wenn für Ihre Konvertierungen die Eingabereihenfolge beibehalten werden muss , ist die Verwendung von PLINQ wahrscheinlich einfacher als die von Parallel.ForEach . Wenn wir beispielsweise RGB-Farbvideoframes in Schwarzweiß konvertieren möchten, sollte die Frame-Reihenfolge am Ausgang natürlich beibehalten werden. In diesem Fall ist es besser, PLINQ und die AsOrdered () - Funktion zu verwenden , die in den Tiefen der PLINQ-Partitionen die Eingabesequenz ausführt, Konvertierungen durchführt und dann das Ergebnis in der richtigen Reihenfolge anordnet.


public static void GrayscaleTransformation(IEnumerable Movie)
{
	var ProcessedMovie =
	Movie
	.AsParallel()
	.AsOrdered()
	.Select(frame => ConvertToGrayscale(frame));
	foreach (var grayscaleFrame in ProcessedMovie)
	{
	  // Movie frames will be evaluated lazily
	}
}


Warum nicht Parallel.ForEach hier verwenden?

Außer in trivialen Fällen erfordert die Implementierung von Paralleloperationen für serielle Daten mithilfe von Parallel.ForEach eine erhebliche Menge an Code. In unserem Fall können wir die Überladung der Foreach- Funktion verwenden , um den Effekt des AsOrdered () -Operators zu wiederholen:


public static ParallelLoopResult ForEach(
				IEnumerable source,
				Actionbody)


In der überladenen Version von Foreach wurde der Indexparameter des aktuellen Elements dem Datenaktionsdelegaten hinzugefügt. Jetzt können wir das Ergebnis an derselben Stelle in die Ausgabesammlung schreiben, kostspielige Berechnungen parallel durchführen und schließlich die Ausgabereihenfolge in der richtigen Reihenfolge erhalten. Das folgende Beispiel zeigt eine Möglichkeit, die Reihenfolge mit Parallel.ForEach aufrechtzuerhalten :


public static double [] PairwiseMultiply( double[] v1, double[] v2)
{
  var length = Math.Min(v1.Length, v2.Lenth);
  double[] result = new double[length];
  Parallel.ForEach(v1,
    (element, loopstate, elementIndex) =>
    result[elementIndex] = element * v2[elementIndex]);
  return result;
}


Mängel dieses Ansatzes werden jedoch sofort entdeckt. Wenn es sich bei der Eingabesequenz um einen IEnumerable-Typ und nicht um ein Array handelt, gibt es vier Möglichkeiten, die Aufbewahrung der Reihenfolge zu implementieren:
  • Die erste Möglichkeit besteht darin, IEnumerable.Count () aufzurufen, was O (n) kostet. Wenn die Anzahl der Elemente bekannt ist, können Sie ein Ausgabearray erstellen, um die Ergebnisse an einem bestimmten Index zu speichern
  • Die zweite Möglichkeit besteht darin, die Sammlung zu materialisieren (indem Sie sie beispielsweise in ein Array umwandeln). Wenn es viele Daten gibt, ist diese Methode nicht sehr geeignet.
  • Die dritte Möglichkeit besteht darin, die Ausgabeauflistung sorgfältig zu überlegen. Die Ausgabeerfassung kann ein Hash sein. Die zum Speichern des Ausgabewerts erforderliche Speichermenge beträgt mindestens das Zweifache des Eingabespeichers, um Hash-Kollisionen zu vermeiden. Wenn es viele Daten gibt, ist die Datenstruktur für den Hash unerschwinglich groß. Außerdem kann es zu Leistungseinbußen aufgrund falscher Freigabe und des Garbage Collectors kommen.
  • Die letzte Option besteht darin, die Ergebnisse mit ihren ursprünglichen Indizes zu speichern und dann Ihren eigenen Algorithmus zum Sortieren der Ausgabesammlung anzuwenden.


In PLINQ fordert der Benutzer lediglich die Beibehaltung der Reihenfolge an, und die Abfrage-Engine verwaltet alle Routinedetails, um die richtige Reihenfolge der Ergebnisse sicherzustellen. Das PLINQ- Framework ermöglicht es dem AsOrdered () -Operator, Streaming-Daten zu verarbeiten. Mit anderen Worten, PLINQ unterstützt die verzögerte Materialisierung. In PLINQ ist das Materialisieren der gesamten Sequenz die schlechteste Lösung. Sie können die oben genannten Probleme auf einfache Weise vermeiden und mit dem Operator AsOrdered () parallele Operationen für die Daten ausführen .

Paralleles Streaming


Verwenden von PLINQ zum Verarbeiten eines Streams


PLINQ bietet die Möglichkeit, eine Anfrage als Anfrage über einen Stream zu verarbeiten. Diese Funktion ist aus folgenden Gründen äußerst nützlich:
  • 1. Die Ergebnisse werden nicht im Array gespeichert, sodass beim Speichern von Daten im Speicher keine Redundanz auftritt.
  • 2. Sie können die Ergebnisse in einem einzigen Datenstrom auflisten, sobald Sie neue Daten erhalten.

Fahren Sie mit dem Beispiel der Analyse von Wertpapieren fort und stellen Sie sich vor, Sie möchten das Risiko jedes Papiers aus einem Wertpapierportfolio berechnen, indem Sie nur Wertpapiere ausgeben, die die Kriterien für die Risikoanalyse erfüllen, und dann einige Berechnungen für die gefilterten Ergebnisse durchführen. In PLINQ sieht der Code ungefähr so aus:


public static void AnalyzeStocks(IEnumerable Stocks)
{
  var StockRiskPortfolio =
  Stocks
  .AsParallel()
  .AsOrdered()
  .Select(stock => new { Stock = stock, Risk = ComputeRisk(stock)})
  .Where(stockRisk => ExpensiveRiskAnalysis(stockRisk.Risk));
  foreach (var stockRisk in StockRiskPortfolio)
  {
    SomeStockComputation(stockRisk.Risk);
    // StockRiskPortfolio will be a stream of results
  }
}


In diesem Beispiel werden die Elemente in Teile ( Partitionen ) aufgeteilt, von mehreren Threads verarbeitet und anschließend neu angeordnet. Es ist wichtig zu verstehen, dass diese Schritte parallel ausgeführt werden, wenn Filterergebnisse angezeigt werden und ein Single-Thread-Consumer in der foreach-Schleife Berechnungen durchführen kann. PLINQ ist auf Leistung und nicht auf Latenz optimiert und verwendet Puffer intern. Es kann vorkommen, dass ein Teilergebnis, das bereits erhalten wurde, im Ausgabepuffer verbleibt, bis der Ausgabepuffer vollständig gesättigt ist und keine weitere Verarbeitung mehr zulässt. Die Situation kann mit der Erweiterungsmethode PLINQ WithMergeOptions korrigiert werden, mit der Sie die Ausgabepufferung festlegen können. MethodeWithMergeOptions akzeptiert die ParallelMergeOptions- Enumeration als Parameter . Sie können angeben, wie die Abfrage das Endergebnis zurückgibt, das von einem einzelnen Stream verwendet wird. Folgende Optionen werden angeboten:

  • ParallelMergeOptions.NotBuffered - Gibt an, dass jedes verarbeitete Element von jedem Thread zurückgegeben wird, sobald es verarbeitet wird.
  • ParallelMergeOptions.AutoBuffered - Gibt an, dass die Elemente im Puffer gesammelt werden. Der Puffer wird regelmäßig an den Consumer-Stream zurückgegeben
  • ParallelMergeOptions.FullyBuffered - Gibt an, dass die Ausgabesequenz vollständig gepuffert ist. Auf diese Weise können Sie schneller Ergebnisse erzielen als mit anderen Optionen. In diesem Fall muss der Consumer-Thread jedoch lange warten, bis das erste Element für die Verarbeitung empfangen wird.


UsingMergeOptions-Beispiel auf MSDN verfügbar

Warum nicht Parallel.ForEach?


Schieben Sie die Mängel von Parallel.ForEach beiseite, um die Reihenfolge der Sequenzen beizubehalten. Bei ungeordneten Berechnungen über einen Stream mit Parallel.ForEach sieht der Code folgendermaßen aus:


public static void AnalyzeStocks(IEnumerable Stocks)
{
  Parallel.ForEach(Stocks,
    stock => {
    var risk = ComputeRisk(stock);
    if(ExpensiveRiskAnalysis(risk)
    {
      // stream processing
      lock(myLock) { SomeStockComputation(risk) };
      // store results
    }
}


Dieser Code ist mit Ausnahme des expliziten Blockierens und des weniger eleganten Codes fast identisch mit dem PLINQ-Beispiel. Beachten Sie, dass Parallel.ForeEach in dieser Situation das Speichern der Ergebnisse in einem thread-sicheren Stil bedeutet, während PLINQ dies für Sie erledigt.
Um die Ergebnisse zu speichern, haben wir drei Möglichkeiten: Die erste besteht darin, die Werte in einer stream-unsicheren Auflistung zu speichern und für jeden Datensatz eine Sperre zu verlangen. Die zweite Möglichkeit ist das Speichern in einer thread-sicheren Auflistung. Glücklicherweise stellt .NET Framework 4 eine Reihe solcher Auflistungen im System.Collections.Concurrent- Namespace zur Verfügung, und Sie müssen sie nicht selbst implementieren. Die dritte Möglichkeit besteht darin, Parallel.ForEach mit thread-local zu verwendenSpeicher, der später beschrieben wird. Jede dieser Methoden erfordert eine explizite Kontrolle über die Auswirkungen des Schreibens in die Sammlung durch Dritte, während PLINQ es uns ermöglicht, von diesen Vorgängen zu abstrahieren.


Operationen auf zwei Sammlungen


Verwendung von PLINQ für Operationen an zwei Sammlungen


Der PLINQ ZIP- Operator führt speziell parallele Berechnungen für zwei verschiedene Sammlungen durch. Da es mit anderen Abfragen kombiniert werden kann, können Sie gleichzeitig komplexe Vorgänge für jede Sammlung ausführen, bevor Sie die beiden Sammlungen kombinieren. Zum Beispiel:

public static IEnumerable Zipping(IEnumerable a, IEnumerable b)
{
	return
	a
	.AsParallel()
	.AsOrdered()
	.Select(element => ExpensiveComputation(element))
	.Zip(
	  b
	  .AsParallel()
	  .AsOrdered()
	  .Select(element => DifferentExpensiveComputation(element)),
	  (a_element, b_element) => Combine(a_element,b_element));
}

Das obige Beispiel zeigt, wie jede Datenquelle von verschiedenen Vorgängen parallel verarbeitet wird. Die Ergebnisse beider Quellen werden dann vom Zip-Operator kombiniert.

Warum nicht Parallel.ForEach?


Eine ähnliche Operation kann mit Parallel.ForEach-Überladung mithilfe von Indizes ausgeführt werden. Beispiel:


public static IEnumerable Zipping(IEnumerable a, IEnumerable b)
{
	var numElements = Math.Min(a.Count(), b.Count());
	var result = new T[numElements];
	Parallel.ForEach(a,
	(element, loopstate, index) =>
	{
	  var a_element = ExpensiveComputation(element);
	  var b_element = DifferentExpensiveComputation(b.ElementAt(index));
	  result[index] = Combine(a_element, b_element);
	});
	return result;
}


Es gibt jedoch potenzielle Fallen und Mängel, die bei der Anwendung von Parallel beschrieben werden. Bei jeder Beibehaltung der Datenreihenfolge besteht einer der Nachteile in der vollständigen Anzeige der gesamten Sammlung und der expliziten Indexverwaltung.

Der lokale Strömungszustand ( Gewinde-Local State )


Verwenden von Parallel.ForEach, um auf den lokalen Status eines Streams zuzugreifen


Obwohl PLINQ präzisere Mittel für parallele Vorgänge mit Daten bietet, eignen sich einige Verarbeitungsszenarien besser für die Verwendung von Parallel.ForEach , z. B. Vorgänge, die den lokalen Status eines Streams unterstützen. Die Signatur der entsprechenden Parallel.ForEach- Methode sieht folgendermaßen aus:


public static ParallelLoopResult ForEach(
    IEnumerable source,
    Func localInit,
    Func body,
    Action localFinally)


Es ist zu beachten, dass eine Überlastung des Operators Aggregate vorliegt , die den Zugriff auf den lokalen Status des Streams ermöglicht und verwendet werden kann, wenn die Datenverarbeitungsvorlage als Dimensionsverringerung beschrieben werden kann. Das folgende Beispiel zeigt, wie Sie Zahlen, die keine Primzahlen sind, aus einer Sequenz ausschließen:

public static List Filtering(IEnumerable source)
{
	var results = new List();
	using (SemaphoreSlim sem = new SemaphoreSlim(1))
	{
		Parallel.ForEach(source, () => new List(),
		(element, loopstate, localStorage) =>
		{
			bool filter = filterFunction(element);
			if (filter)
			localStorage.Add(element);
			return localStorage;
		},
		(finalStorage) =>
		{
			lock(myLock)
			{
				results.AddRange(finalStorage)
			};
		});
	}
	return results;
}

Eine solche Funktionalität könnte mit PLINQ viel einfacher erreicht werden. Das Beispiel soll zeigen, dass die Verwendung von Parallel.ForEach und des lokalen Status des Streams die Synchronisationskosten erheblich senken kann. In anderen Szenarien sind jedoch lokale Flusszustände unbedingt erforderlich, das folgende Beispiel zeigt ein solches Szenario.

Stellen Sie sich vor, Sie haben als brillanter Informatiker und Mathematiker ein statistisches Modell zur Analyse von Wertpapierrisiken entwickelt. Dieses Modell wird Ihrer Meinung nach alle anderen Risikomodelle auf den Kopf stellen. Um dies zu beweisen, benötigen Sie Daten von Websites mit Informationen zu den Aktienmärkten. Das Laden der Datensequenz ist jedoch sehr lang und ein Engpass für einen Computer mit acht Kernen. Obwohl zu verwendenParallel.ForEach ist eine einfache Möglichkeit, Daten mithilfe von WebClient parallel zu laden . Jeder Stream wird bei jedem Download blockiert, wodurch die Verwendung asynchroner E / A verbessert werden kann. Weitere Informationen finden Sie hier . Aus Leistungsgründen haben Sie sich für Parallel.ForEach entschieden , um die URL-Sammlung zu durchlaufen und Daten parallel hochzuladen. Der Code sieht ungefähr so ​​aus:


public static void UnsafeDownloadUrls ()
{
	WebClient webclient = new WebClient();
	Parallel.ForEach(urls,
	(url,loopstate,index) =>
	{
		webclient.DownloadFile(url, filenames[index] + ".dat");
		Console.WriteLine("{0}:{1}", Thread.CurrentThread.ManagedThreadId, url);
	});
}

Überraschenderweise tritt zur Laufzeit eine Ausnahme auf: "System.NotSupportedException -> WebClient unterstützt keine gleichzeitigen E / A-Vorgänge." Nachdem Sie festgestellt haben, dass viele Threads nicht gleichzeitig auf denselben WebClient zugreifen können, möchten Sie einen WebClient erstellen für jeden Download.


public static void BAD_DownloadUrls ()
{
	Parallel.ForEach(urls,
	(url,loopstate,index) =>
	{
		WebClient webclient = new WebClient();
		webclient.DownloadFile(url, filenames[index] + ".dat");
		Console.WriteLine("{0}:{1}", Thread.CurrentThread.ManagedThreadId, url);
	});
}

Mit diesem Code kann das Programm mehr als einhundert Webclients erstellen, und das Programm löst eine Timeout-Ausnahme in WebClient aus. Sie werden verstehen, dass auf dem Computer kein Server-Betriebssystem ausgeführt wird, sodass die maximale Anzahl von Verbindungen begrenzt ist. Dann können Sie davon ausgehen, dass die Verwendung von Parallel.ForEach mit dem lokalen Status des Streams das Problem löst:

public static void downloadUrlsSafe()
{
	Parallel.ForEach(urls,
	() => new WebClient(),
	(url, loopstate, index, webclient) =>
	{
		webclient.DownloadFile(url, filenames[index]+".dat");
		Console.WriteLine("{0}:{1}", Thread.CurrentThread.ManagedThreadId, url);
		return webclient;
	},
	(webclient) => { });
}
}


In dieser Implementierung ist jede Datenzugriffsoperation von einer anderen unabhängig. Gleichzeitig ist der Access Point weder unabhängig noch threadsicher. Durch die Verwendung des lokalen Stream-Speichers können wir sicherstellen, dass so viele WebClient- Instanzen wie erforderlich erstellt wurden und jede WebClient- Instanz zu dem Stream gehört, der sie erstellt hat.

Warum ist PLINQ hier schlecht?


Wenn Sie das vorherige Beispiel mit ThreadLocal- und PLINQ-Objekten implementieren, lautet der Code wie folgt:

public static void downloadUrl()
{
  var webclient = new ThreadLocal(()=> new WebClient ());
  var res =
  urls
  .AsParallel()
  .ForAll(
  url =>
  {
    webclient.Value.DownloadFile(url, host[url] +".dat"));
    Console.WriteLine("{0}:{1}",
    Thread.CurrentThread.ManagedThreadId, url);
  });
}


Während die Implementierung dieselben Ziele erreicht, ist es wichtig zu verstehen, dass die Verwendung von ThreadLocal <> in jedem Szenario erheblich teurer ist als die entsprechende Parallel.ForEach- Überladung . Beachten Sie, dass in diesem Szenario die Kosten für das Erstellen von ThreadLocal <> -Instanzen im Vergleich zu der Zeit, die zum Herunterladen einer Datei aus dem Internet benötigt wurde, vernachlässigbar sind.

Beenden Sie den Vorgang


Verwenden von Parallel.ForEach zum Beenden von Vorgängen


In einer Situation, in der die Kontrolle über die Ausführung von Operationen von entscheidender Bedeutung ist, ist es wichtig zu verstehen, dass Sie mit dem Beenden des Zyklus Parallel.ForEach den gleichen Effekt erzielen können, als wenn Sie die Bedingungen überprüfen, unter denen die Berechnung im gesamten Zyklus fortgesetzt werden muss. Eine der Parallel.ForEach- Überladungen , mit denen Sie ParallelLoopState verfolgen können, sieht folgendermaßen aus:



public static ParallelLoopResult ForEach(
    IEnumerable source,
    Action body)

ParallelLoopState bietet Unterstützung für die Unterbrechung der Schleifenausführung auf zwei verschiedene Arten, die im Folgenden beschrieben werden.

ParallelLoopState.Stop ()


Stop () informiert die Schleife über die Notwendigkeit, Iterationen zu stoppen. Mit der ParallelLoopState.IsStopped- Eigenschaft kann jede Iteration bestimmen, ob eine andere Iteration die Stop () - Methode aufgerufen hat . Die Stop () - Methode ist normalerweise nützlich, wenn die Schleife eine ungeordnete Suche durchführt und beendet werden soll, sobald das Element gefunden wurde. Wenn wir beispielsweise herausfinden möchten, ob ein Objekt in der Auflistung vorhanden ist, sieht der Code folgendermaßen aus:

public static boolean FindAny(IEnumerable TSpace, T match) where T: IEqualityComparer
{
  var matchFound = false;
  Parallel.ForEach(TSpace,
  (curValue, loopstate) =>
  {
    if (curValue.Equals(match) )
    {
      matchFound = true;
      loopstate.Stop();
    }
  });
  return matchFound;
}

Die Funktionalität kann auch mit PLINQ erreicht werden. In diesem Beispiel wird veranschaulicht, wie ParallelLoopState.Stop () zum Steuern des Ausführungsflusses verwendet wird.

ParallelLoopState.Break ()


Break () informiert die Schleife darüber, dass die dem aktuellen Element vorangehenden Elemente verarbeitet werden sollen, für nachfolgende Elemente der Iteration ist jedoch ein Stopp erforderlich. Der niedrigere Iterationswert kann über die ParallelLoopState.LowestBreakIteration- Eigenschaft abgerufen werden . Break () ist normalerweise nützlich, wenn Sie nach bestellten Daten suchen. Mit anderen Worten, es gibt ein bestimmtes Kriterium für die Notwendigkeit der Datenverarbeitung. Für eine Sequenz mit nicht eindeutigen Elementen, in der der untere Index eines übereinstimmenden Objekts ermittelt werden muss, sieht der Code beispielsweise folgendermaßen aus:

public static int FindLowestIndex(IEnumerable TSpace, T match) where
T: IEqualityComparer
{
	var loopResult = Parallel.ForEach(source,
	(curValue, loopState, curIndex) =>
	{
		if (curValue.Equals(match))
		{
			loopState.Break();
		}
	});
	var matchedIndex = loopResult.LowestBreakIteration;
	return matchedIndex.HasValue ? matchedIndex : -1;
}


In diesem Beispiel wird die Schleife ausgeführt, bis ein Objekt gefunden wurde. Das Break () - Signal bedeutet, dass nur Elemente mit einem niedrigeren Index als das gefundene Objekt verarbeitet werden sollen. Wenn eine andere übereinstimmende Instanz gefunden wird, wird das Break () - Signal erneut empfangen. Dies wird wiederholt, bis Elemente vorhanden sind. Wenn das Objekt gefunden wurde, zeigt das LowestBreakIteration-Feld auf den ersten Index des übereinstimmenden Objekts.


Warum nicht PLINQ?


Obwohl PLINQ das Beenden der Abfrageausführung unterstützt, sind die Unterschiede in den Exit-Mechanismen von PLINQ und Parallel.ForEach erheblich. Um die PLINQ-Anfrage zu beenden, muss die Anfrage mit einem Abbruchtoken versehen werden, wie hier beschrieben . C Parallel.ForEach- Exit-Flags werden bei jeder Iteration abgefragt. Bei PLINQ können Sie sich nicht auf eine stornierte Anforderung verlassen, um schnell zu stoppen.

Fazit


Parallel.ForEach und PLINQ sind leistungsstarke Tools, mit denen Sie schnell Parallelität in Ihre Anwendungen einführen können, ohne tief in die Mechanismen ihrer Arbeit eintauchen zu müssen. Beachten Sie jedoch die in diesem Artikel beschriebenen Unterschiede und Tipps, um das richtige Tool zur Lösung eines bestimmten Problems auszuwählen.

Nützliche Links:


Threading in C #
RSDN: Arbeiten Sie mit Threads in C #. Parallele Programmierung
Microsoft-Beispiele für die parallele Programmierung mit .NET Framework

Jetzt auch beliebt: