Threadprogrammierung

Für Fragen zur Programmiersprache auf welcher Lazarus aufbaut
schnullerbacke
Beiträge: 1187
Registriert: Mi 13. Dez 2006, 10:58
OS, Lazarus, FPC: Winux (L 1.2.xy FPC 2.6.z)
CPU-Target: AMD A4-6400 APU
Wohnort: Hamburg

Beitrag von schnullerbacke »

Auch wenn ihr mich jetzt erschlagt wegen der Indy's, hier ein Thread-Beispiel für Euklid. Das kannst Du dir ja etwas umbauen und erweitern:

Code: Alles auswählen

unit TCPListenerThread;
 
interface
 
uses
  Classes, IdTCPConnection;
 
type
 
  TTCPListenerThread = class(TThread)
  private
    { Private-Deklarationen }
    FConnection   : TIdTCPConnection;
    FOnDataPresent: TNotifyEvent;
    FReadDataSize : integer;
 
    function  GetConnection: TIdTCPConnection;
    procedure SetConnection(Value: TIdTCPConnection);
    procedure LookForData;
    procedure DoDataPresent;
  protected
    procedure Execute; override;
  public
    constructor Create(ASuspended: boolean);
 
    property Connection: TIdTCPConnection read FConnection write FConnection;
    property OnDataPresent: TNotifyEvent
      read FOnDataPresent write FOnDataPresent;
    property ReadDataSize: integer read FReadDataSize write FReadDataSize;
  end;
 
implementation
 
{ Wichtig: Methoden und Eigenschaften von Objekten in visuellen Komponenten dürfen
  nur in einer Methode namens Synchronize aufgerufen werden, z.B.
 
      Synchronize(UpdateCaption);
 
  und UpdateCaption könnte folgendermaßen aussehen:
 
    procedure TTCPListenerThread.UpdateCaption;
    begin
      Form1.Caption := 'Aktualisiert in einem Thread';
    end; }
 
{ TTCPListenerThread }
 
{-----------------------------------------------------------------------------
  Class:     TTCPListenerThread
  Methode:   Create
  Author:    hardy
  Date:      07-Jun-2006
  Arguments: ASuspended: boolean
-----------------------------------------------------------------------------}
constructor TTCPListenerThread.Create(ASuspended: boolean);
const
  cProcName = 'TCPListenerThread.TTCPListenerThread.Create';
 
  function SetErrorParams: string;
  begin
    Result:= '';
    // 'ASuspended: boolean'
  end; // of function SetErrorParams: string
 
begin
  { procedure body }
  inherited Create(ASuspended);
  {$IFDEF WIN32}
    Self.Priority:= tpIdle;
  {$ENDIF}
  {$IFDEF LINUX}
    Self.Priority:= 3;
  {$ENDIF}
end; // of TTCPListenerThread.Create
 
{-----------------------------------------------------------------------------
  Class:     TTCPListenerThread
  Method:    GetConnection
  Author:    hardy
  Date:      06-Jun-2006
  Arguments: None
  Result:    TIdTCPConnection
-----------------------------------------------------------------------------}
function TTCPListenerThread.GetConnection: TIdTCPConnection;
const
  cProcName = 'TCPListenerThread.TTCPListenerThread.GetConnection';
 
  function SetErrorParams: string;
  begin
    Result:= '';
    // 'None'
    // 'TIdTCPConnection';
  end; // of function SetErrorParams: string
 
begin
  { function body }
  try
    Result:= FConnection;
  except
    raise;
  end;    
end; // of TTCPListenerThread.GetConnection
 
{-----------------------------------------------------------------------------
  Class:     TTCPListenerThread
  Methode:   SetConnection
  Author:    hardy
  Date:      06-Jun-2006
  Arguments: Value: TIdTCPConnection
-----------------------------------------------------------------------------}
procedure TTCPListenerThread.SetConnection(Value: TIdTCPConnection);
const
  cProcName = 'TCPListenerThread.TTCPListenerThread.SetConnection';
 
  function SetErrorParams: string;
  begin
    Result:= '';
    // 'Value: TIdTCPConnection'
  end; // of function SetErrorParams: string
 
