Autor Téma: Start náročné procedury ve vláknu + okamžité zrušení v případě potřeby  (Přečteno 1486 krát)

Offline age.new

  • Hrdina
  • ****
  • Příspěvků: 309
  • Karma: 0
Vážená skupino,

mohl bych Vás poprosit o jednoduchý příklad, jak správně zavolat náročnou proceduru ve vláknu a v případě potřeby ji okamžitě zrušit? Zní to jednoduše, ale tápu, zda a jak vůbec použít WaitForSingleObject. Jde mi o "správné" zrušení vlákna, kdy padne do terminated stavu aniž bych musel čekat na doběh eventu.

Příklad pro jednoduché pochopení:

procedure UlozDataNaHDD; <- tato procedure začne ukládat velké množství dat (řádově i stovky MB) do souboru na HDD

Zavolání UlozDataNaHDD je časově náročné a může trvat i několik vteřin. Zavolání ale může přijít i několikrát za sekundu (záleží na uživateli).

Na každé zavolání bych potřeboval vytvořit vlákno, které si bokem spustí proceduru UlozDataNaHDD. V případě, že přijde požadavek v momentě, že vlákno stále běží, je nutné vlákno okamžitě přerušit. Je jedno co se uložilo a neuložilo, protože ty data jsou již neplatná. V momentě, kdy dojde k přerušení vlákna se obratem vytvoří nové vlákno s UlozDataNaHDD a celý proces se opakuje od začátku.

PS: důležité je to "okamžité přerušení vláka" a jeho opětovné vytvoření, aby se data začali ukládat odznovu. Tj. ty původní soubory, se hned před ukládáním vymažou, protože jsou již neplatná.

Děkuji za pomoc.

 
« Poslední změna: 15-09-2021, 14:28:50 od age.new »

Offline Jan Fiala

  • Plnoletý
  • ***
  • Příspěvků: 237
  • Karma: 3
    • Verze Delphi: 10.4.1
    • PSPad editor
A neměl bys počkat ve druhém vláknu, až to první doběhne?
Ty budeš dělat tohle:
spustíš vlákno
spustíš druhé, to zjistí, že první běží a ukončí se a před ukončením by mělo spustit další vlákno a stále dokola

Asi bych zavedl nějaký semafor - vlákno jej při spuštění nastaví a při ukončení shodí.
Druhé vlákno se při spuštění podívá, zda je semafor nastaven a pokud ano, bude čekat (můžeš zavést i timeout, aby se po nějaké době ukončilo, pokud by to šlo vše do kytek) Až první vlákno skončí a nastaví semafor, tak se čekající vlákno rozběhne.

Offline age.new

  • Hrdina
  • ****
  • Příspěvků: 309
  • Karma: 0
Semafor je přesně to, co nechci. Ukládání, tedy běh vlákna, může být například i 20 vteřin. Když ale přijde požadavek na nové uložení již po 5 vteřinách, musel bych čekat na dokončení původního vlákna. Nehledě na to, že při čekání na dokončení původního vlákna by mohl přijít další a další požadavek na nové uložení. Proto je nutné původní vlákno ihned přerušit + uvolnit soubory pro nové ukládání.

Offline vandrovnik

  • Guru
  • *****
  • Příspěvků: 1274
  • Karma: 51
    • Verze Delphi: 10.3
A nestačí, aby to vlákno (potomek tThread), které provádí ukládání, během práce testovalo příznak Terminated? Ten se z hlavního vlákna nastavuje zavoláním .Terminate...

Offline Jan Fiala

  • Plnoletý
  • ***
  • Příspěvků: 237
  • Karma: 3
    • Verze Delphi: 10.4.1
    • PSPad editor
Proto je nutné původní vlákno ihned přerušit + uvolnit soubory pro nové ukládání.

