this dir | view | cards | source | edit | dark
top
Pokročilé programování v C#
Základní typování
- typové konverze
- když lze přiřadit proměnnou typu A do proměnné typu B?
- referenční typy
- pokud jsou typy kompatibilní z hlediska hierarchie dědičnosti → implicitní konverze
- hodnotové typy
- existuje implicitní konverze int → long
- provádí se sign extension
- podobně pro ostatní celočíselné typy (směrem, kterým se zachovává hodnota)
- existuje i implicitní konverze na float nebo double – tam se ztrácí přesnost
- neexistují konverze s boolem
- hodnotový → referenční
- provede se (implicitní) boxovací konverze
- referenční typy – explicitní konverze
- máme proměnnou typu object, chceme ji (explicitní konverzí) přiřadit do typu string
- je potřeba ověřit, že typ objektu uvnitř proměnné typu object je string (nebo jeho potomek) – tedy jestli se dá přiřadit
- provádí se runtime check, aby se zjistilo, že se dá přiřadit
- když to nejde, vyhodí se InvalidCastException
- int a long od sebe nijak nedědí, pouze mezi nimi existují konverze
- long → int je potřeba konvertovat explicitně (můžou se ztratit data)
- unboxovat je potřeba explicitně (nemusí to vždycky fungovat)
- musím přesně napsat typ, který je uvnitř (tzn. zaboxovaný int se nedá unboxovat do longu)
- extension methods
- uvažujme třídu Fraction (klasický zlomek, má intový čitatel a jmenovatel)
- chci umět se zlomky pracovat třeba pomocí Math.Sin(…) apod.
- hodilo by se implementovat konverzi na double
- to by šlo zajistit metodou ToDouble()
- tam potřebujeme inty dělit reálně, takže použijeme operátor explicitní konverze k implicitní konverzi
public double ToDouble() => ((double) a) / b;
- hodilo by se implementovat konverzi z double na Fraction
- mohli bychom mít statickou metodu ToFraction, která vrátí Fraction
- Fraction.ToFraction … opakuje se tam slovo
- tak by tam mohl být konstruktor
public Fraction (double d)
- dědičnost?
- chtěli bychom double.ToFraction
- tomu se říká fluent syntax
- použijeme extension metody
- nová syntaxe
- připomínka klíčového slova params – umožňuje metody s proměnným počtem parametrů
class X { public static void f(this A a, int b) {…} }
- dá se volat
X.f(a1, 5);
- ale díky
this
se dá taky volat a1.f(5);
- to se za překladu vyhodnotí jako
X.f(a1, 5);
this
se dá napsat pouze před první parametr
- kdyby existovala i třída Y se stejnou metodou f, tak by se
a1.f(5);
nepřeložilo
- třída X musí být statická (tzn. tohle this se dá zapsat jen u statické metody uvnitř statické třídy)
- to urychluje compile-time hledání vhodné metody
- vhodná metoda se hledá jen uvnitř aktuálního jmenného prostoru (a uvnitř jmenných prostorů importovaných pomocí
using
)
- pokud volám
a.f(…)
- nejdřív se hledá vhodná metoda na typu proměnné
- pak se hledají extension metody v konkrétním namespacu
- pak se hledají extension metody v dalších namespaces
- třída X by se měla jmenovat StringExtensions
- extension metody se dají volat i na potomcích (tzn. hledají se tranzitivně extension metody všech předků)
- pokud třída neimplementuje interface IComparable, tak nám extension metoda CompareTo nepomůže
- k čemu se hodí extension metody
- externí typy
- obrovský projekt a malý podprojekt (do velkého projektu nechci sahat a přidávat tam pomocné metody / zvětšovat rozhraní jednotlivých tříd)
- umožňuje nám to implementovat fluent syntax
- method overloading
m(int i)
a m(long l)
spolu vůbec nesouvisí
- v CIL kódu se objeví jako
m'int
a m'long
- teda místo
int
tam bude System.Int32
apod.
- máme volání – za překladu se podle typu parametrů rozhodne, která metoda se bude volat
- situace
- metody v knihovně
- volání v programu, který knihovnu používá
- za překladu se určilo, že se volá
m'int
- ale v používané verzi knihovny je jenom
m'long
- JIT nutně zahlásí chybu – volaná metoda neexistuje
- jak překladač vyhledává vhodný overload?
- hledá v aktuálním kontextu
- hledá se v kontextu metody a typu za překladu
- tzn. mezi metodami, které jsou definované v daném typu
- tudíž pokud se najde vhodná metoda definovaná uvnitř potomka, tak už se v předkovi nehledá
- proč? kvůli tomu, že předek a potomek můžou být v různých assemblies (např. knihovna a hlavní program)
- hledá podle arity
- hledá nejspecifičtější overload + tak, aby to bylo nejméně práce
- pokud mám metodu s overloady pro long, ValueType a object a volám ji pro int, tak se zavolá longová verze
- dá se definovat uživatelská implicitní konverze
- statická metoda
operator
v jednom z typů
- parametr = zdrojový typ
- název metody = cílový typ
- lze zvolit, jestli je konverze implicitní nebo explicitní
- způsob konverze probereme na cvičení
- není dobré to s konverzemi přehánět
- musí existovat jen jedna taková metoda – jinak překladač vyhodí chybu (ale až pokud chceme konverzi použít)
- když bude jedna implicitní a druhá explicitní a pokusíme se provést explicitní konverzi, taky to vyhodí chybu (jelikož i implicitní konverzi lze volat explicitně)
- když existuje (i uživatelsky definovaná) implicitní konverze na typ, pro který je definovaný overload, tak se použije ten (místo overloadu pro object)
- dokonce to funguje i E2 –> E --> D –> A
- kde –> je dědičnost
- --> je uživatelská implicitní konverze
- existuje overload pro A (ale pak už jen pro object)
- pro proměnnou typu E2 se pustí overload pro A
- ale vybírá se maximálně jedna uživatelská konverze
- protože více konverzí vede ke zmatení
- tedy na řetízku konverzí musí být maximálně jedna uživatelská, překladačových (implicitních hodnotových nebo dědičnostních) může být libovolně mnoho
- zabudovaných konverzí se ale může vybrat víc
- takže pokud na dané úrovni existuje jenom doublová varianta metody, tak se zavolá, pokud ji zavolám s parametrem typu char
- protože existují implicitní konverze char → int → long → float → double
- když se nic nenajde, tak se přesunu o kontext výš a opakuju kroky
- situace
- v předkovi A je metoda m(int)
- v potomkovi B je metoda m(double)
- chceme v potomkovi volat intový overload
- ale
m(1);
volá B.m(double);
- použijeme
((A) this).m(1);
- nemůžeme použít
base.m(1);
?
- někdy ano, ale tohle vynucuje nevirtuální volání, což někdy nechceme
- když se overloady (např. pro int a long) neliší jen typem, ale také sémantikou (třeba efektivitou apod.), tak je vhodné je pojmenovat různě
Generiky
Generické metody
- chceme metody
int Max(int, int)
a long Max(long, long)
- mohli bychom je rozkopírovat, protože vlastně vypadají úplně stejně
- ale kopírování je častým zdrojem chyb
- mohli bychom mít
object Max(object, object)
?
- hodnotové typy by se musely boxovat :(
- co když funkci najednou předám int a double? co má vrátit?
- použijeme generické metody!
T Max<T>(T a, T b) { … }
- uvnitř můžeme používat typ
T
jako placeholder
- metoda se používá jako
Max<int>(…);
apod.
- v C# se dá použít
Max<>(…);
, ale to má velmi specifické použití
- v C++
- hlavičkový soubor
- ve výsledném souboru nikdy není původní šablona
- máme k dispozici jenom konkrétní specializace šablony, které jsme se rozhodli použít
- v C# je myšlenka hlavičkových souborů nahrazena metadaty v assembly
- generická metoda zůstává generickou na úrovni CIL kódu
- tudíž CIL kód musí být dostatečně obecný – k tomu se dostaneme později
- v CIL kódu volání bude výběr konkrétní specializace generické metody
- JIT za run-timu vyrábí specializované varianty generické metody
- konvence – placeholder typy obvykle začínají písmenem T
- důležitá myšlenka – překladač si může vhodnou specializaci zvolit sám
- takže napíšu
Max(…)
a překladač zvolí vhodnou specializaci automaticky
- pokud má metoda např. tři parametry typů T1, T1, T2, tak překladač vlastně řeší rovnici
- pokud se mi nelíbí automatická volba specializace a chci to ovlivnit, tak můžu přetypovat parametry – ale vhodnější je prostě definovat specializaci do špičatých závorek
- generická metoda se dá kombinovat s konkrétními overloady
- překladač zvolí overload pro konkrétní typy parametrů, pokud existuje
- pokud neexistuje, typicky zvolí generickou metodu
- situace – mám generickou metodu, která volá generickou metodu s konkrétními overloady
- metoda
m
má generickou variantu, ale také variantu pro parametr typu double a pro object
CallM<T>
je generická, uvnitř je volání m(v)
, kde v
je typu T
- to se přeloží na volání
m<T>(v)
- konkrétní overloady se nikdy nezavolají, jelikož se za překladu musí určit, která jedna metoda se v rodičovské generické metodě bude volat
- a generická
m
je prostě jediná vhodná – je použitelná pro všechna možná T
- kdyby tam generická metoda nebyla, volal by se overload s parametrem object
- tohle chování je jiné v C++
- tam se za generický typ dosazuje při překladu
- připomenutí
- máme generickou metodu
- za compile timu vznikne jeden CIL kód této metody
- za run timu – jakmile se zavolá určitá (např. intová) varianta metody, JIT vygeneruje strojový kód pro danou variantu metody
- v CIL kódu je zapsáno, jaká metoda se volá
- zda je to nějaká specializace generické metody
- nebo je to třeba nějaký z konkrétních overloadů metody
- ve složitější typové hierarchii
- generická metoda může být virtuální
- dá se overridovat pouze generickou metodou (jinou implementací)
- pokud v potomkovi zadefinuju negenerickou metodu s dosud neexistujícím typem (tedy overload) s
new
, tak je to new
zbytečné, nic se nezakrývá
- co můžeme dělat uvnitř generické metody s parametrem typu
T
?
- můžeme volat metody objectu
- připomenutí
- v Pythonu duck typing
- v C# compile-time duck typing u pattern matchingu (viz dekonstruktory)
- v C++ compile-time duck typing u šablon (generických metod) – strojový kód jednotlivých specializací metod se generuje za compile timu
- v C# se taky dá používat pythonovský duck-typing
- vydáme na půdu materiálního vulgarismu – použijeme klíčové slovo
dynamic
- u proměnné s typem
dynamic
se zapne runtime duck typing
- proměnná se přeloží jako typ object
- ale dají se na ní volat libovolné metody
- za runtimu se zjistí, jestli existují – pokud ne, tak se vyhodí chyba
- u generických metod použijeme interfaces, abychom mohli volat něco jiného než metody objectu
void m<T>(T a) where T : podmínky {}
- další where se píše na další řádek
- tento způsob kontraktu je důležitý – i pro vývojáře daných metod (aby v další verzi něco nerozbili)
- metoda s interfacovým parametrem vs. generická metoda s constraintem na daný interface
- funguje to hodně podobně
- rozdíl – interfaces u hodnotových typů
- v metodě s interfacovým parametrem se bude hodnotový typ boxovat
- v generické metodě se nic boxovat nebude, navíc se strojový kód vygeneruje přímo pro danou hodnotu (respektive pro daný typ) včetně všech optimalizací
- ale typicky dává smysl preferovat klasickou metodu s interfacovým parametrem
- JIT umí pro referenční typy recyklovat strojový kód
- pro (různé) hodnotové typy to vždycky generuje nový strojový kód
- u generických metod typicky chceme používat type inference (automatické generování špičatých závorek u volání) – to může komplikovat práci překladači (?)
- další použití
- generická rozhraní (interfaces) – např.
IComparable<T>
- extension metody
public static T[] Slice<T>(this T[] source, …) { … }
- s generickými extension metodami je potřeba šetřit (a psát jasné constraints)
Generické typy
- může to být třída, struktura, interface
- syntaxe podobná jako u metod – také s constraints
- za compile timu se generuje jeden CIL kód generického typu
- za runtimu JIT generuje typy jednotlivých specializací
- strojový kód metod se klasicky JITuje až při prvním volání
- každá specializace má svůj vlastní class constructor a své vlastní statické fieldy
- dědičnost
- specializace generických typů jsou z hlediska stromu dědičnosti na stejné úrovni (nevedou mezi nimi hrany)
- generická třída může dědit od negenerické třídy nebo od generické třídy, kde její specializace může být daná nebo může odpovídat specializaci potomka
- v generické třídě můžou být metody…
- negenerické s určenými typy parametrů
- negenerické s typy parametrů odpovídajícími specializaci generické třídy (tedy
T
)
- generické (s typy parametrů nezávislých na specializaci generické třídy)
- → překladač vždy vybere nejspecifičtější variantu
- když má na výběr mezi negenerickou s určeným typem a negenerickou s typem T, tak vybere tu s určeným typem
m(T t)
, kde T
je int
vs. m(int i)
→ vyhraje m(int i)
- u constraints se dá použít čárka, ta znamená AND
- lze mít více generických typů s různými constraints
- generické fieldy nejsou, ale můžu mít field typu T v generické třídě
- interfaces
- metoda interfacu se dá implementovat zděděním (rodičovský typ má metodu, kterou vynucuje interface)
- jedna metoda může zároveň naplňovat více kontraktů (když třída implementuje víc interfaců a všechny požadují jednu metodu, např.
Close
)
- interfaces:
IReader
, IWriter
- metoda v obou interfaces se jmenuje
Close
- je potřeba rozmyslet, kdo volá
Close
(nebo Dispose
) – kdo daný zdroj drží (a kdo je zodpovědný za jeho uzavření/uvolnění)
- u TCP protokolu dává smysl mít dvě různé implementace
Close
- chceme mít oddělené zavření pro čtení a pro zápis
- můžeme to poskládat pomocí dědičnosti – Reader implementuje IReader, Writer je potomkem Readera, zakrývá Close, implementuje IWriter, ReaderWriter je potomkem Writera, jeho Close zavolá obě varianty
- ale je to hrozně složité řešení
- explicitní implementace metody z interfacu
- syntaxe
- takhle implementovanou metodu nemůžu zavolat přímo na daném typu, musím použít typ interfacu
- takový přístup používá System.Int32, který implementuje interface IConvertible, ale interfacové metody to má implementované explicitně, takže se uživatelům běžně nezobrazují jako součást rozhraní
- generické typy v kombinaci s interfaces
- situace ze 4. prezentace
- nepřeloží se – není jasné, metoda kterého interfacu se má volat
- implementace generických interfaces
- také v prezentaci
- např. dvě vlastnosti se stejným jménem a odlišným typem nemůžou koexistovat, aspoň jednu z nich musíme implementovat explicitně
- this
- v metodách máme implicitní parametr this
- to platí i pro metody v interfacech
- metody v interfacech musí být instanční, jinak by nebylo jasné, co bude this
- nová funkce C# – statické abstraktní metody v interfacech
- je nezbytné je implementovat statickou metodou
- k čemu je to užitečné?
- když máme generický typ a chceme volat jeho statickou metodu
- nově se u instančních metod v interfacu dá psát abstract (ale je lepší to tam nepsat)
- funguje to i pro method-like věci (třeba vlastnosti)
- do interfacu se dá zapsat defaultní implementace
- this dává smysl používat jenom v instančních metodách
- co když chci odkazovat na typ implementátora
- použiju
TSelf
interface I1<TSelf> where TSelf : I1<TSelf> { … }
- kdy se tohle používá?
- kdybych chtěl implementovat komplexní čísla s (generickým typem) volitelnou přesností
- problém by nastal např. při implementaci sčítání
- nově v C# existují interfaces, které nám tohle umožňují – mají implementovány potřebné operátory
- speciální constrainty
- constraint na hodnotové typy
where T : struct
- constraint na referenční typy
where T : class
Variance
C<object>
a C<string>
mezi sebou nemají žádnou vazbu – nijak od sebe nedědí
- bylo by hezké, kdyby se
List<string>
dal použít jako List<object>
, protože všechny stringy jsou potomky objectu
- pojmy
- (α) typ B je typově kompatibilní s A
- typicky pokud B dědí od A
- takže instance B se dá přiřadit do proměnné typu A
- (β) typ C je parametrizovaný T a
C<B>
je typově kompatibilní s C<A>
- (γ) typ C je parametrizovaný T a
C<A>
je typově kompatibilní s C<B>
- C je kontravariantní podle T
- (δ) třetí varianta kompatibility
C<A>
a C<B>
- generické typy jsou invariantní (??)
- pole referenčních typů jsou kovariantní
- pole hodnotových typů jsou invariantní
- každý zápis do každého pole referenčních typů vede na runtime check
- kdybych měl pole stringů
- to přiřadil do proměnné typu pole objectů
- a pak do prvku přiřadil int
- tak by to bylo blbě, protože bych do pole stringů přiřadil int
- i když kovarianci nepoužívám, check se provádí
- kovariance
- getter je v pohodě
- setter je problém
- kontravariance
- setter je v pohodě
- getter je problém
- typ nemůže být zároveň kovariantní i kontravariantní
- pro jiné typy než pole se dá zapnout variance – ukážeme si příště
- od C# 9 jsou virtuální metody kovariantní dle návratové hodnoty
- v overridu metody můžu vracet „lepší“ typ, než který vracela původně
- ale tohle zase funguje jenom u referenčních typů
- protože by si navrácené hodnoty neodpovídaly velikostně ani sémanticky (reference na haldu vs. číslo apod.)
- kovariance u polí
- někdy na škodu – musíme provádět runtime check
- někdy užitečná – můžeme psát univerzálnější kód
- specializace Listu jsou invariantní
- takže do parametru typu
List<object>
nemůžeme předat List<Person>
- parametr
List<object>
je zbytečně specifický, stačí nám vlastnost Count
a možnost indexace
- mohli bychom použít interface
IList<object>
- takže můžeme jako parametr použít pole objectů
- generické interfacy u referenčních typů jsou volitelně variantní
interface I<T>
je invariantní dle T
interface I<out T>
je kovariantní dle T
- funguje jako výstupní typ metody
interface I<in T>
je kontravariantní dle T
- funguje jako vstupní typ metody
- pro každý typový parametr je to nezávislé
- příklad
interface I<out T1, T2, in T3, in T4>
I<A,B,C,D> i = X : I<E,F,G,H>
- E dědí od A
- B = F
- C dědí od G
- D dědí od H
- tady je to správně, v přednášce chybně, viz errata dokument
- nestačí implicitní konverze – musí to být podle typového systému
- neprovádějí se runtime checky, prostě se zakáže špatné použití
IList<T>
musí být zjevně invariantní, protože indexer vyžaduje getter a setter – tudíž musí být jako vstupní i výstupní typ
- existuje
IReadonlyList<out T>
, kde indexer vyžaduje jenom getter
- máme List stringů, chceme ho přiřadit do
IList<object>
, to nejde
- překladač nám poradí použít cast
- s castem se to přeloží, ale runtime check selže
- obecně se dá castovat typ do interfacu, který neimplementuje, jen se provádí runtime check, jestli konkrétní objekt (jeho typ) implementuje daný interface
- v proměnné typu A mám instanci typu B
- B dědí od A
- typ A neimplementuje interface I, ale typ B ho implementuje
- můžu tu proměnnou explicitně castnout na interface I
- kdyby typ A byl sealed, tak by se nám cast na interface, který neimplementuje, ani nepřeložil
interface IComparer<in T>
int Compare(T a, T b)
- kontravariantní
- máme metodu s argumentem typu
IComparer<B>
B
je potomkem A
- takové metodě můžeme předat argument typu
IComparer<A>
- kovarianci použijeme, když budeme chtít mít pro zvíře jeden logger
- pro vlka chceme mít speciální logger uložený ve stejné proměnné
Kolekce
- interfacy kolekcí
- dává smysl používat generické varianty – ty negenerické jsou tam kvůli zpětné kompatibilitě
- IList má oproti ICollection navíc indexer
- IEnumerable
- kdyby IEnumerable měl vlastnost Current a metody MoveNext a Reset
- seznam by byl immutable
- ale backing field vlastnosti Current by byl mutable
- takže by ten typ nebyl immutable jako celek
- to je nepraktické při vícevláknovém programování
- návrhový vzor iterátor
- v dotnetu
IEnumerator<out T>
- dostaneme ho pomocí metody GetEnumerator
- pak z každého vlákna můžeme kolekci procházet nezávisle
- z dotnetu 1 je tam negenerický typ IEnumerable
- vlastnost Current v negenerickém interfacu IEnumerator je typu object
- takže při implementaci IEnumerable musíme implementovat obě metody GetEnumerator
- a taky musíme implementovat oba iterátory
- na
IEnumerable
spoléhá implementace foreach cyklu
- metoda Reset je často k ničemu
- mnoho iterátorů ji nemá
- místo ní hážou výjimku (not implemented)
- když budu implementovat iterátor, je lepší ho tam mít
- když budu používat nějaký obecný iterátor, je lepší Reset nevolat – radši získat nový iterátor
- namespaces (jmenné prostory)
- můžu je vnořovat – je to syntaktická zkratka
- tečka je v dotnetu validní součást identifikátoru
- překladač vidí jenom názvy typů jako celek (i s tečkami)
- v názvu jmenného prostoru může být tečka
- takže namespace A.C je stejný jako namespace C uvnitř namespace A
- jmenné prostory jsou z pohledu CLR ploché – zanoření nemá žádný speciální význam
- když použijeme using, tak se aktivují extension metody daného jmenného prostoru
- nested types (vnořené typy)
- z pohledu CLR jsou typy opravdu vnořené (na rozdíl od namespaces)
- mějme namespace X, v něm typ A, uvnitř vnořený typ B
- konvence CLI pro názvy vnořených typů (pro výpis)
- konvence C# pro názvy vnořených typů (pro použití v kódu)
- k čemu je to užitečné?
- v Javě je souvislost mezi instancemi A a B
- uděláme instanci A
- v kontextu A vyrobíme B
- to B má zpětně referenci na instanci A
- v C# tohle neplatí
- v C# můžu lépe upravovat viditelnost typů
- normální typy můžou mít viditelnost internal, public nebo file
- vnořené typy můžou být private, protected, public, internal, …
- docela častá je viditelnost private – daný typ vidí jenom kód uvnitř A
- kdyby A byl SortedDictionary implementovaný červeno-černým stromem, tak potřebuju nějaký typ, který bude reprezentovat vrchol toho stromu
- je to implementační detail, takže je vhodné, aby typ Node byl vnořený – takže nehrozí, aby ho začal používat někdo jiný
- když máme metodu v internal interface a nějaká třída ji implementuje jako interfacovou, tak se bez přístupu k interfacu ta metoda nedá zavolat
- metoda by taky mohla být interní a výsledek by byl stejný, ale to teď není důležité
- podobně můžeme mít v nějakém typu vnořený private interface a použít podobný efekt u vnořeného typu, který ten interface implementuje
- vnořený typ má přístup k private věcem nadřazeného typu
- vnořený typ může dědit od nadřazeného typu
IEnumerable<T>
- dává smysl Enumerator mít jako privátní vnořenou třídu
- jak se enumerátor chová
- kolekce o 0 prvcích
- pokud zavoláme getter vlastnosti .Current před spuštěním, vyhodí to InvalidOperationException
- pokud zavoláme .MoveNext() u prázdné kolekce, vrátí to false
- když potom zavoláme .Current, vyhodí to InvalidOperationException
- kolekce o 1 prvku
- .Current → InvalidOperationException
- .MoveNext() → true
- .Current → 1. prvek
- .Current → 1. prvek
- .MoveNext() → false
- .Current → InvalidOperationException
- enumerátor má tři stavy – před kolekcí, uvnitř kolekce, za kolekcí
- metoda Reset by nás z libovolného stavu měla vrátit do stavu před kolekcí – ale často nebývá implementována
- proč má enumerátor stav „před kolekcí“? aby podporoval prázdné kolekce
- kdyby pro nás bylo důležité, kolik má kolekce prvků, použijeme ICollection s vlastností Count
- nikdo nás nenutí projít IEnumerable celé, nikdo nás nenutí číst prvky (třeba nás jenom zajímá, jestli kolekce obsahuje nějaký prvek → jednou použijeme MoveNext)
- v pokročilejších frameworcích může být uvnitř enumerátoru drženo spojení do databáze nebo nějaké cenné zdroje
- IEnumerable rozšiřuje IDisposable
- takže pokud používám enumerátor, měl bych nakonec zavolat Dispose
- můžu vyrábět nekonečné kolekce – např. sekvenční generátor náhodných čísel
- pokud potřebuju konečnou kolekci, použiju ICollection
- concurrent modification
- během používání enumerátoru modifikuju kolekci
- může mi vzniknout race condition
- tedy kolekce IEnumerable typicky nepodporují concurrent modification, může se vyhodit InvalidOperationException
- problém je, když se modifikace provede mezi dvěma použitími jednoho enumerátoru
- bug není v tom, že já modifikuju kolekci (pokud je třeba IList)
- bug je v tom, že mi někdo dal moc velká práva ke kolekci
- takže výjimka by měla být vyhozena z MoveNext
- kolekce si může evidovat verze
- enumerátor si může evidovat verzi, ze které vznikl
- při volání MoveNext se porovná verze
- foreach
- generuje while cyklus
- překladač…
- nejdříve zkusí, jestli se na typu dá zavolat GetEnumerator
- můžeme ji dodat i pomocí extension metody
- tohle je vlastně duck-typing
- pak zkouší, jestli typ implementuje generický interface
- nakonec zkouší, jestli typ implementuje negenerický interface
- na nalezený interface to přetypuje
- dá se použít type inference (var)
- pokud použijeme konkrétní typ, tak se návratová hodnota Current explicitně přetypuje na daný typ
- jak pracovat s IList?
- použít foreach nebo for cyklus?
- při každém použití foreach se vytváří nový enumerátor
- ale u polí se foreach překládá efektivněji – do for cyklu
- pokud je za překladu jasné, že to bude pole
- dává smysl, aby enumerátor byla struktura?
- ale IEnumerable je interface, takže se enumerátor bude boxovat
- no ale mohli bychom ve veřejné metodě GetEnumerator vracet strukturu
- tím pádem musíme typ enumerátoru zveřejnit, aby se dal používat
- tohle pak bude fungovat i s foreach cyklem
- pokud překladač o typu ví jen to, že implementuje interface, tak na něm volá interfacovou metodu
- programujeme enumerátor
- máme třídu A, tam jsou dvě pole x1, x2, ale ta se mají tvářit jako jedno
- kdybychom chtěli dělat PrintAll, pomocí for cyklu bychom přeiterovali přes obě pole – prolíná se algoritmus pro výpis a pro iteraci
- v enumerátoru budeme mít uložený stav procházení
- v jaké fázi jsme (které ze dvou polí zrovna procházíme)
- v kolikátém jsme prvku
- je vlastně docela těžké tvořit enumerátory
- v C# je koncept iterátorových metod
Iterátorové metody
- pokud metoda vrací IEnumerator a obsahuje
yield return
, zcela se změní způsob jejího překladu
- metoda se podle yield returnů rozseká na jednotlivé kroky
- první krok – od začátku metody do návratové hodnoty yield returnu (včetně)
- druhý krok – od středníku za yield returnem do dalšího yield returnu
- poslední krok – od středníku za posledním yield returnem do konce metody
- návratová hodnota yield return se někam uloží, takže volání getteru vlastnosti Current vrací přímo hodnotu (už ji znova nepočítá)
- neplatná volání Current jsou v rozporu s typickým kontraktem enumerátoru
- volání getteru Current před prvním MoveNext vrací defaultní hodnotu
default(T)
- get_Current po posledním MoveNext vrací poslední hodnotu
- náš kód skončí uvnitř enumerátoru
- v těle naší metody nezůstane náš kód, ale bude tam vyrobení a vrácení toho enumerátoru
- lokální proměnné z našeho kódu budou uloženy jako fieldy enumerátoru
- platí to pro všechny lokální proměnné
- v Release režimu si překladač všimne, že některé lokální proměnné není třeba držet globálně a přeloží je jako lokální (ne jako fieldy)
- parametry naší iterátorové metody se taky uloží do enumerátoru (respektive jejich hodnoty se tam uloží – „capture by value“)
- pokud je iterátorová metoda instanční metoda nějakého objektu, tak v ní můžu použít vlastnosti toho objektu (protože má implicitní parametr this)
- pak se musí
this
nakopírovat (capture by value) dovnitř enumerátoru
- náš kód se přeloží do stavového automatu (metoda MoveNext funguje jako stavový automat)
_state
- na začátku ve stavu 0
- při běhu se nastaví na -1
- až na konci se nastaví na další validní stav
- tím způsobem se ošetřuje situace, kdy se při běhu MoveNext vyhodí výjimka – mohlo dojít k poškození vnitřního stavu enumerátoru, takže se prostě skončí
- -1 je koncový stav
- sdílený kód jednotlivých stavů se sdílí pomocí goto
- můžeme používat
yield break
– to je okamžitý přechod do koncového stavu
- enumerátor podporuje lazy evaluation
- iterátorové metody taky – s každým voláním MoveNext se provede jenom jeden krok
- chci IEnumerable převést na List
- lazy evaluation se dá převést na eager evaluation
- konstruktor Listu má overload
new List<T>(IEnumerable<T>)
- někdy se to hodí, někdy to není dobrý nápad
- třeba pokud potřebuju jenom první tři položky, tak není vhodné převádět celou kolekci na seznam, když má milion položek
- ale pokud se při každém MoveNext něco stahuje ze sítě a chci přes kolekci iterovat víckrát, tak dává smysl si ji někam uložit pomocí eager evaluace
- v System.LINQ jsou k tomu metody ToList a ToArray pro IEnumerable
- trochu efektivnější je ToList, protože ToArray se pak musí kopírovat do pole, jelikož LINQ předem nezná délku IEnumerable
- ale pokud je daná věc zároveň IReadOnlyCollection a tedy má Count, tak ho LINQ použije
- LinkedList
- veřejná třída LinkedListNode – jednotlivé krabičky s hodnotami, aby se dalo přidávat před ně, za ně apod.
LinkedList<T>
implementuje IEnumerable<T>
- bylo by hezké, kdyby se dalo enumerovat přes krabičky v LinkedListu
LinkedList<T>
by mohl implementovat IEnumerable<LinkedListNode<T>>
, ale pak by se ten enumerátor složitěji používal
- takže přidáme extension metodu
AsNodeEnumerable
- můžeme použít iterátorovou metodu
x = new A {1,2,3};
je syntaktická zkratka za x = new A(); x.Add(1); x.Add(2); x.Add(3).
- používá se compile-time ducktyping
- Add se dá doplnit jako extension metoda
- LinkedListNode
- vlastnost Value s getterem a setterem
- vlastnost ValueRef vrací referenci přímo na hodnotu (má jenom getter), takže může zefektivnit práci s hodnotami uvnitř LinkedListu
- přidáváme krabičky uvnitř foreache za aktuální krabičku
- pokud budeme enumerovat lazy evaluací přes LinkedList, tak se to zacyklí
- protože enumerátor typicky nepodporuje concurrent modification
- pokud LinkedList převedeme pomocí eager evaluace na List, tak se to nezacyklí, ale zabere to dost paměti
- nejefektivnější varianta bude taková, že budeme prvek přidávat za minulou krabičku
- iterátorové metody
- můžou vracet i IEnumerable
- např. metoda
IEnumerable<T> Range(int from, int to)
s yield returny uvnitř
- rozpadne se to do dvou tříd
- jedna (α) bude implementovat IEnumerable
- budou tam captured params by value
- bude tam metoda GetEnumerator
- tam se vytvoří instance enumerátoru (tedy té druhé třídy)
- dovnitř se nakopírují captured params
- druhá (β) bude implementovat IEnumerator
- tam bude náš kód
- budou tam captured local vars by move
- bude tam state
- IEnumerator nemá odkaz na první třídu, ale má vykopírované její zachycené lokální proměnné
- velice typická situace –
foreach (var x in Range(1, 10))
- tzn. vytvořily by se instance dvou tříd a ty by se zahodily
- tedy místo dvou oddělených tříd C# překladač reálně vygeneruje jenom jednu třídu (γ), která implementuje IEnumerable i IEnumerator
- sémanticky to funguje tak, jako to byly dvě oddělené třídy
- další stav … -2
- pokud ještě enumerátor nikdo nepoužil
- takže pokud se GetEnumerator zavolá podruhé, potřetí apod., tak se vytvoří nová instance třídy γ (kontroluje se, jestli byl iterátor použitý a jestli se používá ze správného vlákna)
- v C++ 20 jsou taky iterátorové metody
- máme kód pro generování Fibonacciho čísel
- chceme po ChatGPT 3.5, aby nám to přepsalo do C#
- obecná poznámka: pozor, ChatGPT lže
- nabídne nám async metodu – což nepotřebujeme
- poprosíme o přepsání → úspěch
- řekneme, ať nám napíše testovací metodu, která bude zachycovat výjimku, když je požadovaná sekvence moc dlouhá
- ale ta metoda nefunguje, výjimka se nezachytí
- prosíme o přepsání, ale to nikam nevede
- problém je v tom, že se výjimka vyhazuje v MoveNext – protože v metodě pro získání instance IEnumerable se žádný náš kód neprovádí
- ale výjimka by se neměla šířit z MoveNext
- je to špatná implementace
- potřebovali bychom, aby se výjimka vyšířila z metody FibSeq
- řešením je přepsat to – tenhle check provádět „výš“
- mít neiterátorovou metodu s checkem, která volá iterátorovou metodu, pokud check projde
- je fajn používat CLS compliant typy – místo uintu použít int
- místo InvalidOperationException je lepší vyhodit ArgumentOutOfRangeException
- ChatGPT nám vygeneruje hezkou dokumentaci
Reflection
- proces překladu
- máme C# zdrojáky
- ty se přeloží do CIL kódu
- přípona .dll
- assembly
- CIL kód a metadata
- ten se JITuje do konkrétního strojového kódu
- programy ildasm a ILSpy zkoumají metadata jiné assembly
- Reflection
- typ Type
- když zavoláme
typeof(A)
, kde A
je typ, nebo x.GetType()
, kde x
je proměnná
- typ Assembly
- statické metody
Assembly.GetExecutingAssembly()
Assembly.GetCallingAssembly()
Assembly.GetEntryAssembly()
- můžu přistupovat i ke knihovnám, které používám
- jakmile dostanu instanci Assembly, můžu zavolat GetTypes
- GetType(string) vrátí instanci Type, pokud typ s daným jménem existuje
- na typu Type existuje spousta užitečných metod, které vrací instance potomků abstraktní třídy MemberInfo
- GetFields
- GetMethods
- GetConstructors
- GetProperties
- GetEvents
- co s tím?
- můžeme implementovat pokročilejší koncepty, než nám C# defaultně umožňuje
- kdybychom chtěli pythonovský duck typing (bez klíčového slova dynamic – to se někdy nedá použít)
- dvě nesouvisející třídy A a B, obě mají metodu Run
- chceme metodu RunIt, která na libovolném typu zavolá metodu Run, pokud ji ten typ má
MethodInfo? mi = o.GetType().GetMethod("Run");
- zásadní problém reflection – není to safe, může to vést k běhovým chybám
- na typu MethodInfo existuje metoda Invoke, která má dva parametry – první je typu object („this“ parametr) a druhý je
params
pole objectů (ostatní parametry metody)
if (mi is not null) mi.Invoke(o, null);
- můžu hledat konkrétní variantu metody s parametry určitých typů
- druhý parametr GetMethod …
new Type[] { typeof(string), typeof(long) }
- metoda Invoke
- boxuje hodnotové typy a pak je případně zase unboxuje, aby typy pasovaly s typy parametrů
- Reflection se pokouší dělat implicitní konverze samostatně – třeba pokud voláme metodu s longovým parametrem, ale dáme jí int, tak se to pokouší konvertovat
- u GetMethods můžu použít BindingFlags
- problémy reflection
- je pomalá – kvůli obecnosti, boxingu, konverzím apod.
- není safe – může způsobovat běhové chyby
- je potenciálně nebezpečná – dá se přistupovat k privátním metodám, fieldům apod.
- serializace objektů
- instance objektů reprezentují data
- tato data chci převést do textového (XML, JSON) nebo binárního (ProtocolBuffers) formátu
- JSON
- JavaScript Object Notation
- javascriptové objekty jsou prototype-based, nemají klasické typy
- JSON i XML data ukládají ve formě stromu
- různé serializační frameworky nám umožňují různé způsoby serizalizace
- serializační frameworky obvykle zvládají serializovat DAGy
- pokud k objektu A vedou z kořene dvě orientované cesty, tak se vytvoří jeho kopie
- někdy se framework dá nastavit, aby nevytvářel kopie, ale označil si objekty nějakými identifikátory
- takže při deserializaci se zachová původní tvar grafu objektů
- v C# jsou serializátory
- v namespacu
System.Text.Json
je string JsonSerializer.Serialize<T>(T root)
- používá Reflection
- co serializovat?
- serializace veřejných vlastností – výchozí nastavení
Delegáti
- motivační příklad
- máme třídu B s binárním stromečkem, která implementuje
IEnumerable<int>
- mohli bychom foreachem enumerovat přes podstromy a yield returnem vracet hodnoty
- při prvním volání MoveNext se vygeneruje kaskáda (vlastně spoják) enumerátorů
- pro každý uzel se vyrobí enumerátor
- kód je hezký a krátký, ale je to hrozně neefektivní
- takže to implementujeme nějak líp
- pomocí enumerátoru se na strom můžeme dívat jako na pole
- metoda IndexOf vrátí index prvku v tom pohledu
- můžeme použít enumerátor, ale to vede na O(n) algoritmus
- IndexOf zná implementační detaily, takže může hodnotu hledat nějak efektivněji, třeba i v O(log n)
- máme třídu A
- interně má odkaz na třídu B
- chceme najít prvek, který se rovná 5
- chceme najít prvek, který je menší než 5
- musíme do třídy B dopsat IndexLessThan
- to, co třída A potřebuje, vnucuje třídě B, že to musí umět
- ale přitom jsme mohli napsat v třídě A klasický foreach s podmínkou – to by nebylo tak efektivní
- chceme efektivitu i specifičnost
- foreach
- řídí to třída A
- subkroky dělá třída B (MoveNext)
- mohli bychom tu logiku otočit
- v třídě B by byl cyklus
- používal by kontrolu z třídy A
- tzn. třída B by to řídila a subkroky by dělala třída A
- chtěli bychom rozhraní IComparison s metodou Compare(int x) → bool
- kde se ten interface má implementovat?
- třída A by ho mohla implementovat
- ale z implementačního detailu jsme ho dodali jako veřejné API třídy A
- takže to nechceme
- dovnitř bychom mohli dodat privátní třídu LessThan5, která by ho implementovala
- to by šlo, ale je to hrozně ukecané
- bylo by fajn, kdybychom to mohli udělat syntakticky úsporněji
- my prostě chceme zavolat metodu
- zatím neumíme předat ukazatel na metodu
- v C to jde, ale není tam typová kontrola
- v C# je koncept
delegate
(delegát)
delegate
- klíčové slovo
- každý delegát je referenčního typu
- vždycky musíme deklarovat konkrétní typ delegáta
- např.
delegate int D(string s);
- každý konkrétní delegát je potomkem System.MulticastDelegate (viz ET přednáška), ten je potomkem System.Delegate, ten je potomkem System.Object
- funguje to jako libovolný jiný typ
- default hodnota proměnné je null
d1 = new D(m);
- kde m je identifikátor metody
- nesmíme tam napsat m() – musí tam být proměnná typu D
- obsahuje ukazatel na metodu
- delegáti mají metodu Invoke
- pozor, neplést s Invoke v reflection – tam je to výrazně obecnější (a pomalejší)
- na jménech parametrů v deklaraci delegáta nezáleží – je to dokumentace
- ale je možné „volat“ přímo delegáta – je to syntaktická zkratka pro Invoke
- různí delegáti, kteří mají stejné parametry a stejný návratový typ, spolu nesouvisejí – nedají se navzájem přiřazovat
- pozor
D d = new D(m);
D d = m;
- je to skoro to samé
- syntaxe s new vynucuje nového delegáta
- druhá varianta používá cache – takže nemusím delegáta vyrábět znovu, pokud už existuje
- obvykle můžeme použít druhou variantu
- ale pokud instanci delegáta někde používám jako klíč, tak chci použít verzi s new
- instance delegátů jsou immutable (podobně jako stringy)
- delegáti a viditelnost
- z vnějšího kontextu nemůžu vyrobit delegáta privátní statické metody
- ale můžu mít public metodu, která vrací delegáta, který ukazuje na private statickou metodu
- delegáti můžou ukazovat i na instanční metody
- instanční metody mají jeden skrytý parametr – ale bylo by divné o nich uvažovat jako o víceparametrických než jsou
- v delegátu jsou dva ukazatele – na funkci a na
this
- u statických metod je tam null
- u instančních metod ukazuje na tu instanci
- dají se dělat pokročilejší věci – viz ET přednáška
- stále platí, že delegáti jsou immutable – to konkrétní
this
je tam navždy
- u struktur – do
this
se zkopíruje struktura (zaboxuje se)
- delegáti můžou být generičtí
- můžou být kovariantní a kontravariantní – viz ET přednáška
- u delegátů se dává suffix Delegate
- občas mi jde čistě o parametry – je mi úplně jedno, co ten delegát dělá
- bylo by zbytečné pro každou takovou metodu deklarovat delegáta
- proto existuje spousta předdefinovaných delegátů
Action<…>(…) → void
Func<…, TResult>(…) → TResult
Predicate<T>(T obj) → bool
- pozor na přehlednost – typicky je lepší používat silně typované delegáty
var
funguje i pro delegáty
var d1 = new D(m);
- od C# 8 natural types
var d1 = m;
- C# ví o delegátech ve standardní knihovně
- vybere se nejlepší match z Action, Func
- efektivita delegátů
- výroba delegáta něco stojí
- volání delegáta vyjde téměř nastejno
- delegát má vlastnost Method
- vrací MethodInfo
- používá to reflection
- je to celkem drahé
- Invoke v reflection je výrazně pomalejší
- vlastnost Method se cachuje
- na MethodInfo je metoda CreateDelegate, abychom nemuseli používat Invoke z reflection – pak se dá volat efektivněji
- reflection přiřazení do statických fieldů je taky pomalé
- JSON serializer
- ten v System.Text.Json serializuje veřejné vlastnosti
- dá se aktivovat serializace privátních fieldů
- metoda Deserialize
- návratový typ odpovídá očekávanému typu kořene
- v JSONu objekty nejsou typované
- při deserializaci se používá duck typing
- může se to hodit k deep cloningu objektů
- na každém objectu je protected metoda MemberwiseClone, která vrací mělkou kopii
- když serializujeme nějaký objekt a pak ho deserializujeme, tak vlastně dostaneme hlubokou kopii (pokud to uděláme dobře)
- tady se může hodit serializace privátných fieldů
- defaultní je serializace veřejného kontraktu, aby nemohl záškodník aplikaci rozbít úpravami JSONu (pokud máme validaci v setterech)
- coroutine
- iterátorová metoda je příkladem coroutiny
- umožňuje nám popsat algoritmus v krocích
- např. v nějaké hře by pomocí kroků coroutine mohly být popsané kroky NPCčka
- když serializujeme objekt enumerátoru (včetně privátních fieldů), tak máme serializovaný stav
- ale je to závislé na implementačních detailech
Lambda funkce
- zpátky s motivačnímu příkladu
- máme metodu FindIndex, která bere delegáta (predikát)
- pokud predikát vrátí true, tak to vrátí ten index
- někdy není moc praktické mít pomocné metody vypsané jako statické ve třídě
- chtěli bychom je deklarovat v místě kódu, kde se používají
- obvykle se tomu říká lambda funkce, my je budeme chápat jako anonymní funkce (mají nějaké jméno vygenerované C# překladačem)
- napíšeme
static
, pak do závorky parametry, pak šipku =>
a potom tělo funkce
- dá se přímo zapsat jako parametr metody FindIndex
- pozor na šipku – u normálních (pojmenovaných) funkcí je to syntaktický cukr
- proč je tam klíčové slovo static?
- označuje, že lambda funkce nemá stav
- dříve se dalo použít slovo delegate – funguje to podobně (ale chybí tomu nějaké další funkce, které lambda funkce mají)
- ekvivalentní příklady lambda funkcí
static (int x) => { return x + 1; }
static (int x) => x + 1
static x => x + 1
static x => { return x + 1; }
- v posledních dvou příkladech se použije type inference
- pokud je parametrů více, musejí tam být vždycky kulaté závorky
- lambda funkce může být bez parametrů, pak se napíšou prázdné kulaté závorky
- lambda funkce nejsou first-class entities – neexistuje proměnná typu „lambda funkce“
- jsou přiřaditelné do proměnných typu delegát (a ještě někam jinam – viz ET přednáška)
- podle delegáta, kam to přiřazuju, funguje type inference parametrů
- když někam přiřadím úplně stejnou lambda funkci, tak C# překladač může recyklovat vygenerovaný kód
- může se stát, že voláme generickou metodu a předáváme ji jako parametr lambda funkci, takže překladač musí řešit trochu složitější rovnici
- když to nevede na jednoznačné řešení, tak je to překladová chyba
- když tam to static nedáme, tak se zapíná nějaká speciální funkce, ale pokud ji nepoužíváme, tak je to jedno
- to static tam píšeme jenom proto, abychom tu speciální fíčuru nepoužili omylem
- pozor, když nějakou lambda funkci používám často a rozmyslím se, že je užitečná, tak může dávat smysl mít ji tam jako klasickou statickou metodu
- kde lambda funkce použít?
- chceme nad kolekcí dat provést transformaci
- tohle se dělá často v relačních databázích
- databázová tabulka je vlastně kolekce řádků – chceme najít nějakou podmnožinu, setřídit to apod.
- tyhle věci se typicky popisují pomocí SQL queries
LINQ
- language integrated queries
- nějakou syntaxí podobnou SQL dotazům bych mohl popsat, co se má stát
- jak je to doopravdy?
- v C# je LINQ pouze syntaxe, nedefinuje to žádnou sémantiku
- zatímco SQL dotazy mají i sémantiku
- syntax LINQ dotazů
from
x in
data source, pak nějaké klauzule
- můžu si to představit jako nějaký foreach (ale takhle to nefunguje – je to jenom představa)
- klauzule má dvě části – nějaké klíčové slovo a kód
- klíčové slovo se přeloží na volání vhodné metody, která má jako parametr lambda funkci
- kód se předá jako tělo té lambda funkce
- parametr lambda funkce je to X
- má to výjimku – když to končí klauzulí select, která vrací jenom X, tak se ta klauzule nepřekládá
from c in customers where c.City == "London" select c.Name
se přeloží na customers.Where(c => c.City == "London").Select(c => c.Name)
- C# definuje jenom přeložení téhle syntaxe, to je celé
- pokud se ta „C# syntaxe“ přeloží, tak je LINQ dotaz syntakticky správně, pokud se nepřeloží, tak je špatně, to je vše
- to X (respektive v příkladu je to
c
) nemá žádný důležitý význam, jenom nám to umožňuje odkazovat se na parametr lambda funkce
- pokud je těch funkcí víc, tak si můžeme představit, že ta jednotlivá X spolu vůbec nesouvisí
- dotaz musí začínat from, vždycky tam musí být in a vždycky musí končit selectem – pokud vrací rovnou X, tak se to ignoruje
- celý ten LINQový výraz má nějakou hodnotu – ta odpovídá návratové hodnotě posledního výrazu (může to být cokoliv – jakýkoliv typ)
- fakt tam není sémantika
- můžu mít metodu OrderBy, která zabíjí příšery a vrací počet těch, které jsme zabili
- Select funguje podobně jako všechny ostatní klauzule – až na tu výjimku s ignorováním „prázdného“ selectu
- LINQ funguje na principu duck typingu
- může se stát, že nějaký typ nepodporuje určitou klauzuli – podpora se dá dodat extension metodami
- v C# je definován základní LINQ to Objects
- statická třída Enumerable – tam je přehršel generických extension metod, které extendují
IEnumerable<T>
- ta třída je definovaná v
System.Linq
- dělá to ty operace, co bychom čekali
- pozor, extension metoda je jenom fallback
- kdybychom měli typ, který implementuje
IEnumerable<T>
, ale byla by tam např. metoda Select, tak bude mít přednost před tou LINQovou
- myšlenka LINQ to Objects – zavolání metod má jenom připravit dotaz, nemá se to reálně dotazovat
- když zavolám Where, tak z ní vypadne krabička, kde je delegát a datový zdroj
- ta krabička se jmenuje třeba WhereEnumerable
- WhereEnumerable taky implementuje všechny metody z LINQu
- ale tady už se nepoužívají extension metody
- pokud zavolám Where na WhereEnumerable, tak si to nové WhereEnumerable bude jako zdroj pamatovat to původní WhereEnumerable
- výsledek LINQ to Objects dotazu vytvoří vázaný seznam těch krabiček
- všechny ty krabičky implementují rozhraní IEnumerable
- můžeme na tom udělat foreach cyklus
- při inicializaci dostaneme enumerátor poslední krabičky
- první volání MoveNext
- vygeneruje enumerátory všech krabiček v tom spojáku
- zkontroluje to platnost predikátů u Where klauzulí
- volá to MoveNext na enumerátorech, dokud nejsou predikáty splněny
- LINQ to Objects provádí líné vyhodnocení (lazy evaluation) dotazu
- ten dotaz nemusím enumerovat celý
- jeden dotaz můžu spouštět vícekrát (od začátku)
- dotazy můžu skládat
- pozor, když mám proměnnou se seznamem, nad kterou postavím LINQ krabičky, a do této proměnné uložím jiný seznam, tak se dotaz nepřegeneruje – má v sobě uložený odkaz na entitu, ne na proměnnou
- lazy evaluace se používá, pokud je to možné
- pro některé operátory se dělá eager evaluace
- Where … lazy
- OrderBy … eager
- ale to se týká jenom LINQ to Objects, neplatí to univerzálně
- u eager evaluace se data musí někam uložit
- C# překladač nedělá žádné optimalizace – je potřeba přemýšlet nad pořadím klauzulí
- where → orderBy vs. orderBy → where
- nejspíš bude efektivnější ta první varianta (pokud where a orderBy fungují tak, jak bychom čekali)
- poznámka – LINQ to Objects dělá nějaké optimalizace
- pokud máme dvě Where za sebou, tak se to sloučí do jedné krabičky, která má seznam podmínek
- podobně Where a Select krabičky se můžou sloučit
- ale nemění to fungování
Lambda funkce se stavem
- dosud nám k vyhodnocení funkce stačily její parametry
- co kdybychom uvnitř funkce chtěli používat nějaké další proměnné?
- když nepoužívám lambda funkce, ale předávám delegáta na instanční metodu – uvnitř delegáta se uloží (odkaz na) this
- lambda funkci si můžeme představit jako funkci, která žije v nějakém kontextu
- parametry – deklarace lokálních proměnných
- tělo – deklarace nějakých proměnných a použití nějakých proměnných
- použité proměnné můžou být 1. vázané (tzn. někde uvnitř funkce jsou deklarované) nebo 2. volné (nejsou deklarované uvnitř funkce)
- některé jazyky umožňují mít funkci s volnými proměnnými jako first-class entity – ty se pak dodefinují před použitím
static
zakazuje volné proměnné
- za překladu musí být jasné, co ty volné proměnné znamenají (v daném kontextu) – převádějí se na vázané
- lambda funkce bez volných proměnných = closure
- v C# vlastně lambda funkce neexistují, jsou tam jen lambda výrazy a do delegátů se vždy přiřazují closures
- potřebovali bychom nějakým trikem dostat do lambda funkce přiřazené hodnoty těch volných proměnných
- vezmeme nějaký její scope
- v tom scopu zachytíme volné proměnné (Scope si můžeme představit jako nějakou třídu/objekt, kde ta lambda funkce je jeho instanční metoda)
- podobný princip jako u iterátorových metod
- v C++ to takhle funguje, ale v C# ne
- syntax lambda funkcí v C++
- do hranatých závorek se dá napsat
=
, to znamená capture by value
- capture by value má zjevně nevýhody v mnoha situacích
- pokud v lambda funkci něco spočítáme, nemůžeme to dostat ven
- C++ umožňuje capture by reference, kdy se do hranatých závorek dá
&
- všechny fieldy ve scopu budou reference na nějaké místo
- tohle v C# nejde, protože tracking reference mají omezení kvůli živnotnosti
- v C# se neprovádí capture by value ani by reference
- budeme tomu říkat „capture by move“
- proměnná se „přesune“ do toho scopu
- když lambda funkce použije nějakou proměnnou z kontextu, změní se způsob překladu veškerého kódu za lambda funkcí (v daném kontextu)
- veškerá další práce s danou proměnnou se přepíše na přístup k proměnné uvnitř scopu – jako by to byla veřejná vlastnost (nebo field) nějakého objektu Scope
- C# překladač opravdu vyrábí instanci nějaké třídy Scope
- všechny lambda funkce v daném kontextu sdílejí stejnou instanci scopu
- jedna instance scopu tedy vlastně odpovídá jednomu volání rodičovské funkce
- do delegátu se předává „this“ ukazující na konkrétní instanci scopu
- ty pomocné třídy se v C# nejmenují Scope, ale DisplayClass
- tohle se generuje C# překladačem na úrovni CIL kódu, takže JIT o lambda funkcích ani closure nic neví
Enumerable.Range(1, 10)
vrátí lazy enumerátor čísel od 1 do 10
- metoda
.ForEach(Action<T>)
- respektive
Array.ForEach(T[], Action<T>)
pro pole
- scope
- vzniká ne kvůli lambda funkci, ale kvůli proměnným v ní
- třídě scopu se říká DisplayClass
- jeden scope odpovídá jedné úrovni životnosti proměnných
- mějme proměnné a, i, j ve vnějším kontextu
- mějme proměnnou k ve vnitřním kontextu
- mějme lambda funkci ve vnitřním kontextu, ta používá proměnné i, j, k
- vznikne jedna instance DisplayClass, která bude obsahovat proměnné i, j, a druhá instance, která bude obsahovat k
- proměnné se neukládají podle názvu – takže pokud mám uvnitř for cyklu nějakou proměnnou
int a
, tak ta se bere v každé iteraci samostatně
- DisplayClass s nejkratší životností
- ukazuje na ty nadřazené s delší životností
- jako metodu obsahuje tělo lambda funkce (?)
- víc lambda funkcí
- na generování DisplayClasses se nic nemění
- pozor na zachytávání proměnných v lambda funkci – abychom něco nezachytili omylem
- když ve VS najedu na šipečku lambda funkce, tak se zobrazí zachycené proměnné
- častý bug – myslím si, že se používá capture by value (ale hodnota se před voláním lambda funkce změní)
- „capture by accident“
Vícevláknové programování
- vlákno = posloupnost zavolání funkcí
- proces v C#
- entrypoint
- CLR-entrypoint
- JIT
- Main()
- chceme provést dva algoritmy (A a B)
- můžeme je pravidelně střídat, provádět je po malých kouscích
- může se zdát, že to běží současně, přestože to běží concurrent
- lidský mozek průměruje svět za 100 ms, takže kdyby se to střídalo takhle rychle, tak by se nám zdálo, že to běží současně
- proč bychom to vůbec chtěli
- jak funguje železnice
- koleje, po kterých jezdí vlaky
- na nich jsou odbočky – výhybky mají směr + a –
- můžeme počítat nápravy, které vjedou do nějakého úseku (nebo z něj vyjedou), abychom mohli určit volnost nějaké výhybky (říká se tomu počítač náprav)
- přestavení výhybky nějakou chvíli trvá
- návěstidla („semafory“) říkají strojvedoucím, v jakém je úsek stavu (na silnici světla nefungují stoprocentně, na železnici je to navržené bezpečně)
- zelená = můžeš jet, nic ti nehrozí
- červená = rozhodně nejezdi
- železniční zabezpečovací zařízení (stavidlo, signalbox) implementuje tuhletu logiku, určuje, jaké světlo má svítit
- myšlenka – výpravčí staví cestu (route) pro konkrétní vlak
- jdeme implementovat stavidlo
- jednotka práce stavidla je stavba jedné cesty, výrobci tomu říkají „driver“
- chtěli bychom spustit dva drivery současně
- stačilo by nám to concurrent – občas se čeká, až se výhybka dostane do koncového stavu
- cooperative (kooperativně přepínaná vlákna)
- coroutine – rozdělení algoritmu na kroky
- výhody
- máme kontrolu nad tím, co se kdy dělá
- coroutine říká, kde jeden krok končí
- problémy
- aktivní čekání – furt to něco dělá, žereme procesorový čas daný pro jedno vlákno
- metoda Thread.Sleep(čas v ms) zařídí pasivní čekání vlákna … pasivní čekání (parametr s časem odpovídá minimálnímu času čekání, může to být i víc)
- nemůžu využít další procesorová jádra
- třída Thread
- ve jmenném prostoru System.Threading
- defaultně každá instance Thread reprezentuje jedno vlákno (ale může to být i nějak jinak)
- preemtivně přepínaná vlákna
- mezi dvěma libovolnými instrukcemi strojového kódu může dojít k přepnutí
- statická vlastnost Thread.CurrentThread
- na instanci vlákna
- ManagedThreadId … dotnetí identifikátor konkrétního vlákna (liší se od identifikátoru, který používá operační systém)
- konstruktor
- jeden overload – bere delegáta ThreadStart
- druhý overload – bere delegáta ParametrizedThreadStart(object o)
- v tom místě to vlákno nevznikne
- na té instanci se dají nastavit různé vlastnosti
- Priority
- IsBackground (defaultně false)
- celý proces (běh aplikace) skončí, až skončí všechna foreground vlákna (v takovém případě CLR zabije všechna background vlákna)
- až voláním Start() vznikne vlákno, zařadí se do plánovací fronty OS a jakmile se uvolní procesor, začne běžet
- dva overloady Start() a Start(object o) odpovídají dvěma overloadům konstruktoru
- context switch
- u kooperativního přepínání vláken – overhead je malý, jenom se vrátím z MoveNext jednoho algoritmu a pustím MoveNext druhého algoritmu
- u preemptivního přepínání vláken – procesor se musí přepnout z uživatelského do supervisor režimu
- IRQ timer → CPU (supervisor) → kernel → plánovač → Yield
- tisíce taktů procesoru
- je potřeba rozmyslet, jestli to dává smysl
- stav vlákna
- Unstarted
- Running (něco jako Running) … neznamená to Running v OS
- může být v OS stavu ready-to-run
- "Ended"
- vlastnost IsAlive – když je false, tak vlákno doběhlo
- v OS to odpovídá stavu terminated
- čekáme na doběhnutí vlákna
- nedává smysl napsat
while (t.IsAlive)
… vyrobili jsme aktivní čekání
- mohli bychom použít semiaktivní čekání pomocí
Thread.Sleep
- vlákno přejde do OS stavu waiting / dotnet stavu WaitSleep
- mohlo by se stát, že nás někdo předběhne a budeme čekat hrozně dlouho
- trik – Thread.Sleep(0)
- vzdáme se procesoru, rovnou se zařadíme do fronty
- v C# se dá použít Thread.Yield(), je to to samé
- ale budeme žrát hodně času procesoru – kvůli context switchi
- správné řešení …
t.Join()
- jakmile vlákno
t
doběhne, tak se naše vlákno rozeběhne dál
- datové struktury, které vlákno používá, nejsou celou dobu v konzistentním stavu
- nechceme, aby jiné vlákno mohlo vidět vadný stav nějaké datové struktury
- tohle se v kooperativním přepínání řeší snadno, prostě do té nekonzistentní oblasti nedáme yield returny
- v preemptivním přepínání to není jednoduché, vede to k race conditions
- můžeme zjistit, jestli datová struktura je thread-safe (nikdy není vidět rozbitý stav)
- datové struktury v dotnetu typicky nejsou thread-safe
- vlákna běží v jednom procesu
- každé vlákno má vlastní volací zásobník
- všechna vlákna sdílí jednu haldu
- problémy
- context switch je hrozně drahý
- vyrobení vlákna je ještě dražší
- v dotnetu je třída ThreadPool
- vyrobí si nějaké rozumné množství vláken (worker threads), které pasivně čekají
- je tam nějaký pool položek (workitems)
- na začátku je prázdný
- pomocí QueueUserWorkitem se tam dá přidat úloha
- jako parametr bere delegáta WaitCallback s parametrem
object state
- není to pool, ale fronta
- měli bychom to používat spíš na malé položky
- když uvnitř vlákna použijeme Wait/Join, tak to bude zabírat vlákno a nebude ho moct použít jiný úkol
- ale s tím se taky trochu počítá – počet vláken v poolu není konstantní
- API threadpoolu je hodně základní, ale naštěstí nad ním existuje spoustu dalších API
- metoda QueueUserWorkitem je asynchronní, hned se vrátí
- ale často by se nám hodilo, abychom pokračovali až ve chvíli, kdy ta úložka skončí
- můžeme použít třídu Parallel, kde je spousta užitečných metod – v základu fungují jako synchronní
- metody Invoke, For a ForEach
- For a ForEach nenarvou do threadpoolu všechno najednou, ale nějak rozumně to dávkují
Futures & promises
- příklad s hamburgery
- je neefektivní začít stavět fastfood, až když mám hamburgery
- použijeme koncept futures (viz studijní materiál Thud!/Buch!)
- výrobce hamburgerů koupí budoucí vepřové, chovatel prasat koupí budoucí kukuřici apod.
- v dotnetu koncept future reprezentovaný třídou
Task<T>
T
… kukuřice
Task<T>
… budoucí kukuřice
- readonly vlastnost Result – nejprve zablokuje vlákno pomocí Wait (pokud není k dispozici výsledek), pak vrátí Result, jakmile je dostupný
- vlastnost IsCompleted
- když chceme vytvořit metodu, která vrací budoucí kukuřici, tak za její jméno dáme Async
Task<T>
je referenčního typu, může být null, pokud nelze požadavek splnit (když se future ani nevytvoří)
Task<T>
je potomek třídy Task
Task
si můžeme představit jako Task<void>
– i to je užitečné (příklad s traktoristou, který orá pole, chceme zjistit, jestli má hotovo)
- jak paralelizovat sčítání výsledků dvou drahých metod
- varianta 0: obě pustíme synchronně
- varianta 1: jednu z nich pustíme asynchronně, druhou synchronně
- varianta 2: obě pusíme asynchronně, sčítáme jejich
Result
s
- varianta 3: použijeme Task.WaitAll, abychom zabránili zbytečným context switchům ve variantě 2
- další metoda – Task.WaitAny
- jak získat
Task<T>
Task.FromResult<T>
vytvoří hotový Task
- koncept promise
- třída TaskCompletionSource
- metoda SetResult (striktně writeonly)
- ke každému TaskCompletionSource existuje Task (ve vlastnosti Task)
- defaultování futures (když slib nedokážu naplnit)
- vlastnost IsCanceled (jedno ℓ, protože americká angličtina) na Tasku
- IsCompleted je pak taky true, ale existuje vlastnost IsCompletedSuccessfully
- metoda SetCanceled na TaskCompletionSource
- Task.Result na defaultnuté future vyvolá OperationCancelledException uvnitř AggregateException (ve vlastnosti InnerExceptions)
- co se stane, když se z delegáta v threadpoolu vyšíří výjimka
- nestane se nic, úložka je jakoby v try-catch bloku
- když máme úložku, o které víme, že může spadnout, tak ji chceme mít v try bloku a v catchi popsat, co se má stát (chceme zachytávat Exception – tedy libovolné výjimky)
- kromě IsCanceled může být future taky ve stavu IsFaulted, k tomu je metoda SetException (tu můžeme zavolat právě z toho catch bloku)
- struktura CancellationToken
- cooperative cancelling
- abychom mohli operaci zrušit v nějakém rozumném místě (nechceme, abychom vytváření prasat zrušili ve chvíli, kdy nějakému praseti chybí noha)
- readonly vlastnost IsCancellationRequested
- přerušitelné async metody berou cancellation token jako parametr
- je dobré ho předávat podřízeným metodám
- je dobré ho kontrolovat hned na začátku metody
- je potřeba ho kontrolovat ve všech místech, kde se dá metoda přerušit
- přerušená metoda by neměla vrátit null, ale cancelled future
- ale cancellation token se předává hodnotou – jak můžeme proces zrušit?
- je tam třída CancellationTokenSource
- funguje jako promise cancel requestu
- vlastnost Token vrací CancellationToken
- struktura CancellationToken obsahuje private referenci na CancellationTokenSource
- na instanci CancellationTokenSource můžeme zavolat metodu Cancel
- tenhle přístup striktně odděluje readonly CancellationToken od writeonly CancellationTokenSource
Thread safety
- máme dvě vlákna, která používají jednu datovou strukturu
- část, kdy nějaké vlákno pracuje se sdílenou datovou strukturou = kritická sekce
- chceme zabránit race condition
- nejjednoduší přístup – mutual exclusion
- použijeme zámky
- v dotnetu – syncblock
- to je jakoby ten zámek
- každý objekt na GC haldě má v overheadu referenci na Type a referenci na syncblock
- syncblock má dvě půlky: lock + „něco“
- lock (zámek)
- je tam reference na vlákno, které drží ten zámek
- kvůli rekurzivnímu zamykání je tam počítadlo, kolikrát to vlákno ten zámek zamklo
- je tam fronta čekajících vláken, které čekají na odemčení
- jak zámek zamknout
- je tam třída
Monitor.Enter(object o)
- při vytvoření objektu syncblock vůbec neexistuje, na haldě je tam null
- při prvním zamčení objektu se vytvoří syncblock a zamkne se (atomicky)
- když to samé vlákno zavolá Monitor.Enter, tak se zvýší Count
- když jiné vlákno udělá Monitor.Enter, tak se zablokuje pasivním čekáním
- jak zámek odemknout
Monitor.Exit(object o)
- sníží se Count a pokud je nula, zámek se odemkne
- pokud někdo čeká, předá se objekt čekajícímu vláknu (to si ho zamkne)
- pokud nikdo nečeká, tak se reference na syncblock nastaví na null a syncblock se zařadí do poolu volných syncblocků, aby se daly použít u jiných objektů
- pozor na zamykání hodnotových typů – Monitor.Exit ho typicky zaboxuje do jiného typu, než jsme zamkli
- můžeme použít syntaktickou zkratku
lock (object o) { … }
- začátek složených závorek odpovídá Enteru, konec Exitu
- kdybychom chtěli zamknout A, zamknout B, odemknout A a odemknout B, tak tuhle syntaxi použít nelze
- zamyká to objekt, ne proměnnou
- když v kritické sekci (v lock bloku) do proměnné přiřadíme nový objekt, tak se to může rozbít
- lock blok bude i v takové situaci fungovat správně – odemkne se zamčený objekt
- lock blok má Exit jakoby ve finally – odemkne se, i když se vyšíří výjimka
- někdy to ale není to, co chceme – datovou strukturu typicky zamykáme, protože během zamčení není v konzistentním stavu
- pokud bychom použili prostou kombinaci Monitor.Enter a Monitor.Exit bez finally, vyšířením výjimky mezi těmito dvěma příkazy by mohl nastat deadlock, pokud by stejný objekt chtěl zamknout někdo jiný
- deadlocku si zákazník hned všimne, protože se mu aplikace zasekne
- naopak nekonzistentního stavu datové struktury si všimnout nemusí
- musíme zvážit, co je pro nás vhodnější
- co když chceme zamknout proměnnou
- vytvoříme si pomocnou proměnnou
object dataLock = new object();
- to je ta „esence zámku“
- pak si s proměnnou
data
můžeme dělat, co chceme – třeba do ní přiřadit nový objekt
Asynchronní metody
- asynchronní metoda (vol. 1)
- na konci názvu bude typicky mít
Async
- vrací „future“ …
Task<T>
- ten má Result, Exception, Status
- hotovou future lze vytvořit pomocí
.From
- můžeme vrátit „promise“ …
TaskCompletionSource<T>
- někdy by se nám hodilo něco, co by zahrnovalo dohromady future i promise
- mělo by to delegáta na odpovídající úlohu, která vrací
T
- tohle se dá udělat právě tak, že napíšu
new Task<T>(úloha)
- na tasku je metoda
Start
, která ho zafrontí do thread poolu
- jako další parametr konstruktoru Tasku můžeme uvést cancellation token, pak Task umí detekovat, když se z něj vyšíří OperationCanceledException pro daný token a nastaví se na Canceled
- užitečná věc: na cancellation tokenu existuje metoda ThrowIfCancellationRequested
- když chci vytvořený Task rovnou pustit, napíšu
Task.Factory.StartNew(úloha)
- chci znát aktuální Task
t = Task.Factory.StartNew(() => { … t … });
- tohle se někdy může rozbít –
t
může být null
- je tam race condition – může se stát, že se úloha nejdřív zařadí do thread poolu a než se provede přiřazení do
t
, tak dojde ke kontext switchi → v t
bude null
t = new Task(() => { … t … }); t.Start();
Task.Run(úloha)
- pustí task v thread poolu
- pomocí StartNew nebo Start se úloha nemusí pustit v thread poolu (pokud to nenakonfiguruju pomocí parametrů) – více viz poslední přednáška
- příklad s velkými a malými úlohami v thread poolu
- pokud uvnitř velké úlohy budeme plánovat malou úlohu do thread poolu a takových velkých úloh budeme mít 100, tak může dojít k thread pool starvation – úlohy se začnou vykonávat až při 101 vláknech
- řešením může být použití třídy Parallel
- nebo můžeme použít metodu ContinueWith
- voláním
Task t2<int> = t1.ContinueWith(prevTask => { … });
navěsíme task2 za task1
- metoda
Task.Factory.ContinueWhenAll
- dá se vyrobit future, která bude completed nejdříve po určitém čase …
Task.Delay(ms)
- dá se vyrobit future, která bude completed, jakmile budou všechny zadané tasky splněné …
Task.WhenAll
- podobná je
Task.WhenAny
– když bude aspoň jeden naplněný
- na dokončení tasku můžeme počkat pomocí Wait (nebo WaitAll, WaitAny)
- lokální metody
- uvnitř metody můžu definovat metodu
- chová se to jako lambda funkce
- variable capture (hodnoty proměnných) závisí na místě použití
- více v ET přednášce
- cíl: napsat kód ve formátu „něco rychlého, něco pomalého, něco rychlého, něco pomalého, něco rychlého, …“, který se bude vykonávat postupně, ale nebude blokovat vlákno
- můžu použít iterátorové metody na plánování asynchronních korutin
- ale to by bylo hodně programování
- v C# na to existují „asynchronní metody“ (vol. 2) s klíčovým slovem
async
async
async Task<T> LoremIpsumAsync(…) { … }
- jednotlivé kroky asynchronní metody jsou reprezentovány klíčovým slovem
await
await
funguje podobně jako yield return
- opět se konstruuje stavový automat
- když jsou v metodě dva řádky s await, tak se rozdělí do 3 kroků – před prvním await (včetně toho řádku s await), mezi prvním a druhým await a za druhým await
- první krok se vždycky provádí synchronně ve volající metodě
- tam můžeme provést kontrolu, třeba že parametry jsou v pořádku
- ale neměli bychom tam provádět nic náročného – pokud nechceme blokovat volající metodu
yield break
… konec korutiny
- paralelním konceptem v
async
metodě je return
- za
await
se píše objekt typu Task<U>
- ten se vrátí plánovacímu kódu, aby věděl, kdy má naplánovat další krok
- await vrací
U
- za
return
se píše objekt typu T
- to
return
tam fakt někde musí být, abychom věděli, co má být výsledek
- i lambda funkce může být asynchronní
- pozor na šíření výjimek
- když se vyšíří výjimka z
async
metody f2Async
- nastaví se na faulted
- stejně se to chová, i když se výjimka vyšíří ze synchronního (prvního) kroku
- kdybychom tam chtěli vyšířit klasickou výjimku, můžeme přidat mezikrok – klasickou asynchronní metodu
- co když f2Async voláme z metody f1Async, aniž bychom četli výsledný Task?
- nedozvíme se, že se vyšířila výjimka
- await unwrapuje výjimky, respektive vezme první z nich a vyšíří ji ven
- pozor na kombinaci zámku a awaitů
- překladač nám zakáže psát
await
dovnitř lock
bloku
Monitor.Enter
a Monitor.Exit
selže výjimkou
- protože Exit voláme z jiného vlákna než Enter
- jestli to fakt potřebujeme, tak musíme použít jiné synchronizační primitivum
- semafor – má dvě implementace
- Semaphore
- potomek třídy WaitHandle
- žádní potomci WaitHandle nejsou určeni k použití jako základní synchronizační primitivum, trvají hrozně dlouho
- je to meziprocesové komunikační primitivum
- SemaphoreSlim
- ten chceme používat
- má stejné vlastnosti jako syncblock
- na začátku definujeme initial count (povolený počet zamčení)
- vlastnost CurrentCount, metody Wait a Release
- když je počítadlo na nule, tak metoda Wait zablokuje volajícího
- jinak dekrementuje CurrentCount
- Release inkrementuje CurrentCount
- dá se nastavit i maximum count, abychom semafor neodemkli mockrát
- není to omezené na vlákna
- SemaphoreSlim(1) je něco jako zámek (s nerekurzivním zamykáním), akorát pro víc vláken (u zámku ho musí odemknout stejné vlákno)
- je tam i metoda WaitAsync, která vrací Task označující, zda se to podařilo zamknout
- další synchronizační primitiva
- ReaderWriterLockSlim
- readeři se navzájem nevylučují, writeři ano
- přístup může mít v dané chvíli buď libovolně mnoho readerů, nebo jeden writer
- CountdownEvent
- něco jako semafor naopak
- čeká se, dokud není hodnota nulová
Monitor, threading model
- promise, future
- příklad: v jednom vlákně dáme task.Wait(), to vlákno se zablokuje
- SetResult musíme provést v jiné vlákně
- pak chceme opět pustit to zablokované vlákno
- syncblock se skládá ze dvou částí
- lock
- owner thread
- + counter (kvůli rekurzivnímu zamykání)
- waiting threads list – nějaký férový seznam (nebo fronta, na tom nesejde)
- condition variable
- poznámka: zámku a podmínkové proměnné se dohromady říká monitor
- třída Monitor
- Monitor.Enter a Monitor.Exit obsluhují zámek
- Monitor.Wait přidá vlákno do seznamu spících vláken – blokující volání
- Monitor.Pulse probudí jedno ze seznamu spících vláken (nebo žádné, pokud je prázdný) – neblokující volání
- Monitor.PulseAll probudí všechna vlákna
- aby se dal použít Wait, Pulse nebo PulseAll, musí být obalený v lock bloku (pro stejný objekt) – jinak to vyhodí výjimku
- kdyby to fungovalo přesně takhle, tak by tam byl deadlock
- takže se Monitor.Wait chová trochu komplikovaně
- nejprve to za mě udělá Monitor.Exit
- pak nějaký InternalWait
- tyhle dva kroky se dějí atomicky
- pak se provede normální Monitor.Enter … blokující volání
- proč musím držet ten zámek (proč se vynucuje)
- mám nějakou podmínku
- třeba boolovskou proměnnou
- chceme vědět, kdy bude proměnná true
- pokud neplatí podmínka, tak chceme čekat, až ji někdo splní
- v jiném vlákně nastavíme proměnnou na true a dáme Monitor.Pulse
- byla by tam race condition – proto tam musí být zámek
- bez Monitor.Exit by tam byl deadlock
- bez atomičnosti by se to mohlo rozbít
- u Monitor.Pulse se zámek vynucuje kvůli tomu, že by mezi nastavením podmínky na true a Pulsem mohl někdo nastavit podmínku na false
- ale ani takhle to ještě úplně nefunguje
- místo
if (!condition)
tam musíme mít while (!condition)
- ilustrace použití – problém producent/konzument (producer/consumer)
- producent ukládá data do datové struktury
- konzument data bere z datové struktury
- dá se to snadno ladit – když je produkce rychlejší než konzumace, můžeme přidat konzumenty (nebo naopak)
- jednoduchá situace
- datová struktura … fronta
- máme jednoho producenta a jednoho konzumenta
- budeme používat Wait a Pulse
- je důležité říct si v hlavě, co je ta podmínka
- to je typická chyba
- je fajn to explicitně napsat do komentáře
- např. podmínka je „fronta je neprázdná“
- je dobré, aby pro konkrétní instanci objektu existovala jedna podmínka (aby po celou dobu existence znamenala to samé)
- v producentovi – po přidání položky dáme Monitor.Pulse(queue)
- v konzumentovi –
while (queue.Count == 0) Monitor.Wait(queue);
- dáme tam poison pill, abychom vlákna mohli ukončit, na konci programu vlákna vzbudíme všechna pomocí PulseAll
- můžu použít i jiný objekt k zamykání
- nechali jsme ChatGPT implementovat QueueWithConditions
- příklad použití má po
queue.WaitUntilNotEmpty();
řádek s queue.Dequeue();
- to vede k race condition
- o podmínkových proměnných je potřeba dobře přemýšlet – jestli to nevede na race conditions nebo deadlocky
- situace – funkce si chtějí mezi sebou předávat informaci, když běží ve stejném vlákně
- můžeme použít
Thread.CurrentThread.ManagedThreadID
a mít slovník – ale to má nějaké nedostatky
- tohle řeší operační systém – má thread local storage (TLS)
- dotnet na to má wrapper
- statickou proměnnou můžu označit atributem ThreadStatic
- na každém místě, kde z ní čteme, se vygeneruje volání ReadTLS
- kdekoliv do ní zapisujeme se vygeneruje WriteTLS
- ale nestačí
static int a = 42;
- protože se to dá do statického konstruktoru, který se volá právě jednou z hlavního vlákna
- takže pro ostatní vlákna tam bude nula
- existuje třída ThreadLocal, kterou k tomu můžu použít
- field nemusí být statický, ale to obvykle není dobrý nápad
- pozor, jsou to drahé zdroje
- různé frameworky definují threading model
- pravidla, jak se mám k třídám chovat z více vláken
- frameworky WinForms, WPF, MAUI, Avalonia a Uno mají podobný threading model
- UI thread s nekonečnou smyčkou
- když vznikne událost, tak se zařadí do fronty
- vyřešení události se provede synchronně v UI threadu
- nikdy se nemůže stát, že by běžely dvě události současně
- přístup k prvkům uživatelskému rozhraní musí být z UI threadu
- veškeré dlouhotrvající operace (nad 100 ms) bych měl dělat v jiném vlákně, aby nezamrzlo UI
- jak dostat informaci o dokončení události do UI
- v C# je koncept synchronizačního kontextu … abstraktní třída SynchronizationContext
- každý framework si implementuje vlastní
- metoda Post(úloha) … asynchronní
- metoda Send(úloha) … synchronní, blokující
- zachytíme (capture) sync context
- chceme progress bar
- krok těžkého zpracování → synchronizace → další krok → …
- co když použijeme async metodu
- vadilo by nám, kdyby se tasky plánovaly do thread poolu – blokovalo by nás to
- Task.Start, Task.Factory.StartNew, ContinueWith a await se ptají na aktuální sync context – pokud není žádný, tak použijou thread pool
- takže pokud tam je synchronizační kontext, tak se použije Post/Send
- naopak Task.Run vždycky použije thread pool
- když je tam těžkotonážní kód, který nechceme pustit konkurentně, ale opravdu paralelně, tak můžu napsat
await Task.Run
- metoda
SetSynchronizationContext
- metoda
ConfigureAwait(bool continueOnCapturedContext)
- ruší zachytávání synchronizačního kontextu, vynucuje threadpool
- defaultní chování je s
true
parametrem
- když tam dáme false, tak se ty věci budou volat vůči threadpoolu
- nejtypičtější rada na internetu „dej tam ConfigureAwait(false)“
- ale je důležité vědět, co chceme udělat
- můžeme takhle část kódu vytáhnout do vlákna threadpoolu a zbytek nechat v synchronizačním kontextu