Proč je čtení ze Socketu blokjící operace?

anonym

Proč je čtení ze Socketu blokjící operace?
« kdy: 30. 07. 2018, 19:27:22 »
Zrovna jsem přemýšlel nad tím, proč Java spotřebuje nutně na každou blokující operaci jeden thread. V případě Socketu jsem si myslel, že v C++ je možné definovat pro Socket funkci, která bude sloužit jako obsluha pro přerušení z OS. Třeba na Arduinu si totiž můžu definovat funkci jako obsluhu přerušení při nově přijatých datech třeba z SPI. CPU při tomto přerušení přeruší úlohu a spustí obslužnou funkci.

Takže jsem měl představu, že když přijde nový packet, tak se vyvovalá kaskáda přerušení až do programu, kde se tato událost následně oblouží.

Nicméně zjistil jsem, že i v C++ obsloužení socketu umožňuje jen ty možnosti, co v Javě: buďto blokující čtení a nebo dotazování na nová data ve smyčce. Což mě teda trochu zklamalo.

Proč OS tento systém přerušení zazdí a nedovolí událost (nový packet, stisknutá klávesa) zpropagovat až do programu?

A druhá otázka, jak je na úrovni OS řešeno to, když vlákno čeká při blokující operaci? Tady doufám, že tam skutečně je nějaký inteligentní systém, kdy si kernel hlídá které vlákno zrovna čeká na jakou událost a když ta vypukne, tak hned ví, které vlákno má probudit. Tzn. doufám že OS nepřepíná mezi vlákny a nedělá interně něco jako

while(avalable())

ale že má nějakou tabulku typu Vlákno<->Přerušení, takže když přijde nový packet na port 666, tak on hned ví, že vlákno XYZ na to čeká.

Nicméně i přesto tento model dneska vede k problémům u výkonných webových služeb, které musí dokázat obsloužit třeba tisíce requestů za vteřinu, protože to nutí dělat aplikace, které na každý request spotřebují minimálně jeden thread. Např. u Javy EE to tak je.

PS: Jirsák má zakázáno odpovídat.


v

Re:Proč je čtení ze Socketu blokjící operace?
« Odpověď #1 kdy: 30. 07. 2018, 19:30:54 »
epoll?

backup

Re:Proč je čtení ze Socketu blokjící operace?
« Odpověď #2 kdy: 30. 07. 2018, 19:49:10 »
proc blokujici?

inu, protoze je to smysluplne. Priznam se, ze jsem se obcas take nekdy za ta leta zamyslel, proc nektere veci funguji jak funguji. A zatim jsem vzdy zjistil, ze ten Thompson a Ritchie to vedeli spravne a smysluplne.

JSH

Re:Proč je čtení ze Socketu blokjící operace?
« Odpověď #3 kdy: 30. 07. 2018, 19:58:00 »
Psát asynchronní obsluhu událostí je záhul na mozkovnu. Takže se to dělá jen pokud není zbytí. Vlákno, které čeká na data, toho zas tolik nespotřebuje. Žádný modernější OS při čtení dat ze socketu aktivně nečeká, ale dotčené vlákno uspí. Ve většině případů to nepřináší žádný zásadní problém s výkonem. Pro pár IO operací stačí spustit blokující operaci v jiném vlákně, takže běžná api vypadají podle toho.

Pokud to nestačí (hodně zatížený server a podobně), pak má každý OS nějaký platformně specifický způsob, jak na to. V Linuxu může jedno vlákno obsluhovat tři zadeke socketů najednou přes epoll. Na Windowsech se dá spustit asynchronní operace, která po skončení pošle zprávu.

MD

Re:Proč je čtení ze Socketu blokjící operace?
« Odpověď #4 kdy: 30. 07. 2018, 20:03:57 »
Citace
Proč OS tento systém přerušení zazdí a nedovolí událost (nový packet, stisknutá klávesa) zpropagovat až do programu?
Nevím, jak OS obecně, ale třeba Windows má implementaci socketů dost asynchronní (jak v usermode, tak zejména v kernelu), viz například funkce WSARecv, která svými parametry přímo vyzývá k asynchronnímu použití. Provádět tyto operace synchronně je však pro programátora mnohem jednodušší.

Co se týče Arduina, podívejte se blíže, jak fungují SPI, I2C, UART a podobné komunikační mechanismy. Přerušení je obvykle jejich součástí, takže není divu, že vám jej knihovny Arduina dovolují využít.


klass

Re:Proč je čtení ze Socketu blokjící operace?
« Odpověď #5 kdy: 30. 07. 2018, 20:12:30 »
Protote ji máš tak nastavenou, můžeš si ji nastavit i jako neblokující.

gll

  • ****
  • 429
    • Zobrazit profil
    • E-mail
Re:Proč je čtení ze Socketu blokjící operace?
« Odpověď #6 kdy: 30. 07. 2018, 20:38:45 »
Psát asynchronní obsluhu událostí je záhul na mozkovnu. Takže se to dělá jen pokud není zbytí. Vlákno, které čeká na data, toho zas tolik nespotřebuje. Žádný modernější OS při čtení dat ze socketu aktivně nečeká, ale dotčené vlákno uspí. Ve většině případů to nepřináší žádný zásadní problém s výkonem. Pro pár IO operací stačí spustit blokující operaci v jiném vlákně, takže běžná api vypadají podle toho.