begin
  { procedure body }
  try
    if (Value <> FConnection) and (Value <> nil) then begin
      FConnection:= Value;
    end; // of if (Value <> FConnection) then begin
  except
    raise;
  end;    
end; // of TTCPListenerThread.SetConnection
 
{-----------------------------------------------------------------------------
  Class:     TTCPListenerThread
  Method:    LookForData
  Author:    hardy
  Date:      06-Jun-2006
  Arguments: None
-----------------------------------------------------------------------------}
procedure TTCPListenerThread.LookForData;
const
  cProcName = 'TCPListenerThread.TTCPListenerThread.LookForData';
 
  function SetErrorParams: string;
  begin
    Result:= '';
    // 'var ADataSize: integer'
    // 'boolean';
  end; // of function SetErrorParams: string
 
begin
  { function body }
  if (FConnection.IOHandler <> nil) then begin
    {-------------------------------------------------------------------------
      Author:    hardy
      Date:      13-Jun-2006
      Subject:   Wichtig
 
      Comment:   An dieser Stelle auf keinen Fall etwas ändern.
                 Der except-Block muß leer bleiben, da sonst
                 ein Disconnect des Client durchgeführt wird. Dies
                 tritt immer dann ein, wenn der Read-Stack des Sockets
                 leer, der Server also vorübergehend keine Daten sendet
    -------------------------------------------------------------------------}
    try
      FConnection.InputBuffer.PackBuffer;
      FReadDataSize:= FConnection.ReadFromStack(false, -1, false);
    except
    end;
  end; // of if (Con.IOHandler <> nil) then begin
end; // of TTCPListenerThread.LookForData
 
{-----------------------------------------------------------------------------
  Class:     TTCPListenerThread
  Methode:   DoDataPresent
  Author:    hardy
  Date:      06-Jun-2006
  Arguments: None
-----------------------------------------------------------------------------}
procedure TTCPListenerThread.DoDataPresent;
const
  cProcName = 'TCPListenerThread.TTCPListenerThread.DoDataPresent';
 
  function SetErrorParams: string;
  begin
    Result:= '';
    // 'None'
  end; // of function SetErrorParams: string
 
begin
  { procedure body }
   if Assigned(FOnDataPresent) then begin
     Self.Suspended:= true;
     OnDataPresent(Self);
     Self.Suspended:= false;
   end;
end; // of TTCPListenerThread.DoDataPresent
 
{-----------------------------------------------------------------------------
  Class:     TTCPListenerThread
  Methode:   Execute
  Author:    hardy
  Date:      06-Jun-2006
  Arguments: None
-----------------------------------------------------------------------------}
procedure TTCPListenerThread.Execute;
const
  cProcName = 'TCPListenerThread.TTCPListenerThread.Execute';
 
  function SetErrorParams: string;
  begin
    Result:= '';
    // 'None'
  end; // of function SetErrorParams: string
 
begin
  { procedure body }
  repeat
    if not(Suspended) and not(Terminated) then begin
      Self.Synchronize(LookForData);
      if (FReadDataSize > 0) then Self.Synchronize(DoDataPresent);
    end; // of if FConnection.Connected then begin
  until Self.Terminated;
end; // of TTCPListenerThread.Execute
 
end.
Der horcht solange am TCPClient bis Daten kommen und löst dann einen Event aus. Das macht er solange bis man ihn explizit beendet oder das Programm beendet.
Humor ist der Knopf, der verhindert, daß uns der Kragen platzt.

(Ringelnatz)

Euklid
Lazarusforum e. V.
Beiträge: 2808
Registriert: Fr 22. Sep 2006, 10:38
OS, Lazarus, FPC: Lazarus v2.0.10, FPC 3.2.0
Wohnort: Hessen
Kontaktdaten:

Beitrag von Euklid »

Hallo! Danke für den ganzen Code! Werde das mal ausprobieren...

schnullerbacke
Beiträge: 1187
Registriert: Mi 13. Dez 2006, 10:58
OS, Lazarus, FPC: Winux (L 1.2.xy FPC 2.6.z)
CPU-Target: AMD A4-6400 APU
Wohnort: Hamburg

Beitrag von schnullerbacke »

Noch ne kleine Anmerkung,

