Skippov blog

Pre Vas. Pre mna. Pre dalsie generacie.

Singletony, HttpContext a ThreadStatic alebo čo je v skutočnosti private-local v ASP.NET

Ahoj všetci. Nakoľko je toto môj prvý post na tomto blogu, rád by som na úvod napísal pár vecí o mne a o tom, čomu sa chcem venovať. Programujem už nejaký ten rôčik, mal som tú česť pracovať na širokej škále projektov, od windows a web aplikácii, cez skriptovacie jazyky, až po komplikovanejšie paralelné aplikácie na Linuxovom klastri. S ASP.NET robím pomerne krátko, takže moje príspevky profíkom zrejme veľa nedajú, no ďalšie generácie slovenských, a možno aj českých programátorov, tu možno objavia stratégiu bojov, ktoré sme zvádzali snáď všetci. A v neposledom rade možno svojou troškou podporím slovenskú a českú .NET komunitu. Myslím, že som konečne objavil oblasť (.NET, webové aplikácie), ktorej sa chcem dlhodobo venovať. V práci sa venujem prevažne užívateľskej strane projektov, moje príspevky sa budú týkať oblastí ako Ajax, JavaScript, CSS, ASP.NET a bezpečnosť. Opakujem prevažne. :-) V rámci toho mála voľného času sa zaoberám grafikou a tvorbou oldschool hier. Ak má niekto podobné hobby, ozvite sa, rád si pokecám a vymením skúsenosti. A teraz späť k téme.

Singleton je už klasik medzi návrhovými vzormi (design patterns). Jednoducho povedané, termín singleton reprezentuje triedu, ktorá umožní vytvoriť práve a iba jednu jej inštanciu. Zvyčajne k jej metódam a vlastnostiam pristupujeme veľmi jednoducho, a to cez statickú vlastnosť (property), ktorá vráti inštanciu triedy, ak je vytvorená. V opačnom prípade ju vytvorí a vráti inštanciu. Spomínaná vlastnosť garantuje maximálne jednú inštanciu triedy. Načo je nám taký singleton dobrý? V prvom rade umožňuje tzv. lazy instantiation, čo voľne preložené znamená, že inštancia triedy sa vytvorí až v momente, ked budeme k triede pristupovať (ak vôbec). Ďalej oproti static metódam a properties je singleton trieda thread-safe, t.j. volanie metód sa nám nezgulášuje. Tu nie som celkom presný, singleton môže byť implementovaný ako not-thread-safe, takejto implementácii by sme sa mali vyhnúť.

Príklad je lepší ako sto slov, preto ukážka jednoduchého thread-safe singletonu:

public class MojSingleton
{
    private static MojSingleton m_oInstance=null;
    private static readonly object padlock = new object();

    private MojSingleton ()
    {
    }

    public static MojSingleton oInstance
    {
        get
        {
            lock (padlock)
            {
                if (m_oInstance==null)
                {
                    m_oInstance = new MojSingleton();
                }
                return m_oInstance;
            }
        }
    }
}

Pre tých, ktorí budú ohŕňať nos nad zamykaním obsahu gettera, napíšem len to, že je to rovnaký problém ako klasická snaha programátorov o optimalizáciu použitia premenných alebo volania funkcie, ktorá je v dobe dnešných kompilátorov zanedbateľná. Toto v 99% nie je úzke miesto programu (bottleneck) a osobne by som sa pozrel na volanie databáz, ajax callbackov resp. dynamickú úpravu obsahu napríklad javascriptom.

Takáto implementácia je fajn, ale čo v prípade ASP.Net, kde nám každý request obsluhuje jeden z niekoľkých threadov poolu a my nechceme zdieľať pre všetky requesty jeden singleton? Šikovný pán Roman Macháček ponúka modifikované riešenie:

public class Singleton
{
    private Singleton()  {}

    public static Singleton Instance
    {
        get 
        {
            if (HttpContext.Current.Session["Singleton"]==null)
                HttpContext.Current.Session["Singleton"]=new Singleton();
            return (Singleton)HttpContext.Current.Session["Singleton"];
        }
    }
}

