www.rolandk.de
- Aktuelle Themen zu .Net -
Achtung: Hier handelt es sich um meine alte Seite.
Die aktuelle ist unter folgendem Link erreichbar: www.rolandk.de/wp/
Home Blog Threading Probleme




















































Threading Probleme
Mittwoch, den 18. Juli 2012 um 19:30 Uhr

Threads in C#-Programmen zu verwenden ist an sich relativ einfach. Egal, ob man fertige Komponenten wie BackgroundWorker, die Parallel Task Library, das neue async-Schlüsselwort oder die gute alte Thread-Klasse verwendet. Wirklich kompliziert ist es oft nicht. Das beim Einsatz von Threads auch solche Sachen wie Synchronisierung zu beachten sind, ist soweit bekannt. So müssen gemeinsam verwendete Variablen korrekt synchronisiert sein. Leider wird das gelegentlich an verschiedenen Stellen vergessen. In diesem Artikel will ich zwei kleine Beispiele zeigen, bei denen das fatal sein kann und zu zum Teil schwer auffindbaren Fehlern führt.

 

Paralleler Zugriff auf List<T>

Für eine kleine Demonstration will ich ein Beispielprogramm zeigen, bei welchen mehrere Threads auf eine Liste zugreifen und dabei jeweils in einer Schleife ca. 1000 Einträge hinzufügen. Es wird also die Methode Add parallel aufgerufen. Die gemeinsame Variable ist die Liste s_stringList.

  1. private static List s_stringList;

Jeder Thread ist wie nachfolgend codiert und enthält eine Schleife mit 5000 Durchläufen und ruft dabei in jedem Durchlauf die Methode Add auf. Jeder Eintrag ist dabei eindeutig Identifizierbar, somit kann am Ende des Programmes überprüft werden, ob auch wirklich alle Einträge in der Liste enthalten sind – sofern das Programm bis dahin nicht abstürzt.

  1. /// <summary>
  2. /// Main method of Thread 1.
  3. /// </summary>
  4. public static void ThreadMain1()
  5. {
  6. Console.WriteLine("Started Thread 1..");
  7.  
  8. try
  9. {
  10. for (int loop = 0; loop < 5000; loop++)
  11. {
  12. s_stringList.Add("ENTRY_THREAD1_" + loop.ToString().PadLeft(4, '0'));
  13. Thread.Sleep(1);
  14. }
  15. }
  16. catch (Exception ex)
  17. {
  18. Console.WriteLine("Exception in thread 1: " + ex.Message);
  19. }
  20.  
  21. Console.WriteLine("Finished Thread 1..");
  22. }

Die anderen Threads sind genauso implementiert. Das Hauptprogramm schließlich kümmert sich darum, dass die Threads angelegt und gestartet werden (insgesamt 3), wartet, bis die Threads fertig sind am Ende wird überprüft, ob alle Einträge in der Liste enthalten sind, die eigentlich enthalten sein sollten.

  1. /// <summary>
  2. /// Main method.
  3. /// </summary>
  4. public static void Main(string[] args)
  5. {
  6. s_stringList = new List<string>();
  7.  
  8. Thread thread1 = new Thread(new ThreadStart(ThreadMain1));
  9. thread1.Start();
  10.  
  11. Thread thread2 = new Thread(new ThreadStart(ThreadMain2));
  12. thread2.Start();
  13.  
  14. Thread thread3 = new Thread(new ThreadStart(ThreadMain3));
  15. thread3.Start();
  16.  
  17. thread1.Join();
  18. thread2.Join();
  19. thread3.Join();
  20.  
  21. //Check for existance of each entry
  22. for (int loop = 0; loop < 5000; loop++)
  23. {
  24. string key1 = "ENTRY_THREAD1_" + loop.ToString().PadLeft(4, '0');
  25. string key2 = "ENTRY_THREAD2_" + loop.ToString().PadLeft(4, '0');
  26. string key3 = "ENTRY_THREAD3_" + loop.ToString().PadLeft(4, '0');
  27. if (!s_stringList.Contains(key1))
  28. {
  29. Console.WriteLine("Entry " + key1 + " is missing!");
  30. }
  31. if (!s_stringList.Contains(key2))
  32. {
  33. Console.WriteLine("Entry " + key2 + " is missing!");
  34. }
  35. if (!s_stringList.Contains(key3))
  36. {
  37. Console.WriteLine("Entry " + key3 + " is missing!");
  38. }
  39. }
  40.  
  41. Console.WriteLine();
  42. Console.WriteLine("-- finished");
  43. Console.ReadLine();
  44. }

Was ist wohl das Ergebnis?
Exception kommt wider erwarten keine, aber: Einige Einträge fehlen – zumindest bei den meisten Versuchen. Nachfolgender Screenshot zeigt einen Versuch auf meinem Rechner.

 

 