není pravda. S použitím coroutin nebo promisů je to jednodušší než s použitím vláken. Kód je téměř identický, ale nemusíte řešit race conditions. Asynchronní obsluhu socketů dnes zvládá každý webař.

gll

  • ****
  • 429
    • Zobrazit profil
    • E-mail
Re:Proč je čtení ze Socketu blokjící operace?
« Odpověď #7 kdy: 30. 07. 2018, 20:52:28 »
Zrovna jsem přemýšlel nad tím, proč Java spotřebuje nutně na každou blokující operaci jeden thread.

protože blokující = zablokuje vlákno.

gll

  • ****
  • 429
    • Zobrazit profil
    • E-mail
Re:Proč je čtení ze Socketu blokjící operace?
« Odpověď #8 kdy: 30. 07. 2018, 21:00:28 »
Nicméně zjistil jsem, že i v C++ obsloužení socketu umožňuje jen ty možnosti, co v Javě: buďto blokující čtení a nebo dotazování na nová data ve smyčce. Což mě teda trochu zklamalo.

tu smyčku si nemusíš psát sám. Můžeš použít třeba Boost.Asio nebo libuv.

borekz

  • ****
  • 492
    • Zobrazit profil
    • E-mail
Re:Proč je čtení ze Socketu blokjící operace?
« Odpověď #9 kdy: 30. 07. 2018, 21:14:16 »
Napsat smyčku je ten nejmenší problém. Těžší je např. parserovat text s LL gramatikou v průběhu načítání. Rekurzivní parser by se implementoval obtížně.
« Poslední změna: 30. 07. 2018, 21:16:37 od borekz »

Re:Proč je čtení ze Socketu blokjící operace?
« Odpověď #10 kdy: 30. 07. 2018, 21:17:54 »
Spíš je otázkou, jaký programátorský nebo programovací styl byste na ten socket ještě chtěl navěsit, pokud Vám nestačí poll() nebo blokující recv() ? Jako že prostě zaregistrovat callback?

Pak je třeba si uvědomit, co by znamenalo takový callback zavolat. Buď by Libc musela na pozadí mít nějaké vlákno, které ty callbacky bude obsluhovat. Nebo pro každé "probuzení callbacku" nějaké to vlákno odštípnout. A pokud ne plnotučné vlákno, tak aspoň nějaký ten tasklet... ale o těch jsem slyšel jenom v kernelu. Nebo by ty callbacky prováděl přímo scheduler? V zásadě pokud by se to mělo dělat nějak "lehkotonážně", jako že ten "callback prostě systém zavolá v nějakém svém kontextu", tak je třeba ten kontext napřed nějak vymyslet/zavést a zřejmě by pak z toho plynula nějaká omezení, co ten callback smí v takovém kontextu dělat.

Vlastně mě jedna analogie napadá: POSIXové signály v UNIXu. Pro signály se taky registrují callbacky. Ale stejně je zase logika taková, že signálů je velmi omezený počet a signál se doručuje vláknu jako celku. Dál tam tuším není jak předat nějaký další argument (uživatelský kontext) - jediným argumentem handleru je číslo signálu. Takže třeba pokud by Váš program v jednom vlákně obsluhoval víc socketů tím způsobem, že by si nechal posílat signály (a třeba by v hlavní smyčce prostě chrápal), tak by při příchodu signálu neměl jak zjistit, který socket zrovna dostal data - aniž by je všechny postupně olíznul. To už mi přijde přímočařejší, použít sockets API a jeho funkci poll().

Nebyl náhodou původní Winsock (ten bez čísla, před Winsock2.dll) řešený tak, že člověk dostával od socketů v zásadě Window Messages? Jak to řekl Linus o event-driven programování? "... It feels good, but it doesn't actually get anything done." ?

Ona i ta obsluha přerušení má svá omezení, podobně jako callback zvaný "signal handler", který si můžete zaregistrovat v user space.

Pravda v souvislosti se sockety se signály zřejmě nepoužívají, přinejmenším nikoli pro "užitečnou práci" socketu = lifrování dat. Pokud se někde mluví o signálech v souvislosti se sockety, tak obvykle v tom smyslu, že funkce recv() spinkající na socketu může být "mimořádně probuzena" signálem, pokud si pár věcí kolem takto nastavíte. Čili ne že by ten signál přišel od socketu jako upozornění na příchozí data. Spíš se to používá na mimořádné věci, jako třeba timeout ("už chrápeš nějak moc dlouho, dlouho nepřišla žádná data") nebo při ukončení programu, pokud se snažíte o graceful shutdown.