Niektoré moje singletony zgrupujú iba metódy a volanie konštruktora je zanedbateľné, preto sa mi ukladanie singletonu do objektu Session velmi nepozdávala. V prípade, že by som v triede ukladal nejaké privátne dáta a potreboval ich držať pre daný request počas celej session, hore uvedená implementácia by mi úplne vyhovovala. Avšak ako každý programátor, v túžbe optimalizovať moju aplikáciu som začal hľadať riešenie, ako vytvárať singleton na úrovni requestu. T.j. po zavolaní Request_End funkcie by sa singleton pekne a čisto odpratal. Pokiaľ možno automaticky. Pri svojom pátraní som sa dostal k parametru [ThreadStatic]. Aplikácia tohto parametra na static field spôsobí, že thready si nebudú field zdieľať, ale každý thread bude mať svojú vlastnú inštanciu threadu. (Poznámka na okraj: ako by ste do slovenčiny preložili termín field?). Vyzerá to dobre, stačí ak modifikujeme prvý príklad singletonu a sme za vodou. Teraz sa priznám, že som to neskúšal a neviem povedať, aké situácie by bolo treba riešiť. Uvádzam preto ešte jeden príklad singletonu, ktorý je thread-safe, ale bez uzamykania. Túto verziu budú preferovať odporcovia príkazu lock :-) A ostatní, ktorí sa s touto implementáciou ešte nestretli, sa niečo nové dozvedia. Pokial volanie konštruktora nie je časovo náročné a nespôsobí nejaké vedľajšie účinky, vo veľa prípadoch si vystačíte nie celkom lazy singletonom:

public class MojSingleton
{
    [ThreadStatic] 
    private static readonly MojSingleton m_oInstance=new MojSingleton();

    // Explicit static constructor to tell C# compiler
    // not to mark type as beforefieldinit
    private static MojSingleton()
    {
    }

    private MojSingleton()
    {
    }

    public static MojSingleton oInstance
    {
        get
        {
            return m_oInstance;
        }
    }
}

[ThreadStatic] je fajn, ale nefunguje tak, ako by ste na prvý pohľad očakávali. Dá sa použiť iba v niektorých situáciach. Vysvetlím. Keď som sa dozvedel o tomto attribúte, už od začiatku ma sprevádzala akási nedôvera. Static fieldom veľmi neverím, preto aj tie pochybnosti ohľadom už toľkokrát spomínaného atribútu. Skúmal som teda ďalej, aké sú reálne možnosti jeho použitia.

“Aj ked si myslíš, že vieš čo robíš, NIE je bezpečné ukladať čokoľvek v ThreadStatic fielde, CallContext-te alebo Thread Local Storage v prípade ASP.NET aplikácie, ak je nejaká šanca, že hodnota bude nastavená pred volaním Page_Load (napríklad v IHttpModule alebo konštruktore stránky) a pristupovať sa k nej bude počas nej alebo neskôr” Piers7.

Piers vo svojom blogu dobre rozanalyzoval otázky okolo spracovania requestu ASP.Net-tom. Problém s ThreadStatic atribútom spočíva v tom, že ASP.NET nie je iba thread-pooled ale aj thread agile (-: Opäť otázka pre náruživého čitateľa: Ako preložiť posledné slovné spojenie do rodnej materčiny? :-) Zjednodušene povedané, ASP.NET každému requestu pridelí nejaký thread z poolu. Tých treadov je tam štandardne cca 25 (?). Tu je kameň úrazu, pretože neplatí tvrdenie: request je obsluhovaný threadom od jeho začiatku do konca. To anglické thread agile znamená, že ASP.NET môže (a robí) prepnúť thready počas spracovávania requestu a dokáže obslúžiť viac requestov jedným threadom. A teda jeden request može byť spracovaný viacerými threadmi. Nemáme kontrolu nad Thread Pool a životným cyklom threadov, preto atribút [ThreadStatic] nefunguje ako privátny a lokálny v rámci requestu, ale ako privátny a lokálny medzi všetkými threadmi poolu. Ak vieme kontrolovať thready, potom aj správanie atribútu je korektné, to ale neplatí pre ASP.NET.