Aha, To jsem z toho původního nepochopil, že chceš ukončit původní vlákno.
Jak psal vandrovnik, musíš kontrolovat v původním vláknu nastavený příznak terminated, ale to při kopírování velkého souboru může také nějakou dobu trvat. Pokud máš vlastní mechanismus na kopírování, pak by to problém být neměl, můžeš testovat i v průběhu kopírování a to v případě potřeby přerušit.

Offline pf1957

  • Padawan
  • ******
  • Příspěvků: 3292
  • Karma: 139
    • Verze Delphi: D2007, XE3, DX10
PS: důležité je to "okamžité přerušení vláka" a jeho opětovné vytvoření, aby se data začali ukládat odznovu. Tj. ty původní soubory, se hned před ukládáním vymažou, protože jsou již neplatná.
"okamzite" to nedocilils ani sestrelenim procesu, tj. volanim TerminateProcess, protoze ceka na pending I/O...
Co se da bezne udelat, rozdelit ten zapis napr. po bufferech a mezi jednotlivymi zapisy bufferu otestovat Terminated.

##

Nevim, co ma byt  cilem snazeni, jestli "plati posledni data", pak zalezi, jake je riziko jejich overrunu ve vztahu k trvani zapisu bufferu do souboru. Pokud neni, tak staci rict prave zapisujicimu threadu, za ma udelat rewind a psat nova data. Pokud overrun hrozi, tak jedine FIFO se semaforem, u ktereho ceka nekolik threadu, kazdy zapisuje do nejakeho vlastniho docasneho souboru, pri vyskytu novych dat nejaky broadcast do ostatnich threadu, ze to maji zabalit a po dokonceni "commit" v podobe prejmenovani souboru. Ev. jeden thread cekajici u semaforu a vytvarejici worker thready pro zapis, ktery si je osefuje a ten commit u posledniho zaridi.

Offline age.new

  • Hrdina
  • ****
  • Příspěvků: 309
  • Karma: 0
Děkuji za příspěvky. Jsem trochu zklamaný, že nejde vlákno jednoduše terminovat a tím docílit jeho ukončení nezávisle na prováděném kódu v Execute. Testování, během práce vlákna, na stav terminated občas používám, ale není to "okamžité". Když pak čekám u WaitFor, může se to protáhnout i na sekundy, podle toho v jaké části kódu vlákno je. Navíc pak musí být v kódu pro ukládání mnoho testovacích podmínek.

Rewind běžícího vlákna je vpodstatě to samé. Více vláken s ukládáním do "svého" temporary souboru jsem zamítl, aby v jeden čas neběželo více paralelních ukládání - a tím se vše ještě více zpomalovalo.

 
« Poslední změna: 16-09-2021, 07:16:50 od age.new »

Offline pf1957

  • Padawan
  • ******
  • Příspěvků: 3292
  • Karma: 139
    • Verze Delphi: D2007, XE3, DX10
Děkuji za příspěvky. Jsem trochu zklamaný, že nejde vlákno jednoduše terminovat a tím docílit jeho ukončení nezávisle na prováděném kódu v Execute. Testování, během práce vlákna, na stav terminated občas používám, ale není to "okamžité". Když pak čekám u WaitFor, může se to protáhnout i na sekundy, podle toho v jaké části kódu vlákno je. Navíc pak musí být v kódu pro ukládání mnoho testovacích podmínek.
Proste v praxi nemuzes pouzivat reseni, ktera se hodi tak do skolnich uloh... Bez kooperace na strane threadu se to neobejde a zcela jiste se to pri dnesnich moznostech HW da naprogramovat tak, aby odezva threadu i jeho vykonnost byla uspokojiva, pokud se nejedna o neco extremne narocneho na cas.

Offline < z >

  • Administrátoři
  • Guru
  • *****
  • Příspěvků: 1171
  • Karma: 44
    • Verze Delphi: 7, 2010
Vlákno jde sestřelit pomocí TerminateThread, ale jestli pf říká, že I/O stejně musí nějak doběhnout, tak to stejně nepomůže. Navíc už samotné použití je dost za hranou.
Lepší je fakt nechat 1 sekundu vlákno dojet a mezitím už jet nová data v dalším vláknu. To nemůže být výkonový problém, protože tak jako tak I/O musí dojet.