die lokale function:

Code: Alles auswählen

function SetErrorParams: string;
  begin
    Result:= '';
    // 'None'
  end; // of function SetErrorParams: string
ist für eine Errorreporting-System vorgesehen. Das soll wenns fertsch ist online die Fehlermeldungen an den Support senden und entsprechende Params mitliefern. Das sollte Dich nicht kratzen.

Das Dings stammt aus Delphi, da könnte lazarus an der einen oder anderen Stelle mosern, z.B. bei:

Code: Alles auswählen

constructor TTCPListenerThread.Create(ASuspended: boolean);
const
  cProcName = 'TCPListenerThread.TTCPListenerThread.Create';
 
  function SetErrorParams: string;
  begin
    Result:= '';
    // 'ASuspended: boolean'
  end; // of function SetErrorParams: string
 
begin
  { procedure body }
  inherited Create(ASuspended);
  {$IFDEF WIN32}
    Self.Priority:= tpIdle;
  {$ENDIF}
  {$IFDEF LINUX}
    Self.Priority:= 3;
  {$ENDIF}
end; // of TTCPListenerThread.Create
Das wirst Du anpassen müssen. Der Event OnDataPresent könnte z.B. in einem anderen Thread liegen und wird per Synchronize aufgerufen, womit das problemlos gehen sollte.

Aber vorsicht bei Windoofs, das verträgt nicht mehr als 16 Threads im eigenen Programm. Dann wirds entweder langsam oder kackt gnadenlos ab. Bei LINUX liegt die Beschränkung mehr im Speicherausbau, da laufen ohnehin 100te von Threads schon vom System aus.
Humor ist der Knopf, der verhindert, daß uns der Kragen platzt.

(Ringelnatz)

Euklid
Lazarusforum e. V.
Beiträge: 2808
Registriert: Fr 22. Sep 2006, 10:38
OS, Lazarus, FPC: Lazarus v2.0.10, FPC 3.2.0
Wohnort: Hessen
Kontaktdaten:

Beitrag von Euklid »

Jap, verwende Linux, und nicht mehr als 4 Threads... ... wiso liegt da die Beschränkung im Speicherausbau?

Werde hier dann meckern, wenn was nicht klappt, was du vorgeschlagen hast :P

schnullerbacke
Beiträge: 1187
Registriert: Mi 13. Dez 2006, 10:58
OS, Lazarus, FPC: Winux (L 1.2.xy FPC 2.6.z)
CPU-Target: AMD A4-6400 APU
Wohnort: Hamburg

Beitrag von schnullerbacke »

Das Dings wird im Heap abgelegt und speichert seine Zustände auf dem Stack. Erst wenn eins von beiden aufgebraucht ist wirds eng. Aber LINUX verdaut schon ein paar von den Dingern. Allzuviele von den Dingern sollte man eh nie verwenden, beim Fehlersuchen sind die problematisch.
Humor ist der Knopf, der verhindert, daß uns der Kragen platzt.

(Ringelnatz)

paradox
Beiträge: 34
Registriert: Fr 15. Sep 2006, 14:33

Beitrag von paradox »

Hallo Michael,

zu meinen schlechten Erfahrungen. Am besten ein kleines Beispiel, ich habe zwei Threads und moechte als Applikation beide ueber eine Aenderung Informieren.
Haette ich dafuer eine Pipe wuerde nur einer dieser beiden Threads diese Info bekommen. Es koennte auch sein, dass es mit Pipes auch dafuer eine Loesung gibt nur habe ich sie noch nicht gefunden :D.


Wartende Threads:

Windows liefert hier von Haus aus ziemlich coole Sachen mit. Die rede ist von WaitForSingleObject bzw. WaitForMultipleObjects. Sehr angenehme Sache.
Unter Linux gibt es leider nut mutexe die zum Beispiel bei TCriticalSection verwendete werden.
Zurzeit versuche ich eine Library zu entwickeln die die Windows Features auch unter Linux anbieten, leider ist dies noch etwas bugy.
...Aber vorsicht bei Windoofs, das verträgt nicht mehr als 16 Threads im eigenen Programm...
den Text hatte ich mal fuer Windows 98 gelesen.