Pri vysokej záťaži ASP.NET a rôznej časovej náročnosti spracovávaných funkcií sa môže stať, že príliš veľa I/O threadov spracováva requesty. ASP.NET potom zastaví spracovávanie requestu, odloží ho niekam do fronty, a príslušný thread bude pracovať na ďalšom requeste. Časom je request opať na rade a priradí sa mu nejaký dostupný worker thread. (poznámka: do hĺbky spracovania requestov I/O threadmi nevidím, možno by niekto v diskusii mohol ozrejmiť a lepšie konkretizovať túto časť a spojenie I/O threadov a worker threadov).

Ale späť k téme. Pri migrovaní requestu medzi threadmi nepremigruje všetko, ako by si niekto mohol myslieť, ale iba HttpContext. Dáta uložené napríklad v CallContext sú fuč. Keď už je nám známe migrovanie threadov, pri použití [ThreadStatic] nastáva jeden základný problém: ThreadStatic fieldy sú privátne a lokálne zdieľané medzi threadmi (t.j. vždy iba jeden z threadov pristupuje k dátam). Pri migrovaní sa môže ľahko stať, že iný thread dostane dáta niekoho iného. Riešením je použitie niečoho, čo je naozaj privátne a lokálne, ale vzhľadom na request. Odpoveď už zaznela - je to HttpContext. Kontext migruje medzi threadmi aj so všetkými dátami.

Veľmi pekným a odporúčaným miestom pre ukladanie dát v rámci jedného HTTP requestu je HttpContext.Current.Items. Tento objekt implementuje interfejs IDictionary, a teda ponúka elegantný prístup k dátam vo forme kľúč – hodnota. Pekné miesto na uloženie singletonov. A poďme rovno na príklad, jemne modifikovaný singleton č.2:

public class MojSingleton
{
      private MojSingleton ()
      {
      }

      private static readonly object padlock = new object();

      private static readonly string CONTEXT_ITEMS_KEY = "__MojSingleton";

      public static MojSingleton oInstance
      {
          get
          {
              lock (padlock)
              {
                  if (HttpContext.Current.Items[CONTEXT_ITEMS_KEY] == null)
                      HttpContext.Current.Items[CONTEXT_ITEMS_KEY] = new MojSingleton ();
                  return (MojSingleton)HttpContext.Current.Items[CONTEXT_ITEMS_KEY];
              }
          }
      }
}

Výhody a nevýhody. Singleton je naozaj thread-safe, dáta prežijú počas celého requestu a po ukončení je inštancia pekne odprataná z pamäte -- automaticky. Ostatné controly môžu využívať funkcie a dáta inštancie triedy a inštancia sa vytvára až vtedy, ak je potrebná. Ak potrebujete uložiť dáta medzi requestami, použite objekt Session, vid. príklad č.2. Alebo ak volanie konštruktora je drahé a uložené miesto v Session naopak. Nevýhodou je, že nižšie vrstvy vašej aplikácie musia referencovat namespace System.Web, aby mohli pristupovať k property HttpContext.Current. V prípade väčších a zložitejších aplikácií to nemusí byť dobré a žiadúce, ak napríklad plánujete neskôr použiť spodné vrstvy aplikácie pre winforms alebo console projekty. Veľa programátorov potom pristupuje priamo k threadu a ukladá dáta na nejaké storage miesto spojené s threadom, v snahe vyhnúť sa úzkej napojenosti na webové prostredie. Tento prístup však nebude fungovat v prípade ASP.NET, veriť možno iba HttpContext-tu.

A čo s [ThreadStatic] ? Nie je to nepoužiteľný atribút, avšak v ASP.NET aplikáciach by sa mal používať opatrne. Nezabudnúť, že request može byť spracovaný rôznymi threadmi počas svojho krátkeho života. Preto ukladanie dát v takomto fielde je zlým riešením. Ak však trieda, singleton, obsahuje iba metódy a žiaden stav (dáta), bude jeho použitie bezproblémové. (-: až do chvíle, keď zabudneme na ThreadStatic a pridáme do triedy nejaké data :-)

