Autor Beitrag
Gausi
ontopic starontopic starontopic starontopic starontopic starontopic starofftopic starofftopic star
Beiträge: 8535
Erhaltene Danke: 473

Windows 7, Windows 10
D7 PE, Delphi XE3 Prof, Delphi 10.3 CE
BeitragVerfasst: So 04.10.20 12:11 
Ich hätte da mal wieder eine kleine konzeptionelle Frage.

Folgende Situation: Ich habe für meine Audio-Bibliothek den Code ein wenig geändert und verwende dort nun ein Factory-Pattern.

ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
function TAudioFileFactory.CreateAudioFile(aFilename: UnicodeString): TBaseAudioFile;
var ext: UnicodeString;
  aClass: TBaseAudioFileClass;
begin
  ext := AnsiLowercase(ExtractFileExt(aFileName));
  // Aus der Dateiendung die konkrete Klasse bestimmen
  aClass := GetClass(ext);
  // Instanz erzeugen
  if assigned(aClass) then
    Result := aClass.Create
  else
    Result := Nil;
end;


Soweit so gut. Ich kann damit dann im Anwendungs-Code ein TBaseAudioFile (abstrakte Basisklasse) deklarieren, und mir über die Factory eine passende Instanz einer konkreten Klasse erzeugen lassen, das ich dann weiter verwenden kann.

Die Methode GetClass verwendet ein TDictionary, alternativ für ältere Delphi-Versionen eine einfache TObjectList, der ich jedoch eine "Selbstanordnung" spendiert habe, d.h. bei Lese-Zugriff auf einen Eintrag wird dieser nach oben geschoben, so dass wiederholte Zugriffe auf dieses Element schneller gehen.

Zur Erzeugung der Factory-Klasse nutze ich das Konzept über eine global bekannte Funktion und einer lokalen Variable (das ist wohl auch ziemlich standard so)
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
function AudioFileFactory: TAudioFileFactory;
begin
  if not Assigned(fLocalAudioFileFactory) then
    fLocalAudioFileFactory := TAudioFileFactory.Create;
  Result := fLocalAudioFileFactory;
end;


Problem ist: Das brauche ich in verschiedenen Threads. Meine selbstanordnende Liste ist völlig klar nicht thread-safe. Zu TDictionary gibt es im Netz auch widersprüchliche Aussagen. Wenn ich mir den TDictionary-Code anschaue, scheint der bei reinem Lesezugriff(!) thread-safe zu sein - aber es gibt natürlich keine Garantie, dass das so bleibt. :?

Frage, wie sichere ich das am besten ab?
  • Erstelle ich mir für jeden Thread eine eigene Factory? Dann müssten meine Objekte (oder zumindest die Factory-relevanten Methoden) den Thread kennen, in dem sie laufen. Ich bräuchte dann ca. ein halbes Dutzend Factories, wobei praktisch immer höchstens eine arbeitet. Halte ich für nicht so elegant.
  • Soll ich darauf vertrauen, dass TDictionary thread-safe ist (meine Alternative mit der ObjectList würde ich dann um-implementieren)? Augen zu und durch, wird schon nicht schief gehen?
  • Oder packe ich die GetClass-Aufrufe in eine CriticalSection? Aber wäre das für SingleThread-Anwendungen nicht eine Verschwendung von Ressourcen? GetClass kann dabei so 10-100x pro Sekunde aufgerufen werden, insgesamt mehrere 10.000mal in einer Anwendung.
    Kleine Testläufe mit sowas
    ausblenden Delphi-Quelltext
    1:
    2:
    3:
    4:
    5:
    6:
    function AddOneCS(value: Integer): Integer;
    begin
      EnterCriticalSection(CS);
      result := value + 1;
      LeaveCriticalSection(CS);
    end;
    deuten an, dass bei 1.000.000 Durchläufen die Zeit messbar anders wird (ca. 920 bzw. 950ms, inkl. Ausgabe in eine Memo alle 1000 Schritte). Bei den zu erwartenden Aufrufzahlen in meinem Fall wäre das aber vernachlässigbar. Aber irgendwie finde ich das auch nicht ideal.


Wie handhabt ihr sowas? Auf hard-codierte if-then-else-Konstrukte zur Bestimmung der richtigen Klasse habe ich nämlich auch nicht so Lust...

_________________
We are, we were and will not be.
jfheins
ontopic starontopic starontopic starontopic starontopic starontopic starofftopic starofftopic star
Beiträge: 918
Erhaltene Danke: 158

Win 10
VS 2013, VS2015
BeitragVerfasst: Mo 05.10.20 08:35 
Moin :-)

Du musst natürlich bereits function AudioFileFactory: TAudioFileFactory; um eine critical section ergänzen, damit du auch wirklich nur eine Instanz bekommt. (Potenzielles Speicherleck). Ich würde erstmal das probieren und dann gucken ob es noch benutzbar ist.

Zitat:
Aber wäre das für SingleThread-Anwendungen nicht eine Verschwendung von Ressourcen?

Bei single thread sollte die CS nur minimal overhead haben, da der Fall "A möchte rein, B ist schon drin" ja nicht auftreten sollte. Und der zieht ja die Performance runter, wenn Threads wirklich warten müssen.

Für diesen Beitrag haben gedankt: Gausi
jaenicke
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starofftopic star
Beiträge: 19272
Erhaltene Danke: 1740