Bei LINUX liegt die Beschränkung mehr im Speicherausbau, da laufen ohnehin 100te von Threads schon vom System aus
Mein Windows hat direkt nach einem neustart 605 Threads.
Davon Apache mit 252 system mit 68 scvhost 74 und halt der rest.

paradox
Beiträge: 34
Registriert: Fr 15. Sep 2006, 14:33

Beitrag von paradox »

Hallo schnullerbacke


Die Methode Synchronize führt innerhalb des Haupt-Threads einen Methodenaufruf aus.

Ein Ausschnitt aus der Delphi-Hilfe. Stellt sich mich doch sofort die Frage. Wozu einen Thread schreiben der auf dem Applikations-Thread Prozeduren ausfuehrt ?

Einzige Anwendungspunkt der mir einfaellt, non Threadsafe Code bekanntlich VCL/LCL. Doch warum sollte ein Thread auf der Oberflaeche rum machen ?

schnullerbacke
Beiträge: 1187
Registriert: Mi 13. Dez 2006, 10:58
OS, Lazarus, FPC: Winux (L 1.2.xy FPC 2.6.z)
CPU-Target: AMD A4-6400 APU
Wohnort: Hamburg

Beitrag von schnullerbacke »

@paradox

Das ist eine grundsätzlich falsche Betrachtung. Jede Applikation hat einen Main-Thread. Wenn Du in diesen Main-Thread, wie auch in jeden anderen Thread, einen Aufruf machen mußt geht das nur mit Synchronize. Dabei spielt es grundsätzlich keine Rolle ob der aufgerufene Thread eine Ausgabe macht oder einfach nur Daten liest. Versuch das ohne Synchronize und Du wirst erleben, das deine Ausgabe auf dem Desktop erst nach einer erheblichen Wartezeit kommt. Synchronize macht also nicht anderes als:

Self.Suspended:= true;
// hier den anderen Thread aufrufen
Self.Suspended:= false; // setz dich selber fort

Das geht in dieser Form auch nicht mit TCriticalSektion, das hab ich probiert. TCriticalSektion wird auf dem Stack abgelegt, sobald ein anderer Thread seinerseits seine Zustände auf dem Stack ablegt (nichts anderes erzwingt TCriticalSektion), verliert der Aufrufer seinen Rückkehrpunkt und das läuft furchtbar durcheinander. TCriticalSektion eignet sich daher nur in der Synchronisierten Prozedur(Funktion) um bis zum Abarbeiten der internen Routinen den Thread zu bremsen. Sonst ruft der Thread denselben Codeabschnitt erneut auf und die CriticalSektion ist nicht beendet. Einfacher ausgedrückt, Dein Stack wird mit Rückkehrpunkten vollgeblasen bis nichts mehr geht.

Und warum ein Thread auf der Oberfläche rummachen sollte? Nun, das Beispiel sorgt dafür, das Daten aus einem Stream gelesen werden. Es gibt nur wenige Daten in einer Anwendung, die nicht auch eine Ausgabe nach sich ziehen. Schon deshalb muß der Main-Thread in solchen Fällen synchronisiert werden. Gleichfalls sorgt der sync dafür, das die Daten auch tatsächlich abgeholt werden. Sieh Dir dazu mal die Prozedur LookForData an, die packt den Empfangsstream, löscht also alle gelesenen Daten. Laß das weg und das Programm stürzt gnadenlos ab weil es ständig die gleichen Daten liest.

Wenn Du dir mal die Zeitangaben ansiehst(Gott gebe, das sowas in lazarus auch geht wie in GExperts), dann stellst du fest, das ich an dem Teil etwas länger gesessen habe. Nicht weil ich blöd bin, sondern weil das ganze Threadmodell eigentlich ein Blinddarm ist. Das kann man mit Prozessen und Semaphoren auch lösen. Nur Semaphore sind nicht jedermanns Sache. MircoSchrotts Sache waren sie jedenfalls nicht. Bei LINÜX läuft eigentlich fast alles über Prozesse, das Threadmodell haben die mehr oder weniger erzwungenermaßen eingebaut. Ganz frei von den Windows-Vorgaben sind sie halt auch nicht.