Uvítam, ak do diskusie prispejete aj svojimi poznatkami, prípadne dodatočným vysvetlením. Ak na niektorom mieste píšem nesprávne, napíšte mi a text opravím, resp. doplním. Vďaka a do threadovania.

Update: tento (jediný) príspevok som uverejnil na svojom predchádzajúcom blogu, no nakoľko je server už dlhú dobu mimo provoz, rozhodol som sa presunúť na tento portál, kde už nejaký ten čas pôsobím (pozdravujem fórum). Dúfam, že to tu vydrží dlhšie :-)

Bookmark and Share

Komentáre

T povedal:

Pribuzna tema ku clanku, ktory mam rozpisany :-) nahoda. Este raz si to musim v klude precitat a pozamyslat sa ... Ako si tam vlozil ten blok kody so scrolovatakmi? Si to editoval ako html?

P.S. Neboj urcite vydrzi, ved to uz nejakych par (5?) drzi.

# June 15, 2007 10:48 AM

skippo povedal:

Ano, vsetko pisem v HTML, ja wysiwyg editory dost nemusim. Preto ma prekvapilo, ked aj HTML source konvertuje do svojho 'nepekneho' formatovania. Tazko sa potom edituju veci.. spigi vedel by si s tym nieco poriesit? Nech do HTML zdrojakov system nesaha?

Co sa tyka formatovania zdrojakov, musim to este premysliet, chcem ich mat vyfarbene (skusim najst nejaky nastroj co produkuje pekne HTML, pripadne nakodim sam:-)

Kvoli absolutnej sirke odstavca a preformatovanemu textu som tam hodil scrolovatka, jednoducho cez DIV a CSS overflow:auto (hidden, prida obe a ja som chcel len ked je to potrebne).

# June 15, 2007 11:36 AM

vlko povedal:

s tymi zdrojakmi ti mozem pomoct:

www.jtleigh.com/.../CopySourceAsHtml

a vsimol som si jednej veci:

v kode mas pouzite

private static readonly string CONTEXT_ITEMS_KEY = "__MojSingleton";

nebolo by lepsie pouzit const?

private static const string CONTEXT_ITEMS_KEY = "__MojSingleton";

najma preto, ze sa tato hodnota asi nebude menit, neovplyvni ostatne kniznice, lebo je private

nieco o porovnani readonly a const:

blog.vyvojar.cz/.../c-const-nebo-readonly.aspx

a najma neda sa hodnota menit cez reflection

blog.vyvojar.cz/.../7317.aspx

# June 15, 2007 12:56 PM

skippo povedal:

vlko, diky za link na CSAH, na prvy pohlad to vyzera supr, bud. tyzden sa s tym skusim pohrat, teraz uz bezim domov.

Co sa tyka toho readonly, pouzil som to presne kvoli tomu, o com Tomas Havetta pise - nedam za to ruku do ohna :-)

# June 15, 2007 4:24 PM

spigi povedal:

Pani, vydrzi to tu :) Prave som prisiel domov po 40 hodinach bez spanku.. ozvem sa vam a poriesime veci na blogu.

Ja na svojom blogu pouzivam iba tento wysiwyg editor a zatial nebol ziaden problem (myslim blog na vyvojar.cz)

# June 15, 2007 10:19 PM

Slune povedal:

2 poznamky:

1) To co hledas neni Singleton, tak mi ten nazev prijde kapku zavedejici :)

2) Ten prvni priklad se da udelat i bez toho lock {} proste takhle:

private static readonly MojSingleton oInstance = new MojSingleton();

.NET ti garantuje, ze to bude nakrmeny jenom jednou a az pri prvnim hrabnuti na tridu, takze taky lazy. Sice mas pravdu, ze je lepsi hledat "bottlenecky" v DB nez tady, ale nekdy to muze byt taky pekny zabijak vykonu.

# June 22, 2007 2:51 PM

skippo povedal:

Slune, dik za komenty.

Co sa tyka 2), tak tvoj pristup je ok (dokonca by si oInstance mohol dat public a mas o property menej), ale ak trieda obsahuje ine static metody, tak sa ti to zinstacuje skor ako chces -- no ako obaja suhlasime, toto nie je bottleneck :-) cus.

# June 25, 2007 9:16 AM