Italian community of Lazarus and Free Pascal

Programmazione => Generale => Topic aperto da: bonmario - Dicembre 07, 2023, 03:18:19 pm

Titolo: Sleep all'interno di un Thread
Inserito da: bonmario - Dicembre 07, 2023, 03:18:19 pm
Ciao a tutti,
devo lanciare un thread che, appena partito, deve aspettare 5 secondi prima di fare il suo lavoro.
Se però uso "Sleep(5000)" all'interno del thread, mi ritorna nel MainThread ...
Avete qualche suggerimento su come fare?

Grazie, Mario
Titolo: Re:Sleep all'interno di un Thread
Inserito da: DragoRosso - Dicembre 07, 2023, 03:33:51 pm
Lo sleep produce un cambio di contesto: viene rilasciato il thread corrente e viene eseguito il prossimo thread schedulato (che spesso è il main thread). Tutto ciò è gestito in autonomia dal sistema operativo.

Cosa vuoi esattamente fare e cosa ti aspetti ?

I thread lavorano in parallelo (ovviamente in modo apparente in quanto il numero di processori virtuali è comunque finito), vengono sospesi e riesumati a "piacere" del SO e non è dato avere una cognizione temporale del loro lavoro.

Dai una letta a questi due articoli: https://blog.lazaruspascal.it/2022/04/24/thread/ (https://blog.lazaruspascal.it/2022/04/24/thread/) e https://blog.lazaruspascal.it/2023/06/03/risoluzione-del-timing-sotto-windows/ (https://blog.lazaruspascal.it/2023/06/03/risoluzione-del-timing-sotto-windows/)

Ciao
Titolo: Re:Sleep all'interno di un Thread
Inserito da: bonmario - Dicembre 07, 2023, 04:35:44 pm
Allora, ho un progetto che ha più icone nel form principale.

Quando l'utente clicca su una di queste icone dovrei:
1 - aspettare 5 secondi per dar tempo all'utente di cliccare su un'altra delle icone nel form, far partire un'animazione che "dissolve il form" fino a farlo sparire
2 - mentre fa quello che c'è scritto nel punto 1, deve lanciare un eseguibile

All'inizio, avevo messo quello che c'è nel punto 1 in un Timer, ma il punto 2 veniva eseguito solo dopo la fine del punto 1.
Ho così pensato di mettere il punto 1 sotto thread, ma così ho 2 problemi: il primo è che non riesco a dire al thread di aspettare 5 secondi. Il secondo è che il "dissolve il form" lo faccio modificando il valore di "AlphaBlendValue" ogni 100 millisecondi, ed anche qui sarei fregato non potendo usare lo sleep.

Grazie, Mario
Titolo: Re:Sleep all'interno di un Thread
Inserito da: nomorelogic - Dicembre 07, 2023, 05:10:56 pm
Allora, ho un progetto che ha più icone nel form principale.

Quando l'utente clicca su una di queste icone dovrei:
1 - aspettare 5 secondi per dar tempo all'utente di cliccare su un'altra delle icone nel form, far partire un'animazione che "dissolve il form" fino a farlo sparire
2 - mentre fa quello che c'è scritto nel punto 1, deve lanciare un eseguibile

All'inizio, avevo messo quello che c'è nel punto 1 in un Timer, ma il punto 2 veniva eseguito solo dopo la fine del punto 1.
Ho così pensato di mettere il punto 1 sotto thread, ma così ho 2 problemi: il primo è che non riesco a dire al thread di aspettare 5 secondi. Il secondo è che il "dissolve il form" lo faccio modificando il valore di "AlphaBlendValue" ogni 100 millisecondi, ed anche qui sarei fregato non potendo usare lo sleep.

Grazie, Mario

forse potresti creare il nuovo thread con (come parametro) un timestamp avanti di 5 secondi rispetto al momento della creazione
poniamo che nel metodo create del thread metti un parametro: ExecuteAfterDatetime: TDatetime;
poi nel metodo execute qualcosa tipo
Codice: [Seleziona]
begin
   while now() < ExecuteAfterDatetime do
     sleep(250);

   // le istruzioni vere e proprie da eseguire


end;

penso che potrebbe andare



Titolo: Re:Sleep all'interno di un Thread
Inserito da: bonmario - Dicembre 07, 2023, 05:20:58 pm
Il problema è proprio l'istruzione "Sleep".