Offline age.new

  • Hrdina
  • ****
  • Příspěvků: 309
  • Karma: 0
Přepracoval jsem ukládání souborů a pomocí TFileStream jsem se dostal na výborné časy - cca 250MB dat za 1 sekundu. Ale i přesto bych rád celé ukládání spustil v threadu. Mám ale problém, jak si to vše uspořádat. V každé proceduře mám vlastní logování událostí (nazývám to Trace), které je samo o sobě vlákno starající se o správu logu (třídění, ukládání do soubor atd.). Zároveň mám důležité části kódů v try - except blocích, které v případě chyby předá zprávu do logování chyb, které je opět řízené ve vláknu. Obě vlákna se navzájem neovlivňují a pracují vedle sebe.

Důležitá poznámka - všechny procedury a funkce mám v různých unitech (globální), roztříděných podle toho co provádějí a s jakýmy daty pracují. Když si vytvořím vlákno (potomka TThread), tak si nejsem jistý, zda je "thread safe" volat procedury a funkce mimo třídu vlákna. Přepisovat do třídy vlákna, ležícího v jedné unitě, procedury a funkce ležící jinde by mi v tom udělalo chaos. Níže na příkladu uvedu, co mám na mysli:

// unit_1.pas
TMyThread = class(TThread)

// unit_2.pas
Data = array[0..9] of TData;
procedure Uloz_vsechna_data;

// unit_3.pas
procedure Vymaz_soubory(TData);
procedure Uloz_do_souboru(TData);

Průběh:
Procedure Uloz_vsechna_data:
- podmínka, zda běží vlákno TMyThread, pokud ano, přes Event jej nechám začít od začátku, pokud ne, spustím jej.
- ve vláknu TMyThread se spustí cyklus "for a := 0 to length(Data) - 1" a v každém cyklu:
    1) Vymaz_soubory(Data[a])
    2) Uloz_do_souboru(Data[a])

Obě procedury Vymaz_soubory a Uloz_do_souboru obsahují množství Trace a try-except bloků a obě procedury mohu volat kdykoliv jindy i mimo proceduru Uloz_vsechna_data, tj. například na stisk tlačítko zavolám jen proceduru Vymaz_soubory(Data[a]). Samozřejmě si ohlídám, abych vláknu nezměnil data (nevymazal soubory) při jeho činnosti.

Je to tak v pořádku?

Děkuji.



« Poslední změna: 20-09-2021, 09:00:36 od age.new »

Offline pf1957

  • Padawan
  • ******
  • Příspěvků: 3292
  • Karma: 139
    • Verze Delphi: D2007, XE3, DX10