=> to už mi přijde víc košer, uloupnout si pro obsluhu každého socketu svůj vlastní plnotučný user-space thread, kde můžu většinu času sladce spinkat v recv() a pokud nějaká data přijdou, tak se nemusím omezovat v paletě "vyjadřovacích prostředků". A pokud potřebuju odvést nějakou práci a přitom zase rychle znovu čekat na další data, tak to roztrhnout na více vláken, přidat nějakého konzumenta, data předávat skrz frontu s mutexem apod. Režie vláken není nijak hrozná.

Ohledně Vaší otázky "jak to dělá process scheduler"... no obecně řadí procesy do dvou kategorií: "běžící" a "spící". Process scheduler žije v kernelu a má nějaké tabulky nebo fronty procesů/vláken/tasků, se kterými žongluje. Když se scheduler rozhoduje, kterému procesu dá veslo tentokrát, vybere si logicky z těch, které jsou cinknuté jako "runnable" (a na ten výběr je nějaký dost chytrý algoritmus, navíc laditelný). Jestli má CPU scheduler nějaký vychytaný rychlý index (hash/btree) kde klíčem je sledovaný "systémový zdroj" (třeba socket) a hodnotou pointer na "struct task", to se mě moc ptáte :-) Poměrně blízko u "pramene" je třeba funkce schedule() = usínající proces ji zavolá, a ona se vrátí v jiném procesu, kterému dal CPU scheduler zrovna "veslo" :-) Je vošajstlich vymýšlet moc složité rozhodovací chytristiky zrovna v tak exponované funkci jako je scheduler - spouští se hodně často (tuším spíš při každém přišedším IRQ, než jenom pravidelně podle timeru) takže třeba balancovat v scheduleru nějaký strom mi přijde možná za hranou.

Našel jsem jedno online čtení které trochu popisuje reálie v Linuxu - bohužel odpověď zrovna na Vaši otázku tam možná přímo nebude. Ale vidím tam pár nápadů, odkud začít číst zdrojáky kernelu :-)

gll

  • ****
  • 429
    • Zobrazit profil
    • E-mail
Re:Proč je čtení ze Socketu blokjící operace?
« Odpověď #11 kdy: 30. 07. 2018, 21:19:25 »
Napsat smyčku je ten nejmenší problém. Těžší je např. parserovat textový formát v průběhu načítání.

Stejně jako synchronně. Dobré knihovny mají file-like abstrakci nad sockety.

gll

  • ****
  • 429
    • Zobrazit profil
    • E-mail
Re:Proč je čtení ze Socketu blokjící operace?
« Odpověď #12 kdy: 30. 07. 2018, 21:31:18 »
příklad tcp komunikace po řádcích v pythonu (s použitím knihovny curio).

Kód: [Vybrat]
from curio import run, spawn, tcp_server

async def echo_client(client, addr):
    print('Connection from', addr)
    s = client.as_stream()
    async for line in s:
        await s.write(line)
    print('Connection closed')

if __name__ == '__main__':
    run(tcp_server, '', 25000, echo_client)

s proměnnou s pracujete stejně jako se souborem, jen před blokující operace přidáte await a před for smyčku dáte async. Žádná věda.

kimec

Re:Proč je čtení ze Socketu blokjící operace?
« Odpověď #13 kdy: 30. 07. 2018, 23:46:02 »
Zrovna jsem přemýšlel nad tím, proč Java spotřebuje nutně na každou blokující operaci jeden thread.
Vyhoda pri blokujucom volani je, ze OS za teba automaticky riesi backpressure. Napr. rychlo pisuce thready sa blocknu dokym consumeri nestihaju.

Nevyhoda je, ze potrebujes dodat nejake vlakno na zablokovanie, ktore nieco stoji a (zjednodusene povedane) pri 10K vlaknach to uz nefunguje tak dobre, hlavne pri sietovom IO.

Nad rotacnym diskom alebo paskovou mechanikou asi nebudes mat 10K threadov robiacich random IO subezne. Tam ziskas najviac, ked znizis contention na minimum, povedzme trebars 1 thread a je ti teda jedno, ze citas blokujuco, kedze vies, ze zariadenie primarne obhospodaruje iba teba a si sam.

anonym

Re:Proč je čtení ze Socketu blokjící operace?
« Odpověď #14 kdy: 31. 07. 2018, 09:56:52 »
Ještě by to chtělo nějaký benchmark, kde výstupem bude graf závislosti doby zpracování requestu na počtu vytvořených threadů.

Představoval bych si to tak, že udělám Server, který bude na obyč socketu naslouchat. Thread poolu nastavím fixně 2000 threadů. Každému requestu dá z poolu 1 thread. Ten thread na 6000ms uspí a potom odepíše OK, takže ten thread nebude vlastně celou dobu nic dělat. Potom porovnám dobu zpracování toho requstu minus 6000ms a bude mě zajímat, jak roste doba obsloužení 1 requestu s rostoucím počtem spících threadů.

Ve smyčce bych dal každých 3ms vytvořit z threadpoolu thread pro 1 request přes socket, takže za těch 6000ms by to mělo udělat celkem necelých 2000 threadů.

Akorát problém tady je, že ten Thread.sleep() úplně nepředstavuje správnou blokující operaci. Nebo jo? To si obsluhuje Java předpokládám, kdy má který thread probudit.