Prozesse haben grundsätzlich nur 3 Zustände, wartet, läuft, wird beendet. Das Problem ist das Warten. Der Thread ist genau betrachtet nichts anderes als eine Endlosschleife, die vom BS gelegentlich mal abgefragt wird. Ein Prozess kann die vollständige Kontrolle übernehmen. Das macht den Prozess gefährlich, der legt dann eben auch den Main-Prozess schlafen. Wartet nun der Kindprozess auf einen Semaphore vom Mainprozess und der Mainprozess auf den Semaphore vom Kindprozess hast Du einen sauberen Deadlock. Beim Thread läuft alles weiter bis Dein Stack vollgepustet ist. Die Wirkung ist am Ende kaum anders, Rechner runterfahren und gut. Das ganze Threadmodell ist kaum etwas anderes als eine Vereinfachung des Prozessmodells, eben mit erheblichen Nachteilen.
Humor ist der Knopf, der verhindert, daß uns der Kragen platzt.

(Ringelnatz)

paradox
Beiträge: 34
Registriert: Fr 15. Sep 2006, 14:33

Beitrag von paradox »

Ok,

wuerde dann aber Bedeuten das wenn ich 200 TTCPListenerThread habe, immer nur einer LookForData machen kann.

schnullerbacke
Beiträge: 1187
Registriert: Mi 13. Dez 2006, 10:58
OS, Lazarus, FPC: Winux (L 1.2.xy FPC 2.6.z)
CPU-Target: AMD A4-6400 APU
Wohnort: Hamburg

Beitrag von schnullerbacke »

Eben wieder falsch, sie dir mal das an was direkt danach folgt. Die Execute ist der Angelpunkt, hat er FDataSize > 0 gefunden, dann wird der Event aufgerufen und davon ausgegangen, das der Callback auch die Daten abruft. Solange wird er Thread schlafen gelegt, das BS kann ihn nicht bedienen weil er einfach nicht da ist. Erst wenn der ´Callback zurückerht wird er Thread wieder "scharf gemacht":

Self.Suspenden:= false;

Erst jetzt darf das BS seine Ausführung fortsetzen, es höhrt immer nur einer an der dem spezifizierten Socket. Aber er lockt es nicht für andere. das Beispiel eignet sich nun aber schlecht, wir müssten hier eigentlich eine Liste von Sockets anführen und dann gegentesten welcher Socket eine Antwort bekommt. Wichtig ist aber nur das Prinzip, welcher Socket ein Antwort (also Daten) hat, der löst das Event aus und die Daten werden abgerufen.

Thats it.
Humor ist der Knopf, der verhindert, daß uns der Kragen platzt.

(Ringelnatz)

schnullerbacke
Beiträge: 1187
Registriert: Mi 13. Dez 2006, 10:58
OS, Lazarus, FPC: Winux (L 1.2.xy FPC 2.6.z)
CPU-Target: AMD A4-6400 APU
Wohnort: Hamburg

Beitrag von schnullerbacke »

Einfacher, genau ein TTCPListenerThread bedient 200 Sockets. So wir da ein Schuh draus.
Humor ist der Knopf, der verhindert, daß uns der Kragen platzt.

(Ringelnatz)