Supponiamo che il codice del thread sia questo:
Codice: [Seleziona]
procedure TMyThread.Execute;
const CstMinLivTrasp=1;
      CstMaxLivTrasp=255;
var Idx:Integer;
begin
  Idx:=1;
  Sleep(100);
  Idx:=2;
  Idx:=3;
end;

Dopo aver eseguito l'istruzione "Sleep(100);", non esegue l'istruzione "Idx:=2;", ma torna al thread principale.

Al momento ho fatto questo (che però non mi piace molto):
Codice: [Seleziona]
procedure TMyThread.MyDelay(WrkMsAtt: dword);
var OraIni, OraFin:TTime;
begin
  OraIni:=Now();
  repeat
    OraFin:=Now();
  until (MilliSecondsBetween(OraIni, OraFin) >= WrkMsAtt);
end;

Invece di chiamare Sleep all'interno del thread, chiamo "MyDelay".
Ora il programma fa quello che voglio, ma la soluzione che ho trovato non mi piace molto.
Anche perché, ora chiamo MyDelay con un massimo di 5 secondi di attesa, se mai lo dovessi usare con periodi più lunghi, probabilmente mi occuperebbe la CPU per niente !

Ciao, Mario
Titolo: Re:Sleep all'interno di un Thread
Inserito da: DragoRosso - Dicembre 07, 2023, 06:22:38 pm
Ritengo che il problema che hai sia legato alla ottimizzazione intrinseca che viene generata dal compilatore.

Essendo IDX locale e assumendo come ultimo valore 3 senza essere usato da alcuna funzione esterna, il compilatore pone probabilmente tale valore all'inizio ed esegue lo sleep, terminando immediatamente il thread.

E' una ipotesi.

Lo sleep deve comunque funzionare. Tieni presente che le assegnazioni interne vengono ottimizzate se non ci sono chiamate esterne a tale procedura. Ciò viene fatto anche nei normali metodi. L'unico modo per evitare ciò è eliminare completamente l'ottimizzazione (normalmente posta a O1) o usare variabili definite nella classe, esterne o globali.

Fai una prova così:

Codice: [Seleziona]
va IDX: Integer;  //VARIABILE GLOBALE NELLA SEZIONE INTERFACE

procedure TMyThread.Execute;
const CstMinLivTrasp=1;
      CstMaxLivTrasp=255;
begin
  Idx:=1;
  Sleep(5000);
  Idx:=2;
end;

Così dovrebbe funzionare.

Ciao
Titolo: Re:Sleep all'interno di un Thread
Inserito da: quack - Dicembre 07, 2023, 09:42:26 pm
Ho usato diverse volte sleep all'interno dei thread e non ricordo questo tipo di problema. Tra l'altro è anche un medodo interno alla classe quindi ancora più strano.
Se si escludono eventuali eccezioni che possono interrompere il thread rimane poco da ipotizzare.

Ho provato a simulare più o meno quello che vuoi fare e su linux mi sembra funzionare come aspettato.
Ciao
qk
Codice: [Seleziona]
unit Unit1;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Forms, Controls, Graphics, Dialogs, StdCtrls, Buttons;

type

  { TForm1 }
  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
    procedure DoTerminate1(Sender: TObject);
    procedure DoTerminate2(Sender: TObject);
  private
  public
  end;

  TMyThread1 = class(TThread)
    constructor Create;
    procedure Execute; override;
  end;

  TMyThread2 = class(TThread)
    constructor Create;
    procedure Execute; override;
  end;

var
  Form1: TForm1;
  MyThread1: TMyThread1 = nil;
  MyThread2: TMyThread2 = nil;

implementation

{$R *.lfm}

constructor TMyThread1.Create;
begin
  FreeOnTerminate := True;
  Inherited Create(True);
end;

procedure TMyThread1.Execute;
begin
  Form1.AlphaBlend := True;
  while Form1.AlphaBlendValue > 0 do
  begin
    Sleep(10);
    Form1.AlphaBlendValue := Form1.AlphaBlendValue -1;
    Form1.Refresh;
  end;
end;

constructor TMyTHread2.Create;
begin
  FreeOnTerminate := True;
  Inherited Create(True);
end;