Vlákno jde sestřelit pomocí TerminateThread, ale jestli pf říká, že I/O stejně musí nějak doběhnout, tak to stejně nepomůže.
To jsem popletl a zamenil si TerminateProcess s TerminateThread :-( U toho threadu to nebude s temi I/O tak zhave, i kdyz vlastni I/O stejne neprobihaji na urovni user modu, na disky se psavalo pres DMA, dneska to snad zajistuje nejaky PCI radic...  Pri sestreleni threadu zrejme hned zakazou dalsi prideleni procesoru v user modu.

V kazdem pripade je prasarna zaradit neco takoveho do normalni logiky aplikace, protoze to muze nechat radu sdilenych prostredku v nedefinovanem stavu.

Offline pf1957

  • Padawan
  • ******
  • Příspěvků: 3292
  • Karma: 139
    • Verze Delphi: D2007, XE3, DX10
Mám ale problém, jak si to vše uspořádat. V každé proceduře mám vlastní logování událostí (nazývám to Trace), které je samo o sobě vlákno starající se o správu logu (třídění, ukládání do soubor atd.). Zároveň mám důležité části kódů v try - except blocích, které v případě chyby předá zprávu do logování chyb, které je opět řízené ve vláknu. Obě vlákna se navzájem neovlivňují a pracují vedle sebe.
Z toho popisu jsem Gogo a za logovanim, ktere je samo o sobe threadem, si nedokazu predstavit nic, co by zasahovalo do logiky aplikace: kdyz potrebuju logovat z nejakeho threadu, tak jednoduse zavolam nejakou vhodnou funkci zpravidla globalne pristupneho loggeru a o nic dalsiho se nestaram - zbytek zarizuje logger: ten se musi vyporadat s tim, ze ve stejny cas muze byt volan z ruznych threadu, ze si pripadne data nacpe do vnitrni fronty a rizeni vrati volajicimu, aby nezdrzoval s I/O operaci a pak nejakym internim threadem flushne tu frontu do souboru...

Citace
Když si vytvořím vlákno (potomka TThread), tak si nejsem jistý, zda je "thread safe" volat procedury a funkce mimo třídu vlákna.
Aby bylo neco thread safe, tak to nesmi volne (jako v single-threaded rezimu) pracovat s globalnimi prostredky tj. promennymi, pameti, soubory, I/O etc..., To nijak nesouvisi s tim, jestli je to metoda nebo standalone procedura.

Citace
Je to tak v pořádku?
Dal uz jsem to necetl, protoze me to vycerpalo. Prestoze jsem programoval davno pred vznikem objektoveho paradigmatu, tak me udivuje, ze stale se najdou lide, kteri jak rikaval Ruda Pecinovsky: "dal tvori svoje aplikace proceduralnim zpusobem, jen v objektovych jazykach" :-(
« Poslední změna: 20-09-2021, 09:28:13 od pf1957 »

Offline age.new

  • Hrdina
  • ****
  • Příspěvků: 309
  • Karma: 0
To logování mám udělané tak, abych dokázal zpracovat i tisíce volání bez narušení činnosti běhu aplikace, tj. hlavního vláka. Doslova "pár řádků" tak, aby to bylo co nejméně problematické a "blbu vzdorné". A funguje to velmi dobře.

Mimochodem, programuji tak, jak mě učili na škole, tak jak programují i zkušenější programátoři a jak programují i lidé na různých forech. Je to OOP, ale možná trochu jinak, než jej používáte Vy. Mám kolegy programátory a velmi často bych některé části kódu psal jinak. To ale neznamená, že je jejich kód nefunkční, nebo špatný. 
« Poslední změna: 20-09-2021, 10:10:48 od age.new »

Offline < z >

  • Administrátoři
  • Guru
  • *****
  • Příspěvků: 1171
  • Karma: 44
    • Verze Delphi: 7, 2010
Vlákna nejsou nic zas tak převratného a složitého. Po troše googlení najdeš všechno.
Ono to je už ve standardu v Delphi, kdy ti samy založí novou jednotku s vláknem. Ty jen dáš do execute svůj "výpočet".
Jediné navic musíš udělat nějaký vlastní create (předat parametry) a synchronize (vrátit výsledky). Pro zastavení jen voláš terminate.

Takhle můžeš mít i víc vláken, to si jen schováš do nějakého TList apod.

Já mám logger ve vlastním vlákně. Jen mu thread-safe předám data (critical section) a on se stará ve vlastním vlákně o uložení ve vhodnou dobu.
Celé to je asi o 20 víc řádků delší, než máš ty.

A zde ještě můj oblíbený vzor
http://semi.gurroa.cz/Clanky/Threads.html#ThreadProc

Offline age.new

  • Hrdina
  • ****
  • Příspěvků: 309
  • Karma: 0
Měl bych dotaz k té critical section. Když do ní vstoupím (v Execute části běžícího vlákna) a uvnitř zároveň zavolám Synchronize, může zamrznout aplikace? Přijde mi to logické, kdy na začátku kritické sekce se pozastaví zbývající vlákna a tudíž Synchronize na pozastaveném vláknu nevyvolá činnost a aplikace zde zůstane "trčet" nekonečně dlouho. Je to tak?