mschnell
Beiträge: 3444
Registriert: Mo 11. Sep 2006, 10:24
OS, Lazarus, FPC: svn (Window32, Linux x64, Linux ARM (QNAP) (cross+nativ)
CPU-Target: X32 / X64 / ARMv5
Wohnort: Krefeld

Beitrag von mschnell »

schnullerbacke hat geschrieben:Einfacher, genau ein TTCPListenerThread bedient 200 Sockets. So wir da ein Schuh draus.
Wo hast Di das her ? Das glaube ich nicht. Soll das für Delphi und/oder FP gelten ? Soll das für Windows und/oder Linux gelten ? Welche TCP/IP-Library (indy ind/oder was immer) ?

Sockets sind i.A. blocking. Man braucht pro Socket einen Thread, der auf ankommende Daten wartet.

-Michael

mschnell
Beiträge: 3444
Registriert: Mo 11. Sep 2006, 10:24
OS, Lazarus, FPC: svn (Window32, Linux x64, Linux ARM (QNAP) (cross+nativ)
CPU-Target: X32 / X64 / ARMv5
Wohnort: Krefeld

Beitrag von mschnell »

paradox hat geschrieben: zu meinen schlechten Erfahrungen. Am besten ein kleines Beispiel, ich habe zwei Threads und moechte als Applikation beide ueber eine Aenderung Informieren.
Haette ich dafuer eine Pipe wuerde nur einer dieser beiden Threads diese Info bekommen. Es koennte auch sein, dass es mit Pipes auch dafuer eine Loesung gibt nur habe ich sie noch nicht gefunden :D.
Zwei Pipes. eine zu jedem Thread
paradox hat geschrieben: Wartende Threads:
Windows liefert hier von Haus aus ziemlich coole Sachen mit. Die rede ist von WaitForSingleObject bzw. WaitForMultipleObjects. Sehr angenehme Sache.
Unter Linux gibt es leider nut mutexe die zum Beispiel bei TCriticalSection verwendete werden.
Was hast Du gegen Mutexe ?
paradox hat geschrieben: Zurzeit versuche ich eine Library zu entwickeln die die Windows Features auch unter Linux anbieten, leider ist dies noch etwas bugy.
Lass ich wissen, wie es weiter geht. Ich will mich im Januar mit diesem Thema beschäftigen.
WaitForSingleObject sollte in Linux mit Semaphoren zu machen sein (semop API). Ich habe da vor ein paar Jahren 'mal etwas mit gebastelt. Hat auch funktioniert. Heute sollte man wahrscheinlich besser FUTEX nehmen.
paradox hat geschrieben: bzw. WaitForMultipleObjects.
Das geht in Linux u.U. nur mit select().

-Michael

schnullerbacke
Beiträge: 1187
Registriert: Mi 13. Dez 2006, 10:58
OS, Lazarus, FPC: Winux (L 1.2.xy FPC 2.6.z)
CPU-Target: AMD A4-6400 APU
Wohnort: Hamburg

Beitrag von schnullerbacke »

Nö, Beim Indy-TCPClient kannste mehrere haben. Muß nur ein anderer Port sein. Speicher einfach alle geöffneten in ner Liste mit dem Recall und dann änder den Listener einfach in dem du da die Liste verwaltetest.

Der Socket speichert Daten auf seinem eigenen Stack, von da werden die eingelesen. Das funzt. Obwohl das Wort Stack dafür schlecht gewählt ist, müßte eher Queue heißen, aber das sieht man wohl in Russland anders. :wink:

Guck dir mal den Hinweis genau an, wenn du in dem except-Block was machst, dann kommt der eigenwillige Indy-Error ClosedGracefully, das ist grauenhafter Indy-Blödsinn. Soll einem sagen der Server sendet nix. Ist der except-Block nicht leer, dann ruft er seine interne Exception und closed den Socket, so wie in diesem Listener macht er das nicht.

Da die Indys 10.x.x inzwischen auch auf FP zu haben sind, gilt das für beide. Bei den Indys kannst du das blocken unterbinden. Da darf nur nicht ReadUntilClosed gemacht werden.

Hab ich ne Weile dran gestrickt, weil ich ewig auf den Indy-Käse reingefallen bin bis ich das mit dem Debugger bis in die tiefsten Tiefen verfolgt hab. Den Hinweis auf den Quatsch gibt es bei Indy auch, nur wird aus dem Kauderwelsch keiner schlau.

Gibt auch noch nen anderen Hinweis, der TCPServer ist auch nix anderes als ein Socket und wird auch nicht geschlossen. Urvater ist bei beiden TIdTCPConnection.

mschnell
Beiträge: 3444
Registriert: Mo 11. Sep 2006, 10:24
OS, Lazarus, FPC: svn (Window32, Linux x64, Linux ARM (QNAP) (cross+nativ)
CPU-Target: X32 / X64 / ARMv5
Wohnort: Krefeld

Beitrag von mschnell »

Es scheint in Free Pascal doch eine OS-unabhängige Implementierung von inter-Thread- (und/oder inter-Prozess-) Messages zu geben:

simpleipc

Ich hab simpleipc noch nicht getestet.
-Michael

Antworten