procedure TMyThread2.Execute;
begin
  Sleep(5000);
  // eseguo altro codice ...
end;

{ TForm1 }

procedure TForm1.Button1Click(Sender: TObject);
begin
  if MyThread1 = nil then
  begin
    MyThread1 := TMyThread1.Create;
    MyThread1.OnTerminate := @DoTerminate1;
    MyThread1.Start;
  end;

  if MyThread2= nil then
  begin
    MyThread2 := TMyThread2.Create;
    MyThread2.OnTerminate := @DoTerminate2;
    MyThread2.Start;
  end;
end;

procedure TForm1.DoTerminate1(Sender: TObject);
begin
  ShowMessage('Terminate thread 1');
  Form1.AlphaBlendValue := 255;
  MyThread1 := nil;
end;

procedure TForm1.DoTerminate2(Sender: TObject);
begin
  ShowMessage('Terminate thread 2');
  MyThread2 := nil;
end;

end.
Titolo: Re:Sleep all'interno di un Thread
Inserito da: quack - Dicembre 07, 2023, 10:28:44 pm
Ho sistemato meglio l'esempio:
Codice: [Seleziona]
unit Unit1;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Forms, Controls, Graphics, Dialogs, StdCtrls, Buttons;

type

  { TForm1 }
  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
    procedure DoTerminate1(Sender: TObject);
    procedure DoTerminate2(Sender: TObject);
  private
  public
  end;

  TMyThread1 = class(TThread)
    constructor Create;
    procedure Execute; override;
  end;

  TMyThread2 = class(TThread)
    constructor Create;
    procedure Execute; override;
  end;


var
  Form1: TForm1;
  MyThread1: TMyThread1 = nil;
  MyThread2: TMyThread2 = nil;

implementation

{$R *.lfm}

constructor TMyThread1.Create;
begin
  FreeOnTerminate := True;
  Inherited Create(True);
end;

procedure TMyThread1.Execute;
begin
  Form1.Caption := IntToStr(5) + ' sec'; Sleep(1000);
  Form1.Caption := IntToStr(4) + ' sec'; Sleep(1000);
  Form1.Caption := IntToStr(3) + ' sec'; Sleep(1000);
  Form1.Caption := IntToStr(2) + ' sec'; Sleep(1000);
  Form1.Caption := IntToStr(1) + ' sec'; Sleep(1000);
  Form1.Caption := '';

  Form1.AlphaBlend := True;
  while Form1.AlphaBlendValue > 0 do
  begin
    Sleep(10);
    Form1.AlphaBlendValue := Form1.AlphaBlendValue -1;
    Form1.Refresh;
  end;
end;

constructor TMyTHread2.Create;
begin
  FreeOnTerminate := True;
  Inherited Create(True);
end;

procedure TMyThread2.Execute;
begin
  Sleep(1000);
end;

{ TForm1 }

procedure TForm1.Button1Click(Sender: TObject);
begin
  if MyThread1 = nil then
  begin
    MyThread1 := TMyThread1.Create;
    MyThread1.OnTerminate := @DoTerminate1;
    MyThread1.Start;
  end;

  if MyThread2 = nil then
  begin
    MyThread2 := TMyThread2.Create;
    MyThread2.OnTerminate := @DoTerminate2;
    MyThread2.Start;
  end;
end;

procedure TForm1.DoTerminate1(Sender: TObject);
begin
  ShowMessage('Fine dissolvenza');
  Form1.AlphaBlendValue := 255;
  Form1.Caption := 'Form1';
  MyThread1 := nil;
end;

procedure TForm1.DoTerminate2(Sender: TObject);
begin
  ShowMessage('Lancio l''eseguibile');
  MyThread2 := nil;
end;

end.
PS: Modificare una form direttamente dal thread non è il massimo, si dovrebbe usare syncronize o queue ...
Titolo: Re:Sleep all'interno di un Thread
Inserito da: DragoRosso - Dicembre 07, 2023, 10:32:15 pm
Ho usato diverse volte sleep all'interno dei thread .......
...
qk
Codice: [Seleziona]
unit Unit1;
procedure TMyThread1.Execute;
begin
  Form1.AlphaBlend := True;
  while Form1.AlphaBlendValue > 0 do
  begin
    Sleep(10);
    Form1.AlphaBlendValue := Form1.AlphaBlendValue -1;
    Form1.Refresh;
  end;