Was bedeutet das? Exception kommt keine, das heist, ein klarer Hinweis auf das Problem bleibt aus. Es fehlen aber einige Einträge in der Liste ohne dass man einen direkten Hinweis darauf hat. Ganz klar ein großes Problem, wenn sich ein solcher Fehler in einem größeren Programm verbirgt.

 

Pralleler Zugriff auf Dictionary<TKey, TValue>

Im nächsten Beispiel zeige ich ein in meinen Augen wesentlich größeres Problem bei der bekannten Klasse Dictionary<TKey, TValue>. Im Beispiel gibt es wieder mehrere Threads, welche jeweils eine Endlos-Schleife durcharbeiten, innerhalb der Schlüssel in einer Dictionary angelegt und gelöscht werden. Für dieses Beispiel reichen zwei Threads. Nachfolgende gemeinsamen Variablen werden benötigt.

  1. private static Dictionary<string, string> s_dictionary;
  2. private static List<string> s_keys;
  3. private static Random s_randomizer;

Wie bereits gesagt, wird die Dictionary von mehreren Threads parallel bearbeitet. Die Liste s_keys enthält dabei alle möglichen Schlüssel, welche die Threads setzen oder löschen können. Das Random Objekt s_randomizer sorgt dafür, dass die Threads bei jedem Durchlauf zufällig einen anderen Schlüssel verwenden. Nachfolgender Code ist die Methode, die für jeden Thread aufgerufen wird. Sie beinhaltet wie beschrieben eine Endlosschreibe (while(true)) und in jedem Durchlauf wird zufällig ein Schlüssel ausgewählt, welcher anschließend gesetzt und danach gleich entfernt wird.

  1. /// <summary>
  2. /// A method called by more threads that performs read and write operations
  3. /// on the dictionary.
  4. /// </summary>
  5. private static void UpdateThread()
  6. {
  7. while (true)
  8. {
  9. Thread.Sleep(s_randomizer.Next(1, 3));
  10.  
  11. string newValue = Guid.NewGuid().ToString();
  12. try
  13. {
  14. string keyToManipulate = s_keys[s_randomizer.Next(0, s_keys.Count)];
  15. s_dictionary[keyToManipulate] = newValue;
  16. s_dictionary.Remove(keyToManipulate);
  17. }
  18. catch (Exception ex)
  19. {
  20. Console.WriteLine("Exception occurred: " + ex.Message);
  21. }
  22.  
  23. Console.WriteLine(
  24. "Updated dictionary by thread " + Thread.CurrentThread.ManagedThreadId +
  25. ", Value: " + newValue);
  26. }
  27. }

Im nächsten Ausschnitt folgt noch das Hauptprogramm. Darin nichts besonderes mehr, die Threads werden gestartet. Beendet wird das Programm nur, wenn der Benutzer auf die Enter-Taste drückt.

  1. /// <summary>
  2. /// Main method.
  3. /// </summary>
  4. public static void Main()
  5. {
  6. s_randomizer = new Random();
  7. s_dictionary = new Dictionary<string, string>();
  8. s_keys = new List<string>();
  9.  
  10. //Generate keys
  11. for (int loop = 0; loop < 1000; loop++)
  12. {
  13. s_keys.Add(Guid.NewGuid().ToString());
  14. s_dictionary[s_keys[loop]] = string.Empty;
  15. }
  16.  
  17. Thread updateThread = new Thread(new ThreadStart(UpdateThread));
  18. updateThread.Start();
  19.  
  20. updateThread = new Thread(new ThreadStart(UpdateThread));
  21. updateThread.Start();
  22.  
  23. Console.ReadLine();
  24. }

Was geschieht jetzt, wenn dieses Programm ausgeführt wird? Zunächst kommt alles wie erwartet, wie im nachfolgenden Screenshot wird scheinbar endlos lange auf die Console geschrieben.

 

 

Das Problem: Nach ein paar Sekunden bleibt das Programm plötzlich stehen, nichts reagiert mehr. Unterbricht man das Programm per Visual Studio Debugger, so sieht man, dass beide Arbeits-Threads an nachfolgender Stelle hängen.

 

 

Genau das ist das fatale, von dem ich vorher geschrieben habe. Achtet man bei Dictionaries nicht darauf, dass Zugriffe von verschiedenen Threads aus nicht vernünftig synchronisiert sind, so entsteht die Gefahr, dass das Programm aus schwer erkennbaren Gründen hängen bleibt.

 

Fazit

Wie anfangs beschrieben bin ich auf zwei Probleme eingegangen, die bei falscher Thread-Synchronisierung entstehen können. Vor allem letzterer Fall war für mich persönlich sehr überraschend. Es handelt sich hier nur um zwei Beispiele, die zeigen, wie wichtig korrekte Thread-Synchronisierung ist. Im täglichen Alltag eines Entwicklers gibt es selbstverständlich sehr viel mehr Situationen, die zu ähnlichen oder sogar zu schlimmeren Effekten führen können. Wissen kann man es oft nie genau, was so alles passieren kann, wenn man’s falsch macht.

 

Kommentar hinzufügen

Ihr Name:
Kommentar: