Entwickler-Ecke
Basistechnologien - Ende des letzten Threads, events
funcry - So 26.04.09 12:15
Titel: Ende des letzten Threads, events
An diesem Problem beiße ich mir derzeit die Zähne aus.
Problem:
Nach der Abarbeitung einer Reihe von Threads möchte ich einen Event:
C#-Quelltext
1:
| csv_files_added(this, new CSV_Control_EventArgs(current_csv_core)); |
auslößen - jedoch, das Event soll nicht im Thread-Kontext aufgerufen werden, weil das nachfolgend Probleme verursacht. Innerhalb jedes einzelnen Threads werden Events ausgelößt welche für das UI nötig sind, weshalb der GUI Thread nicht blockiert werden darf. Im Prinzip würde ein BackgroudWorker passen, jedoch ist die Klasse ohne GUI.
Hier der ursprüngliche Ansatz:
C#-Quelltext
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24:
| public void set_csv_files(string path) { this.all = new List<CSV_Core>();
DirectoryInfo di = new DirectoryInfo(path); FileInfo[] fi = di.GetFiles("*.*");
int cnt_threads = fi.GetLength(0); for (int i = 0; i < fi.GetLength(0); i++) { ThreadPool.QueueUserWorkItem(delegate(object param) { FileInfo _fi = (FileInfo)param; CSV_Core current_csv_core = new CSV_Core(_fi.FullName, Config.csv_field_seperator, Config.csv_decimal_thousand_seperator, Config.csv_cnt_columns, Config.csv_symbol_column, Config.csv_date_column, Config.csv_data_column);
this.all.Add(current_csv_core);
if (csv_file_added != null) csv_file_added(this, new CSV_Control_EventArgs(current_csv_core));
if (Interlocked.Decrement(ref cnt_threads) == 0) if (csv_files_added != null) csv_files_added(this, new CSV_Control_EventArgs(current_csv_core)); }, fi[i]); } } |
Meine erste Überlegung war, anstelle des ThreadPools einach Threads verwenden, und diese mit join abschließen. Das Problem dabei, ich blockiere den GUI Thread was sofort zu einem deathlock führt.
Lösungsansatz:
Ich nutze anstelle des ThreadPools BeginInvoke. Hier habe ich einen Callback welcher vielleicht nicht im Thread-Kontext aufgerufen wird. Möglicherweise habe ich den Mechanismus nicht korrekt angewendet. Zwar arbeitet das Programm, aber auch hier ist das letzte Event im Thread-Kontext.
C#-Quelltext
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38:
| private delegate void _del_scf(FileInfo _fi); private int _cnt_scf;
public void set_csv_files(string path) { this.list_csv_core = new List<CSV_Core>();
DirectoryInfo di = new DirectoryInfo(path); FileInfo[] fi = di.GetFiles("*.*");
this._cnt_scf = fi.GetLength(0); for (int i = 0; i < fi.GetLength(0); i++) { _del_scf d = new _del_scf(_dowork_scf); d.BeginInvoke(fi[i], new AsyncCallback(_callback_scf), d); } }
private void _dowork_scf(FileInfo _fi) { CSV_Core current_csv_core = new CSV_Core(_fi.FullName, Config.csv_field_seperator, Config.csv_decimal_thousand_seperator, Config.csv_cnt_columns, Config.csv_symbol_column, Config.csv_date_column, Config.csv_data_column);
this.list_csv_core.Add(current_csv_core); if (csv_file_added != null) csv_file_added(this, new CSV_Control_EventArgs(current_csv_core));
Interlocked.Decrement(ref this._cnt_scf); }
private void _callback_scf(IAsyncResult ar) { _del_scf d = (_del_scf)ar.AsyncState; d.EndInvoke(ar);
if (this._cnt_scf == 0) { if (csv_files_added != null) csv_files_added(this, new CSV_Control_EventArgs(null)); } } |
Unabhängig davon dass ich mein Ziel damit nicht erreiche - auch hier wird:
C#-Quelltext
1:
| csv_files_added(this, new CSV_Control_EventArgs(null)); |
im Thread kontext aufgerunfen, stört mich an dieser Lösung vorallem, dass ich eine globale Variable
this._cnt_scf brauche um herauszufinden wann der letzte Thread beendet wurde.
Da ich nur vc express nutze fehlt mir leider eine genaue Darstellung der Threads. Um herauszufinden ob das letzte Event im thread-Kontext aufgerunfen wird nutze in in meiner GUI-Klasse dieses Statement:
C#-Quelltext
1:
| if (dgv_csv_master.InvokeRequired) dgv_csv_master.Invoke(new MethodInvoker(delegate { _csv_control_csv_files_added(sender, e); })); |
Kha - So 26.04.09 12:59
funcry hat folgendes geschrieben : |
| jedoch, das Event soll nicht im Thread-Kontext aufgerufen werden, weil das nachfolgend Probleme verursacht. |
funcry hat folgendes geschrieben : |
| Im Prinzip würde ein BackgroudWorker passen, jedoch ist die Klasse ohne GUI. |
Das ist für mich ein Widerspruch: Wenn die Klasse GUI-unabhängig ist, kann sie sich nicht um die Synchronisation kümmern, das muss ihr Konsument erledigen. Dummerweise ;) gibt es trotzdem dafür eine Lösung: Feuere die Events innerhalb von
SynchronizationContext.Current.Post ab.
funcry hat folgendes geschrieben : |
| Hier habe ich einen Callback welcher vielleicht nicht im Thread-Kontext aufgerufen wird. |
Nope, es findet kein Threadwechsel statt. Was ja eben, von SynchronizationContext abgesehen, überhaupt nicht möglich wäre.
funcry hat folgendes geschrieben : |
| [...] stört mich an dieser Lösung vorallem, dass ich eine globale Variable this._cnt_scf brauche um herauszufinden wann der letzte Thread beendet wurde. |
Könntest du vermeiden, indem du den Callback als anonyme Methode/Lambda einbettest.
funcry hat folgendes geschrieben : |
| Da ich nur vc express nutze fehlt mir leider eine genaue Darstellung der Threads. |
Wie meinen :gruebel: ?
PS: Da du ja schon mit der TPL gearbeitet hast: Mit der sollte sich die Interlocked-Klasse vermeiden lassen. Ansonsten: Lass wenigstens diese fürchterlichen Unterstriche weg :P .
Edit: Dein Code ist nicht threadsafe, du müsstest um jeden Zugriff auf
all locken.
funcry - So 26.04.09 14:08
Danke für die Antworten!
An diesem Problem habe ich das ganze WE getüftelt :-)
Die Unterstriche werde ich später vermutlich noch rausnehmen, ist mit dem Refractor ja nicht allzu aufwändig.
funcry - Di 28.04.09 15:02
Nach einigem Abwägen habe ich deinen Ratschlag versucht umzusetzen. Ich habe deshalb gezögert, weil ich es auch als elegant empfunden hätte, wenn die GUI sich, mit eher komplizierten Dingen wie Threading, nicht beschäftigten müsste, sonder einfach eine Klasse nutzen könnte, welche dies erledigt. Andererseits geht mir auch darum einen sauberen Programmier-Stil zu verwenden.
Eigentlich wollte ich dabei auch gleich die Unterstriche herausnehmen, was mir jedoch nicht gelingen will. Da ich ja auch auf die Großschreibung bei Variablen verzichte, bekomme ich einfach keine lesbaren Variablen bzw. Methoden hin. Über Ideen und Hinweise - auch ganz allgemein zur Programmierung des folgenden Codes, freue ich mich.
Hier die aktuelle Umsetzung:
C#-Quelltext
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58:
| private void _UI_DGV_CSV_Load(object sender, EventArgs e) {
bgw_set_files.RunWorkerAsync(Config.path_csv); }
private void bgw_set_files_DoWork(object sender, DoWorkEventArgs e) { string path = e.Argument as string; this.list_all_csv_cores = new List<CSV_Core>(); ManualResetEvent mre_done = new ManualResetEvent(false);
DirectoryInfo di = new DirectoryInfo(path); FileInfo[] fi = di.GetFiles("*.*");
int cnt_threads = fi.GetLength(0); for (int i = 0; i < fi.GetLength(0); i++) { ThreadPool.QueueUserWorkItem(delegate(object param) { string _full_filename_csv = (string)param; CSV_Core current_csv_core = new CSV_Core(_full_filename_csv, Config.csv_field_seperator, Config.csv_decimal_thousand_seperator, Config.csv_cnt_columns, Config.csv_symbol_column, Config.csv_date_column, Config.csv_data_column);
this.list_all_csv_cores.Add(current_csv_core);
this._file_added(current_csv_core);
if (Interlocked.Decrement(ref cnt_threads) == 0) mre_done.Set(); }, fi[i].FullName); }
mre_done.WaitOne(); }
private void bgw_set_files_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { this._files_added(); }
private void _file_added(CSV_Core csv_core) { if (dgv_csv.InvokeRequired) dgv_csv.Invoke(new MethodInvoker(delegate { _file_added(csv_core); })); else { dgv_csv.Rows.Add(false, csv_core.symbol_name, null, csv_core.first_date, csv_core.last_date); } }
private void _files_added() { dgv_csv.AutoResizeColumns(); dgv_csv.SelectionMode = DataGridViewSelectionMode.FullRowSelect; dgv_csv.MultiSelect = false; dgv_csv.Sort(dgv_csv.Columns["Symbol"], ListSortDirection.Ascending); dgv_csv[0, 0].Selected = true; dgv_csv.Rows[0].Selected = false; } |
Kha - Di 28.04.09 19:40
funcry hat folgendes geschrieben : |
| Ich habe deshalb gezögert, weil ich es auch als elegant empfunden hätte, wenn die GUI sich, mit eher komplizierten Dingen wie Threading, nicht beschäftigten müsste, sonder einfach eine Klasse nutzen könnte, welche dies erledigt. |
Muss sie auch nicht, nur die Synchronisierung muss sie übernehmen - oder eben doch SynchronizationContext.
funcry hat folgendes geschrieben : |
| Da ich ja auch auf die Großschreibung bei Variablen verzichte,[...] |
Alle Style-Guides schreiben für lokale Variablen camelCase vor.
So, ich versuche mich mal am Code. Ich nehme an, der Bottleneck ist der CsvCore-Konstruktor?
C#-Quelltext
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36:
| CsvAggregator aggregator = new CsvAggregator(); ... aggregator.CoreRead += (sender, e) => BeginInvoke((Action) () => AddCore(e.Core)); aggregator.ReadCompleted += (sender, e) => BeginInvoke((Action)FinishHim);
... private void CsvView_Load(object sender, EventArgs e) { ThreadPool.QueueUserWorkItem(ignore => aggregator.ReadDirectory(Config.CsvPath)); }
private void AddCore(CsvCore core); private void FinishHim(); ...
class CsvAggregator { public event EventHandler ReadCompleted; public event EventHandler<CoreReadEventArgs> CoreRead; public void ReadDirectoryAsync(string path) { string[] files = Directory.GetFiles(path); int threadCount = files.Length; foreach (string file in files) ThreadPool.QueueUserWorkItem(delegate { CsvCore core = new CsvCore(file, Config); OnCoreRead(core);
if (Interlocked.Decrement(ref threadCount) == 0) OnReadCompleted(); }); } } |
Ich habe mal das eigentliche Liste-Führen der CsvCores in die GUI verschoben, um mich nicht mit der Race Condition befassen zu müssen, was im Kontext ganz ok oder völlig unpassend sein könnte ;) . Und zwei verschachtelte Lambdas könnten etwas übertrieben sein, aber so hat man die Synchronisierung an einem Platz.
funcry - Mi 29.04.09 16:03
Vielen Dank!
| Zitat: |
| Ich nehme an, der Bottleneck ist der CsvCore-Konstruktor? |
Richtig.
Damit ist der Grundstein gelegt, ähnlich zu dieser Klasse sind andere Klassen in meinem Projekt die teilweise umfangreicher sind, weshalb ich besonders froh bin, anhand dieses Falles, eine best practice für die Organisation des Codes zu haben :-) !
Entwickler-Ecke.de based on phpBB
Copyright 2002 - 2011 by Tino Teuber, Copyright 2011 - 2026 by Christian Stelzmann Alle Rechte vorbehalten.
Alle Beiträge stammen von dritten Personen und dürfen geltendes Recht nicht verletzen.
Entwickler-Ecke und die zugehörigen Webseiten distanzieren sich ausdrücklich von Fremdinhalten jeglicher Art!