end;

{ TForm1 }


Non si può e non si deve usare il codice LCL in un thread, le LCL non sono THREAD SAFE. Che funzioni non ha valore ... funziona mille volte e la mille e una non funzionerà.
Tutto il codice che deve essere usato con le LCL (cioè i componenti grafici) DEVE venire usato nel MAIN THREAD. Alcune eccezioni possono essere accettate con l'appropriato codice, ma non è questo il caso.

L'ho ripetuto più volte, mi raccomando di rispettare ciò. E' la causa di una buona parte di problematiche.

Ciao

P.S.:
PS: Modificare una form direttamente dal thread non è il massimo, si dovrebbe usare syncronize o queue ...
Questo è perfetto. Di fatto il Synchronize o il Queue (o meglio il codice riferito) vengono eseguiti dentro il Main Thread. Fate attenzione con il Queue, se passate valori relativi al Thread potrebbero esserci delle sorprese se questi valori cambiano prima che vengano processati nel Main Thread.
Titolo: Re:Sleep all'interno di un Thread
Inserito da: quack - Dicembre 07, 2023, 11:02:26 pm
Sono caduto in tentazione !  :-[ 
Accedere alle proprietà della form direttamente dal thread è maledettamente comodo ma SBAGLIATO.
Diciamo che ne ero conscio e l'ho dato per scontato.
Domani sistemo meglio il post nell'eventualità di futuri lettori.
Ciao
qk

EDIT: versione aggiornata che usa synchronize per aggiornare le proprietà della form.
Riassumendo:
- primo thread gestisce il countdown e la dissolvenza della form;
- secondo thread lancia eseguibile un secondo dopo il suo avvio.
Saluti
qk
Codice: [Seleziona]
unit Unit1;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Forms, Controls, Graphics, Dialogs, StdCtrls, Buttons;

type

  { TForm1 }
  TForm1 = class(TForm)
    Button1: TButton;
    Label1: TLabel;
    procedure Button1Click(Sender: TObject);
    procedure DoTerminate1(Sender: TObject);
    procedure DoTerminate2(Sender: TObject);
    procedure DoUpdateForm;
  private
  public
  end;

  TMyThread1 = class(TThread)
    NewCaption: string;
    NewAlphaBlendValue: longint;
    OnUpdateForm: TThreadMethod;
    procedure DoUpdateForm;
    constructor Create;
    procedure Execute; override;
  end;

  TMyThread2 = class(TThread)
    constructor Create;
    procedure Execute; override;
  end;

var
  Form1: TForm1;
  MyThread1: TMyThread1 = nil;
  MyThread2: TMyThread2 = nil;

implementation

{$R *.lfm}

constructor TMyThread1.Create;
begin
  FreeOnTerminate := True;
  Inherited Create(True);
end;

procedure TMyThread1.DoUpdateForm;
begin
  if Assigned(OnUpdateForm) then
    Synchronize(OnUpdateForm);
end;

procedure TMyThread1.Execute;
var
  i: longint;
begin
  NewAlphaBlendValue := 255;

  for i := 5 downto 1 do
  begin
    NewCaption := i.ToString + ' sec';       
    DoUpdateForm;
    Sleep(1000);
  end; 
  NewCaption := 'Form1';
  DoUpdateForm;

  while NewAlphaBlendValue > 0 do
  begin
    Dec(NewAlphaBlendValue);
    DoUpdateForm;
    Sleep(10);
  end;
end;

constructor TMyThread2.Create;
begin
  FreeOnTerminate := True;
  Inherited Create(True);
end;

procedure TMyThread2.Execute;
begin
  Sleep(1000);
end;

{ TForm1 }

procedure TForm1.Button1Click(Sender: TObject);
begin
  AlphaBlend := True;
  if (MyThread1 = nil) and
     (MyThread2 = nil) then
  begin
    MyThread1 := TMyThread1.Create;
    MyThread1.OnTerminate  := @DoTerminate1;
    MyThread1.OnUpdateForm := @DoUpdateForm;
    MyThread1.Start;

    MyThread2 := TMyThread2.Create;
    MyThread2.OnTerminate := @DoTerminate2;
    MyThread2.Start;
  end;
end;

procedure TForm1.DoUpdateForm;
begin
  Caption := MyThread1.NewCaption;
  AlphaBlendValue := MyThread1.NewAlphaBlendValue;
end;

procedure TForm1.DoTerminate1(Sender: TObject);
begin
  AlphaBlendValue := 255;
  Label1.Caption :='Dissolvenza terminata';
  MyThread1 := nil;
end;

procedure TForm1.DoTerminate2(Sender: TObject);
begin
  Label1.Caption := 'Eseguibile lanciato';
  MyThread2 := nil;
end;

end.
Titolo: Re:Sleep all'interno di un Thread
Inserito da: DragoRosso - Dicembre 07, 2023, 11:22:01 pm
Sono caduto in tentazione !  :-[ 
Accedere alle proprietà della form direttamente dal thread è maledettamente comodo ma SBAGLIATO.

Ah ah ah, ti spio un peccato ... ogni tanto cedo anche io a quella tentazione ....  :o ;D

Ciaooo
Titolo: Re:Sleep all'interno di un Thread
Inserito da: Stilgar - Dicembre 11, 2023, 04:18:12 pm
Ciao Bonmario.
Ma che sleep utilizzi?


https://www.freepascal.org/docs-html/rtl/classes/tthread.sleep.html


https://www.freepascal.org/docs-html/rtl/sysutils/sleep.html


Dentro il thread potrebbe esserci un problema di scope di chiamata?


Stilgar
Titolo: Re:Sleep all'interno di un Thread
Inserito da: bonmario - Dicembre 11, 2023, 05:10:54 pm
Naturalmente il secondo  :'( :'(

Fino a 10 minuti fa non sapevo che esistesse "TThread.Sleep"  :-[

Ho fatto una prova veloce, ed ora sembra funzionare bene.
Quello che però non capisco, è che se non ho visto male, TThread.Sleep chiama comunque lo Sleep "normale" che io chiamavo direttamente, e che mi dava il problema !!!

Grazie, Mario
Titolo: Re:Sleep all'interno di un Thread
Inserito da: Stilgar - Dicembre 11, 2023, 05:12:30 pm
Posso ridere? ;D ;D ;D ;D ;D ;D


Stilgar
Titolo: Re:Sleep all'interno di un Thread
Inserito da: bonmario - Dicembre 11, 2023, 05:40:57 pm
Certo !!!

vedo però che scrivono questo:
Codice: [Seleziona]
In difference with TThread.SpinWait, a thread switch may occur during the sleep. 

Quindi può anche essere un caso che nel test che ho fatto prima funzionasse correttamente.
Domani faccio qualche prova in più, e verifico anche il funzionamento di TThread.SpinWait, che potrebbe essere quello che fa al caso mio.

Grazie ancora, Mario
Titolo: Re:Sleep all'interno di un Thread
Inserito da: DragoRosso - Dicembre 11, 2023, 08:36:20 pm
Certo !!!

vedo però che scrivono questo:
Codice: [Seleziona]
In difference with TThread.SpinWait, a thread switch may occur during the sleep. 

Quindi può anche essere un caso che nel test che ho fatto prima funzionasse correttamente.
Domani faccio qualche prova in più, e verifico anche il funzionamento di TThread.SpinWait, che potrebbe essere quello che fa al caso mio.

Grazie ancora, Mario

Il funzionamento normale dello sleep è la commutazione di contesto come già citato. Ci sono poi ulteriori modalità (che in Windows possono o meno venire attivate in autonomia dal SO in base allo condizioni di carico) come avete citato lo SpinWait e lo Yeld.

In ogni caso, queste chiamate sono riportate poi al sistema operativo che le usa in modalità mista normalmente. Se lo spin (di fatto un loop di qualche decina di nanosecondi) dura troppo a lungo o ci sono troppi cicli di "spin" viene comunque poi eseguito un cambio di contesto.

Cosa è meglio fare dipende da situazione a situazione. Lo SpinWait normalmente carica maggiormente la CPU ed è consigliata solo per cicli molto brevi e non frequenti.

Io personalmente uso sempre lo sleep classico e lascio che il SO faccia il suo dovere, e dove ho bisogno di maggior "precisione" uso le tecniche indicate nel blog.
Questo per quanto riguarda Windows, in altri SO non ho sufficiente esperienza.