W11 x64 (Chrome, Edge)
Delphi 11 Pro, Oxygene, C# (VS 2022), JS/HTML, Java (NB), PHP, Lazarus
BeitragVerfasst: Mo 05.10.20 18:41 
Nimm keine Critical Section, zumindest für neuere Delphiversionen. Das ist für deinen Zweck performancemäßig absoluter Overkill.

TMonitor.Enter / TMonitor.Exit ist in aktuellen Delphiversionen deutlich schneller nach einer Optimierung ca. in den ersten XE Versionen. Da laufen zuerst ein paar Spins und wenn man wie hier nur sehr kurze Verweilzeiten in der geschützten Sektion hat, ist das sehr viel schneller als eine Critical Section. Erst nach den Spins werden echte Locks verwendet.

Aber für diesen Fall:
Ja, ein TDictionary ist in der aktuellen Implementierung (!) threadsicher solange man nur lesend darauf zugreift. Da dies aber nicht in der Dokumentation steht, könnte eine zukünftige Änderung dies theoretisch ändern. (Ich halte es allerdings für unwahrscheinlich, dass das passiert.)
Bei nur wenigen Einträgen in der Liste vermute ich allerdings, dass ein TDictionary kaum einen Performancevorteil gegenüber einer Liste oder auch einem einfachen Array hat.

user profile iconGausi hat folgendes geschrieben Zum zitierten Posting springen:
Erstelle ich mir für jeden Thread eine eigene Factory?
Es würde reichen eine threadvar zu verwenden, die ggf. den Inhalt aus einer globalen Variable bekommt, wenn sie noch nicht initialisiert ist. Der Rest der Factory kann dann auch nur einmal existieren.

Für diesen Beitrag haben gedankt: Gausi, Sinspin
Gausi Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starofftopic starofftopic star
Beiträge: 8535
Erhaltene Danke: 473

Windows 7, Windows 10
D7 PE, Delphi XE3 Prof, Delphi 10.3 CE
BeitragVerfasst: Mo 05.10.20 20:48 
user profile iconjfheins hat folgendes geschrieben Zum zitierten Posting springen:
Du musst natürlich bereits function AudioFileFactory: TAudioFileFactory; um eine critical section ergänzen, damit du auch wirklich nur eine Instanz bekommt. (Potenzielles Speicherleck).

Jep, das ist mir auch klar. Für den Fall würde ich dann aber einfach eine globale Factory-Variable nehmen, die im Initialization-Abschnitt erstellt wird. Da sollten dann beim Aufbau der Factory keine Threads reinpfuschen können.

user profile iconjaenicke hat folgendes geschrieben Zum zitierten Posting springen:
Nimm keine Critical Section, zumindest für neuere Delphiversionen. Das ist für deinen Zweck performancemäßig absoluter Overkill.

TMonitor.Enter / TMonitor.Exit ist in aktuellen Delphiversionen deutlich schneller nach einer Optimierung ca. in den ersten XE Versionen.

Bei meinen Vorab-Recherchen habe ich zunächst diesen Blog gefunden, und darin dann jetzt (etwas versteckt) den Link zu diesem Update. Das klingt für mich so, als wären die beiden Varianten in etwa äquivalent. Kannst du das ggf. näher erläutern, oder hast du noch mehr Quellen dazu? Ich gebe zu, dass ich die beiden Artikel noch nicht ganz durchblicke ... :les:
Das Thema "Fairness" kann ich bei mir wohl quasi vernachlässigen. In der Regel läuft nur ein Thread, und ggf. möchte ein zweiter auch mal kurz eine einzelne Aktion durchführen - aber das war es dann.

Zum Dictionary generell: Ja, das ist in dem Fall vermutlich wirklich Overkill. Ich könnte mir auch gut vorstellen, dass meine selbstanordnende TOjectList letztlich schneller ist (und vermutlich das Optimum) - der übliche Usecase ist, dass sehr oft auf den gleichen Eintrag zugegriffen wird, und nur ab und zu auf andere. Aber da brauche ich dann auf jeden Fall Thread-Sicherheit, da in dem Fall ein Lesezugriff die Liste verändert. Die Liste hat ca. 20 Einträge, und vermutlich wird zu 99% auf höchstens 5 davon zugegriffen.

Auf der anderen Seite ist das alles egal, weil das nicht der Flaschenhals ist: Danach wird eine Datei geöffnet, und teilweise gelesen und geparsed ...

_________________
We are, we were and will not be.
jaenicke
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starofftopic star
Beiträge: 19272
Erhaltene Danke: 1740

W11 x64 (Chrome, Edge)
Delphi 11 Pro, Oxygene, C# (VS 2022), JS/HTML, Java (NB), PHP, Lazarus
BeitragVerfasst: Di 06.10.20 06:50 
user profile iconGausi hat folgendes geschrieben Zum zitierten Posting springen:
Das klingt für mich so, als wären die beiden Varianten in etwa äquivalent.
Das stimmt für viele Fälle auch. In wenigen echten Programmen hatte ich allerdings teilweise deutliche Vorteile bei TMonitor. Da ich mich nicht damit beschäftigt habe, weiß ich nicht genau warum. Aber an einer Stelle lag ich oben falsch: Die Critical Section nutzt genauso Spin-Locks.

TMonitor sollte aber zumindest in so gut wie allen Fällen nicht spürbar langsamer sein und ist natürlich deutlich einfacher zu benutzen.

Für diesen Beitrag haben gedankt: Gausi