Ziel der Übung ist es zu verhindern daß auf keinen Fall beide Threads
gleichzeitig in der FileRead/Write API stecken, insbesondere hängt es
wenn A gerade schreibt und dann B kommt und FileRead aufruft, dann
verklemmt sich die Windows-API derart daß FileWrite hängenbleibt.
Erschwernd kommt hinzu daß FileRead blockiert, wenn also B zuerst da war
muss A ihm mit CancelIOEx erstmal einen Fußtritt verpassen.
So wie es oben steht scheint es zwar zu laufen, aber ich sehe da immer
noch eine Möglichkeit, ich hab mal mit Pfeilen eingezeichnet wo sich die
Ausführung theoretisch befinden könnte, das ist zwar unwahrscheinlich
aber durchaus möglich. Wenn jetzt A ins FileWrite läuft und dann B ins
FileRead dann verklemmt sich der darunter liegende Treiber wieder.
Thread A
1
<--Aisthier
2
3
Lock.Acquire;
4
CancelIoEx(FDevice,nil);
5
FileWrite(FDevice,SendBuf,SizeOf(SendBuf));
6
Lock.Release;
Thread B
1
Lock.Acquire;
2
Lock.Release;
3
4
<--Bisthier
5
6
Len:=FileRead(FDevice,Buf,SizeOf(Buf));
Hat jemand ne wasserdichte Idee, gerne auch mit 2 oder 3 Locks?
Ja das Locking ist einfach falsch, egal wie
wahrscheinlich/unwahrscheinlich der Fall des Eintretens ist. Read gehört
vor Lock Release.
Hier zum Üben:
https://deadlockempire.github.io/
Abradolf L. schrieb:> Ja das Locking ist einfach falsch, egal wie> wahrscheinlich/unwahrscheinlich der Fall des Eintretens ist. Read gehört> vor Lock Release.
Dann kann aber CancelIOEx() nicht mehr aufgerufen werden. So einfach ist
es also nicht. Nächster Vorschlag?
Bernd K. schrieb:> Abradolf L. schrieb:>> Ja das Locking ist einfach falsch, egal wie>> wahrscheinlich/unwahrscheinlich der Fall des Eintretens ist. Read gehört>> vor Lock Release.>> Dann kann aber CancelIOEx() nicht mehr aufgerufen werden. So einfach ist> es also nicht. Nächster Vorschlag?
Warum?
Wilhelm M. schrieb:> Bernd K. schrieb:>> Abradolf L. schrieb:>>> Ja das Locking ist einfach falsch, egal wie>>> wahrscheinlich/unwahrscheinlich der Fall des Eintretens ist. Read gehört>>> vor Lock Release.>>>> Dann kann aber CancelIOEx() nicht mehr aufgerufen werden. So einfach ist>> es also nicht. Nächster Vorschlag?>> Warum?
Weil Thread B das Lock hält.
Wilhelm M. schrieb:> Bernd K. schrieb:>>> Thread B>>> Lock.Acquire;>> Lock.Release;>> Len := FileRead(FDevice, Buf, SizeOf(Buf));>> >>> So ist der Lock doch wertlos.
Ja, deshalb hab ich ja die Frage gepostet.
Niemals blockierende Systemcalls in CS!
Deswegen muss Du schauen, das FileRead nicht blockiert.
Also muss Du abfragen ob Du lesen kannst. Deswegen brauchst hier einen
bedingten kritischen Abschnitt -> ConditionVariable
Nein. B ist falsch. Kein blockierenden Aufrufe in kritischen
Abschnitten.
Du musst einen "Begingten kritschen Abschnitt" aus B machen, sonst droht
DeadLock.
Wilhelm M. schrieb:> Klar geht das.> Aus Unix mit select().
Blockierendes Read ist an sich keine schlechte Sache (wenn es
funktioniert), das Aufteilen getrennter Abläufe in getrennte Threads ist
eine Alternative zur großen Statemachine, eine bewußte
Designentscheidung die (IMHO) oftmals zu lesbarerem Code führen kann.
Das einzige das mir hier in diesem konkreten Fall in die Suppe spuckt
ist der schrottige HID-Treiber in Windows 7 der nicht thread-safe zu
sein scheint und sich weghängt wenn man gleichzeitig FileRead und
FileWrite auf dem selben Handle macht, unter Linux funktioniert das
vollkommen reibungslos und die beiden Threads sind dort vollkommen
voneinander entkoppelt und kommen sich nicht in die Quere.
Nein.
Du liest von einer Queue. Angenommen die Queue ist leer und B der erste.
Dann blockiert B und verursacht den Deadlock. A wird am Mutex blockiert.
Nochmal: des wegen brauchst Du einen "Begingten kritischen Abschnitt" in
B.
Etwa:
Mutex lock:
CondiditionVar cv;
B
lock.acquire();
while (file.empty()) {
cv.wait();
}
file.read();
lock.release();
Andreas H. schrieb:> auf die harte Methode?>> Thread A> CancelIoEx(FDevice, nil);> Lock.Acquire;> FileWrite(FDevice, SendBuf, SizeOf(SendBuf));> Lock.Release;>> Thread B> Lock.Acquire;> Len := FileRead(FDevice, Buf, SizeOf(Buf));> Lock.Release;
Ich male mal die Pfeile ein:
Thread A
CancelIoEx(FDevice, nil);
<-- A ist hier
Lock.Acquire;
FileWrite(FDevice, SendBuf, SizeOf(SendBuf));
Lock.Release;
Thread B
Lock.Acquire;
<-- B ist hier
Len := FileRead(FDevice, Buf, SizeOf(Buf));
Lock.Release;
Jetzt läuft B ins blockierende read und A wartet hilflos bis in alle
Ewigkeit.
Bernd K. schrieb:>> Jetzt läuft B ins blockierende read und A wartet hilflos bis in alle> Ewigkeit.
Das versuch ich ihm doch die ganze Zeit zu sagen ...
Bernd K. schrieb:> Wilhelm M. schrieb:>> file.empty()>> Diese Funktion gibt es leider nicht, sonst wärs ja einfach :-(
Vielleicht so etwas ähnliches wie select() oder poll()? Oder was
anderes?
Wenn das wirklich der Fall sein sollte, was ich nicht glaube (was ist
das eigentlich für ein API???), dann muss Du auf non-blocking IO
umstellen und in B pollen.
Bernd K. schrieb:> Wilhelm M. schrieb:>> while (file.empty())>> Inwiefern ist das anders als ein blockierendes Read?
Schau Dir mal ein Buch über BEGINGTE kritische Abschnitte an.
file.empty() sollte ja wohl nicht blockieren, oder?
Wilhelm M. schrieb:> Das versuch ich ihm doch die ganze Zeit zu sagen ...
Deshalb ja meine neue Lösung:
>> while not Lock.TryEnter do>> CancelIoEx(FDevice, nil);>> FileWrite(FDevice, SendBuf, SizeOf(SendBuf));>> Lock.Release;
Jetzt kann A den B aus dem Read rauskicken und gleichzeitig das Lock
bekommen. Das läuft jetzt schon seit einer Stunde mit viel Traffic in
beide Richtungen durch und hat sich noch nicht aufgehängt.
Wilhelm M. schrieb:> Und was entspricht Deiner Meinung nach file.empty() oder file.is_empty()> oder (file.size() == 0) im Windows API ???
Sag Du es mir?
Das Device ist ein generic HID device am USB-Bus, Treiber ist von
Windows. Ich kenne keine Möglichkeit ohne zu lesen festzustellen ob man
lesen kann, es sei denn ich krempel das alles (nur für den Windows-Port)
auf Overlapped-IO um und krempel alles von innen nach außen und darauf
hab ich ehrlich gesagt keinen Bock, das sprengt den Rahmen.
Leider bin ich in der *nix-Welt groß geworden ...
Es gibt aber m.E. auch unter Windows die Möglichkeit, den
Datei-Deskriptor bzw. File-Handle auf ein non-blocking-IO umzustellen.
Unter *nix geht das mit einem ioctl() / fnctl().
Dann könnte Thread B folgendes durchführen (Pseudo-Code):
Bernd K. schrieb:> Ziel der Übung ist es zu verhindern daß auf keinen Fall beide Threads> gleichzeitig in der FileRead/Write API stecken
Wenn das das Ziel war und du TATSÄCHLICH hilfreiche Hinweise haben
willst, musst du schon die Implementierung von "Lock" offen legen. Für
mich sieht das jedenfalls stark nach etwas aus, was jemand benutzt, der
weder "Lock" versteht noch generell das Windows-FileAPI.
Ich hingegen verstehe zwar letzteres durchaus sehr gut, muss aber
bezüglich Hilfe passen, weil "Lock" nur äußerst unzureichend beschrieben
wurde. Eine einfache Win32-CriticalSection (wie es das Subject des
Threads suggeriert) scheint es ja nicht zu sein...
c-hater schrieb:> Bernd K. schrieb:>>> Ziel der Übung ist es zu verhindern daß auf keinen Fall beide Threads>> gleichzeitig in der FileRead/Write API stecken>> Wenn das das Ziel war und du TATSÄCHLICH hilfreiche Hinweise haben> willst, musst du schon die Implementierung von "Lock" offen legen.
Auf gar keinen Fall.
Die Synchronisationsprimitive wie Mutex (Lock), Semaphor,
ConditionVariable, MessageQueue sind in ihrer Semantik wohl definiert in
der Informatik. Genauso wie die Begriffe (unbedingter / bedingter)
Kritische Abschnitt.
c-hater schrieb:> Eine einfache Win32-CriticalSection (wie es das Subject des> Threads suggeriert) scheint es ja nicht zu sein...
Es ist ein Wrapper um eine native CriticalSection auf dem jeweils
zugrundeliegenden OS (in dem Falle Windows). Oder ums genauer zu sagen
es ist eine Instanz von TCriticalSection aus der Unit SyncObjs.
Die Lösung zu dem Problem hab ich ja bereits gefunden:
A
1
repeat
2
3
[...]
4
5
whilenotLock.TryEnterdo
6
CancelIoEx(FDevice,nil);
7
FileWrite(FDevice,SendBuf,SizeOf(SendBuf));
8
Lock.Release;
9
untilTerminated;
B
1
repeat
2
Lock.Acquire;
3
Len:=FileRead(FDevice,Buf,SizeOf(Buf));
4
Lock.Release;
5
6
[...]
7
8
untilTerminated;
Der Grund warum ich mich daran festgebissen habe war der daß ich mich
darauf versteift habe auf Teufel komm raus eine Lösung ohne das while zu
finden, eine geschickte Anordnung von zwei oder mehr Locks die das selbe
bewirkt. Das simple while not Lock.TryEnter war der sprichwörtliche Wald
den ich dann vor Bäumen aus irgendeinem Grund nicht sehen wollte.
c-hater schrieb im Beitrag #4861410:
> Wilhelm M. schrieb:>>> Die Synchronisationsprimitive wie Mutex (Lock), Semaphor,>> ConditionVariable, MessageQueue sind in ihrer Semantik wohl definiert in>> der Informatik. Genauso wie die Begriffe (unbedingter / bedingter)>> Kritische Abschnitt.>> Und schon geloosed.>> "Primitives" haben nicht in jedem Kontext das gleiche "Innenleben".
Das hat niemand behauptet und das muss auch gar nicht so sein.
> Ein> Win32-Mutex z.B. ist defininitiv etwas deutlich anderes als eine> Win32-CriticalSection.
Ja klar, die Win32-Mutex-Implementierung, die das Konzept eines Mutex
umsetzt, ist bspw. zwischen Threads unterschiedlicher Prozesse
verwendbar. Die Win32-CriticalSection-Implementierung realisiert auch
das Konzept eines Mutex, der aber leichtgewichtiger ist in vielen
Aspekten, aber nur zwischen Threads desselben Prozesses verwendet werden
kann.
Im übrigen ist der Name Win32-"CriticalSection" sehr schlecht von MS
gewählt, weil es eben ein Mutex ist.
Dagegen ist eine sog. Critical Section ein Begriff aus der Informatik,
der einen Teil eines Ausführungspfades bezeichnet, der nicht-nebenläufig
ausgeführt werden muss. Zu dessen Schutz kann man Mutexe, Semphore,
RWLocks, Monitore, ... einsetzen.
> Nur C-only-Wichsern
Wen meinst Du damit?
Und ausserdem geht es hier doch gar nicht um "C".
> ist das nicht klar, die allein halten ihren> Sprach-Standard für quasi gottgegeben. Das ist er aber eben NICHT. Was> allerdings eigentlich selbst den absolut Dümmsten dieser Vollidioten> klar sein müsste, schließlich gibt es ja nicht einmal EINEN allgemein> gültigen C-Standard,
den gibt es sehr wohl. Allerdings gibt es eben auch nicht-konforme
Compiler. Die ewige Zwickmühle Standard <-> Umsetzung.
>der auch nur die Sprache selber vollständig> beschreibt, geschweige denn das Verhalten von irgendwelchen Syncobjects> aus irgendwelchen schrägen hinzugelinkten Libs...
Das müssen die Bibliotheken machen, das hat mit der Sprache bzw. konkret
mit "C" hier nichts zu tun. Die C-Standard-Bibliothek macht dies bspw.
für die o.g. Synchronisationsprimitive (aka Synchronisationskonzepte):
http://en.cppreference.com/w/c/thread
Alternativ kommen natürlich auch die durch die Plattform direkt zur
Verfügung gestellten Schnittstellen wie der _POSIX_THREADS Anteil aus
IEEE 1003.1 oder eben aus Win32 in Frage.
Wilhelm M. schrieb:> Das hat niemand behauptet
Mir scheint, du hast das behauptet. Nur indirekt, aber trotzdem sehr
eindeutig, nämlich indem du behauptest hast, das die Implementierung von
"Lock" (was auch immer das gewesen sein mag), keine Rolle spielen würde.
Hier in deiner Antwort gibst du aber ebenso indirekt zu, dass es sehr
wohl eine Rolle spielen könnte, nämlich zumindest dadurch, indem du
zumindest den unterschiedlichen Wirkungsbereich von (Win32-)
CriticalSection und Mutex anerkennst. Die Unterschiede sind aber noch
deutlich weitgehender als das, was du zugestehst, selbst wenn man beides
auf Threads eines einzigen Prozesses anwendet, kann man sie nicht in
jeder Hinsicht "synonym" benutzen. Daran ändert auch die Tatsache
nichts, dass die besonderen Features eines Win32-Mutex innerhalb eines
Prozesses nicht sinnvoll nutzbar sind, man muss bei der Programmierung
trotzdem die Besonderheiten beachten. Es kommt also sehr wohl darauf an,
wie "Lock" implementiert ist. Erstmal darauf, ob es nun tatsächlich auf
Win32-Mutex oder -CriticalSection aufsetzt, und zweitens, falls es auf
einem Mutex aufsetzt, ob dessen Besonderheiten bei der Implementierung
hinreichend gewürdigt wurden.
> Im übrigen ist der Name Win32-"CriticalSection" sehr schlecht von MS> gewählt, weil es eben ein Mutex ist.
Hmm... Das ist wohl Ansichtssache. Aber ich habe keine Lust, über
Nomenklaturen zu streiten. Aber ich gestehe dir gern zu, dass die
MS-Nomenklatur an sehr vielen Stellen zumindest diskussionswürdig ist.
Darüber kann man sich ärgern, darüber kann man diskutieren, aber
letztlich setzt doch das API die Fakten für die Nomenklatur. Und hier
ging es definitiv um das Windows-API und nicht um abstrakte Blasen aus
dem Informatik-Akademiebetrieb, was schon im OP ganz eindeutig
klargestellt wurde...
>> Nur C-only-Wichsern>> Wen meinst Du damit?
Z.B. die Leute, die nicht wissen, was bei der Synchronisation "hinter
den Kulissen" abgeht. Also all die arme Schweine, die höchstens zufällig
mal MT-Anwendungen so hinbekommen, dass kein Deadlock mehr darin lauert.
Ist ja schließlich schon für Leute schwer genug, das zu vermeiden, die
wirklich verstehen, was da im Detail passiert...
> Und ausserdem geht es hier doch gar nicht um "C".
Doch, irgendwie schon. C ist halt nicht für MT designed und bietet
keinerlei Unterstützung zur Vermeidung selbst der primitivsten
Standardfehler in diesem Umfeld. Wie auch für viele andere
Standardfehler nicht, angefangen vom oberprimitivsten Integer-Overflow
bis hin zum Buffer-Overflow...
[...C-Standard...]
> den gibt es sehr wohl.
Nein, es gibt eben nicht nur einen, sondern mehrere. Und noch sehr viel
mehr Interpretationen jedes dieser "Standards"...
> Das müssen die Bibliotheken machen
Wie sollen sie das tun, wenn sich die verschiedenen Standards derselben
Sprache in Details logisch widersprechen? Das ist aus Gründen der
formalen Logik völlig unmöglich. Man kann nicht auf eine indifferente
Basis eine logisch konsistente Implementierung aufsetzen. Das geht
nunmal einfach nicht.
> das hat mit der Sprache bzw. konkret> mit "C" hier nichts zu tun.
Oh doch. Wenn du das nicht erkennst, kannst du einfach kein C.
Allerdings, auch wenn alles wahr ist, was ich schrieb: Ob das nun im
Zshg. mit der Implementierung dieses ominösen "Lock" irgendeine Rolle
spielt, kann man eben nur dann erkennen, wenn eben diese Implementierung
gezeigt wird. Und eben genau darauf zielte ich ab...
Capisce?!
c-hater schrieb:>>>> Nur C-only-Wichsern>>>> Wen meinst Du damit?>> Z.B. die Leute, die nicht wissen, was bei der Synchronisation "hinter> den Kulissen" abgeht.
Und warum bezeichnest Du Leute, von denen Du (!) meinst, dass sie auf
einem bestimmten Gebiet (noch) nicht so viel Wissen haben, als Wichser?
Das ist eine grundlose Beleidigung! In anderen ist das ein
Ausschlußgrund, aber Du bist ja deswegen wahrscheinlich nicht
angemeldet.
> Also all die arme Schweine, die höchstens zufällig
Und die nächste Ausfälligkeit ...
>> Und ausserdem geht es hier doch gar nicht um "C".>> Doch, irgendwie schon.
Schau bitte ganz oben in diesem Thread ...
> C ist halt nicht für MT designed
aber auch nicht dagegen, sondern agnostisch. Wohingegen die
Standard-C-Bibliothek eben mit atomics und der thread-support-library
genau diese Unterstützung bieten.
> Standardfehler in diesem Umfeld. Wie auch für viele andere> Standardfehler nicht, angefangen vom oberprimitivsten Integer-Overflow> bis hin zum Buffer-Overflow...
Und wenn das der Fall wäre, dann geht das Lamentieren um den
Laufzeit-Overhead wieder los.
> [...C-Standard...]>> den gibt es sehr wohl.>> Nein, es gibt eben nicht nur einen, sondern mehrere.
Welche denn noch ausser ISO/IEC 9899 in den Revisionen (C11, C99, C95,
C89/90)?
Wilhelm M. schrieb:> Dagegen ist eine sog. Critical Section ein Begriff aus der Informatik,> der einen Teil eines Ausführungspfades bezeichnet, der nicht-nebenläufig> ausgeführt werden muss.
Wieso, passt doch? Genau das tut Enter/ExitCriticalSection doch. Zumal
es nicht einfach bloss eine System-Mutex ist, sondern es erst einmal mit
einem Spinlock versucht.
A. K. schrieb:> Wilhelm M. schrieb:>> Dagegen ist eine sog. Critical Section ein Begriff aus der Informatik,>> der einen Teil eines Ausführungspfades bezeichnet, der nicht-nebenläufig>> ausgeführt werden muss.>> Wieso, passt doch?
Nein, oben ist Win32-CriticalSection ein konkreter Datentyp einer
bestimmten Bibliothek.
> Zumal> es nicht einfach bloss eine System-Mutex ist, sondern es erst einmal mit> einem Spinlock versucht.
Ein Mutex ist zunächst mal ein abstrakter Datentyp (ADT), der eine
bestimmte Funktion bereitstellt, mit dem man kritische Abschnitte
realisieren kann. Wie die konkrete Realisierung eines Mutex
(posix_mutex_t, futex, binäres Semaphor, ..., win32-CriticalSection) das
macht, ist erst mal irrelevant.
Ein kritischer Abschnitt hingegen ist wieder ein anderer Begriff, der
zunächst einmal nur festlegt, das eine Anweisungsfolge nicht nebenläufig
ausgeführt werden darf. Ob dazu ein Mutex oder irgendetwas anderes der
möglichen Synchronisationsprimitive benutzt wird, spielt keine Rolle.
Wilhelm M. schrieb:> Nein, oben ist Win32-CriticalSection ein konkreter Datentyp einer> bestimmten Bibliothek.
Fast. Der heisst CRITICAL_SECTION. Schon die Schreibweise weist ihn als
Datentyp aus, insofern besteht keine Verwechselungsgefahr. Dass die
Namen von Datentypen in Windows bisweilen einen Bezug zu dem haben, was
damit angestellt wird, finde ich nicht so schlimm.
Dazu gehören Funktionen des API, wie EnterCriticalSection und
ExitCriticalSection. Die heissen gradewegs so, wie das was sie tun.
> Wie die konkrete Realisierung eines Mutex> (posix_mutex_t, futex, binäres Semaphor, ..., win32-CriticalSection) das> macht, ist erst mal irrelevant.
Das gilt gleichermassen für einen kritischen Abschnitt. Also wie man den
realisiert.
> Ob dazu ein Mutex oder irgendetwas anderes der möglichen> Synchronisationsprimitive benutzt wird, spielt keine Rolle.
Eben. Weshalb CRITICAL_SECTION mindestens auf der abstrakten Ebene kein
Mutex-Handle ist, sondern ein Handle für die Verwaltung eines kritischen
Abschnitts.
A. K. schrieb:> Wilhelm M. schrieb:>> Nein, oben ist Win32-CriticalSection ein konkreter Datentyp einer>> bestimmten Bibliothek.>> Fast. Der heisst CRITICAL_SECTION. Schon die Schreibweise weist ihn als> Datentyp aus,
sag ich doch, wobei ich die Schreibweise von dem anderen, ungemein
freundlichen Post (c-hater) übernommen habe.
> insofern besteht keine Verwechselungsgefahr. Dass die> Namen von Datentypen in Windows bisweilen einen Bezug zu dem haben, was> damit angestellt wird, finde ich nicht so schlimm.
Ich finde es grauenhaft: weil es einfach unterschiedliche Dinge sind.
Das eine ein DT, das andere ein Code-Abschnitt.
> Dazu gehören Funktionen des API, wie EnterCriticalSection und> ExitCriticalSection. Die heissen gradewegs so, wie das was sie tun.
Das ist ja wieder ok.
Man könnte auch in bspw. C++ eine RAII-Style Locker so benennen: dieser
wiederum verwendet eine Mutex-Realisierung um einen kritischen Abschnitt
herzustellen. Aber der Mutex selbst ist eben kein kritischer Abschnitt!
>> Wie die konkrete Realisierung eines Mutex>> (posix_mutex_t, futex, binäres Semaphor, ..., win32-CriticalSection) das>> macht, ist erst mal irrelevant.>> Das gilt gleichermassen für einen kritischen Abschnitt. Also wie man den> realisiert.
Auch das habe ich ja gesagt: als Mutex, um einen kritischen Abschnitt
herzustellen, könnte man auch ein binäres Semaphor oder eine
MessageQueue verwenden.
>> Ob dazu ein Mutex oder irgendetwas anderes der möglichen>> Synchronisationsprimitive benutzt wird, spielt keine Rolle.
s.o.
> Eben. Weshalb CRITICAL_SECTION mindestens auf der abstrakten Ebene kein> Mutex-Handle ist, sondern ein Handle für die Verwaltung eines kritischen> Abschnitts.
Was soll das aussagen?
Wilhelm M. schrieb:>> Eben. Weshalb CRITICAL_SECTION mindestens auf der abstrakten Ebene kein>> Mutex-Handle ist, sondern ein Handle für die Verwaltung eines kritischen>> Abschnitts.>> Was soll das aussagen?
Das: CRITICAL_SECTION ist der Datentyp eines Handle für
Enter/ExitCriticalSection. Was dahinter steckt ist mir aus reiner
Programmiersicht schietegal.
A. K. schrieb:> Wilhelm M. schrieb:>>> Eben. Weshalb CRITICAL_SECTION mindestens auf der abstrakten Ebene kein>>> Mutex-Handle ist, sondern ein Handle für die Verwaltung eines kritischen>>> Abschnitts.>>>> Was soll das aussagen?>> Das: CRITICAL_SECTION ist der Datentyp eines Handle für> Enter/ExitCriticalSection. Was dahinter steckt ist mir aus reiner> Programmiersicht schietegal.
Na, jetzt drehen wir uns im Kreis: das hatte ich ganz oben schon gesagt
(allerdings als Antwort c-hater).