it-swarm-eu.dev

Kdy má být klíčové slovo volatile použito v C #?

Může někdo poskytnout dobré vysvětlení klíčového slova volatile v jazyce C #? Které problémy to řeší a které ne? V jakých případech mi to zachrání použití zamykání?

279
Doron Yaacoby

Pokud chcete získat o něco více technické informace o tom, co dělá volatile klíčové slovo, zvažte následující program (používám DevStudio 2005):

#include <iostream>
void main()
{
  int j = 0;
  for (int i = 0 ; i < 100 ; ++i)
  {
    j += i;
  }
  for (volatile int i = 0 ; i < 100 ; ++i)
  {
    j += i;
  }
  std::cout << j;
}

Pomocí standardního optimalizovaného nastavení (release) kompilátor vytvoří následující assembler (IA32):

void main()
{
00401000  Push        ecx  
  int j = 0;
00401001  xor         ecx,ecx 
  for (int i = 0 ; i < 100 ; ++i)
00401003  xor         eax,eax 
00401005  mov         edx,1 
0040100A  lea         ebx,[ebx] 
  {
    j += i;
00401010  add         ecx,eax 
00401012  add         eax,edx 
00401014  cmp         eax,64h 
00401017  jl          main+10h (401010h) 
  }
  for (volatile int i = 0 ; i < 100 ; ++i)
00401019  mov         dword ptr [esp],0 
00401020  mov         eax,dword ptr [esp] 
00401023  cmp         eax,64h 
00401026  jge         main+3Eh (40103Eh) 
00401028  jmp         main+30h (401030h) 
0040102A  lea         ebx,[ebx] 
  {
    j += i;
00401030  add         ecx,dword ptr [esp] 
00401033  add         dword ptr [esp],edx 
00401036  mov         eax,dword ptr [esp] 
00401039  cmp         eax,64h 
0040103C  jl          main+30h (401030h) 
  }
  std::cout << j;
0040103E  Push        ecx  
0040103F  mov         ecx,dword ptr [__imp_std::cout (40203Ch)] 
00401045  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (402038h)] 
}
0040104B  xor         eax,eax 
0040104D  pop         ecx  
0040104E  ret              

Při pohledu na výstup se kompilátor rozhodl použít registr ecx k uložení hodnoty proměnné j. Pro energeticky nezávislou smyčku (první) kompilátor přiřadil i do registru eax. Poměrně jednoduchá. Existuje však několik zajímavých bitů - instrukce lea ebx, [ebx] je efektivně instrukce vícebajtové nop, takže smyčka přeskočí na 16 bajtovou adresu s vyrovnanou pamětí. Druhou možností je použití edxu pro zvýšení čítače smyčky namísto použití instrukce inc eax. Instrukce add reg, reg má nižší latenci na několika jádrech IA32 ve srovnání s instrukcí inc reg, ale nikdy nemá vyšší latenci. 

Nyní pro smyčku s těkavým počitadlem smyčky. Čítač je uložen v [esp] a klíčové slovo volatile říká kompilátoru, že hodnota by měla být vždy čtena z/zapsána do paměti a nikdy nebyla přiřazena registru. Kompilátor dokonce jde tak daleko, že neprovede zatížení/přírůstek/úložiště jako tři odlišné kroky (zatížení eax, inc eax, uložení eax) při aktualizaci hodnoty čítače, namísto toho je paměť přímo modifikována v jediné instrukci (doplnění paměti). , reg). Způsob, jakým byl kód vytvořen, zajišťuje, že hodnota čítače smyčky je vždy aktuální v kontextu jednoho jádra CPU. Žádná operace na datech nemůže mít za následek poškození nebo ztrátu dat (tedy nepoužívání load/inc/store, protože hodnota se může měnit během inc, takže je ztracena v úložišti). Vzhledem k tomu, že přerušení mohou být obsluhována pouze po dokončení aktuální instrukce, data nemohou být nikdy poškozena, a to ani s neupravenou pamětí.

Jakmile do systému zavedete druhý procesor, klíčové slovo volatile nebude chránit před daty, které jsou současně aktualizovány jiným procesorem. Ve výše uvedeném příkladu budete potřebovat data, která mají být nezměněna, abyste získali potenciální poškození. Klíčové slovo volatile nezabrání potenciálnímu poškození, pokud data nelze zpracovávat atomicky, například pokud byl čítač smyček typu dlouho dlouhý (64 bitů), pak by vyžadovalo dvě 32bitové operace pro aktualizaci hodnoty uprostřed. může dojít k přerušení a změnit data.

Klíčové slovo volatile je tedy dobré pouze pro zarovnaná data, která jsou menší nebo rovná velikosti nativních registrů, takže operace jsou vždy atomové.

Klíčové slovo volatile bylo koncipováno tak, aby mohlo být používáno s operacemi IO, kde by se hodnota IO neustále měnila, ale měla konstantní adresu, například zařízení s mapou paměti UART a kompilátor by neměl být opakované použití první hodnoty načtené z adresy.

Pokud pracujete s velkými daty nebo máte více procesorů, budete potřebovat uzamykatelný systém vyšší úrovně (OS) pro řádné zpracování dat.

54
Skizz

Pokud používáte rozhraní .NET 1.1, je klíčové slovo volatile nutné při zamykání s dvojitou kontrolou. Proč? Protože před .NET 2.0, následující scénář může způsobit druhý podproces přístup k null, ale není plně zkonstruován objekt:\t

  1. Vlákno 1 se zeptá, zda je proměnná null. // if (this.foo == null)
  2. Vlákno 1 určuje, že proměnná je null, takže vstupuje do zámku. // lock (this.bar)
  3. Vlákno 1 se zeptá, jestli je proměnná null. // if (this.foo == null)
  4. Vlákno 1 stále určuje, že proměnná je null, takže zavolá konstruktor a přiřadí hodnotu proměnné. // this.foo = new Foo ();

Prior .NET 2.0, this.foo mohl být přiřazen nový instanci Foo, dříve než byl konstruktor dokončen. V tomto případě by mohlo dojít k druhému vláknu (během volání podprocesu 1 konstruktoru Foo) a zažít následující:

  1. Vlákno 2 se zeptá, zda je proměnná null. // if (this.foo == null)
  2. Vlákno 2 určuje, že proměnná není Null, takže se ji snaží použít. // this.foo.MakeFoo ()

Před .NET 2.0, můžete deklarovat this.foo jako volatilní dostat kolem tohoto problému. Vzhledem k tomu, že .NET 2.0, již nemusíte použít klíčové slovo volatile k provedení dvojitého zaškrtnutí.

Wikipedia vlastně má dobrý článek o Double Checked Locking a stručně se dotýká tohoto tématu: http://en.wikipedia.org/wiki/Double-checked_locking

38
AndrewTek

Od MSDN : Prchavý modifikátor se obvykle používá pro pole, ke kterému přistupuje více podprocesů bez použití příkazu lock k serializaci přístupu. Použití volatilního modifikátoru zajišťuje, že jedno vlákno načte nejaktuálnější hodnotu zapsanou jiným vláknem.

22
Dr. Bob

Někdy kompilátor optimalizuje pole a použije jej k uložení. Pokud vlákno 1 zapisuje do pole a přistupuje k němu jiný podproces, protože aktualizace byla uložena v registru (a nikoli v paměti), druhé vlákno by získalo zastaralé údaje.

Můžete si myslet, že volatilní klíčové slovo říká kompilátoru "Chci, abyste tuto hodnotu uložili do paměti". To zaručuje, že druhý podproces načte nejnovější hodnotu.

20
Benoit

CLR rád optimalizuje instrukce, takže když přistupujete k poli v kódu, nemusí se vždy dostat k aktuální hodnotě pole (může to být ze zásobníku atd.). Označení pole jako volatile zajišťuje, že aktuální hodnota pole je přístupná instrukcí. To je užitečné, když hodnotu lze upravit (v scénáři bez uzamčení) souběžným vláknem v programu nebo nějakým jiným kódem spuštěným v operačním systému.

Zřejmě ztratíte nějakou optimalizaci, ale kód je jednodušší.

13
Joseph Daigle

Kompilátor někdy změní pořadí příkazů v kódu, aby jej optimalizoval. Obvykle se nejedná o problém v prostředí s jedním podprocesem, ale může to být problém v prostředí s více podprocesy. Viz následující příklad:

 private static int _flag = 0;
 private static int _value = 0;

 var t1 = Task.Run(() =>
 {
     _value = 10; /* compiler could switch these lines */
     _flag = 5;
 });

 var t2 = Task.Run(() =>
 {
     if (_flag == 5)
     {
         Console.WriteLine("Value: {0}", _value);
     }
 });

Pokud spustíte t1 a t2, očekávali byste jako výsledek žádný výstup nebo hodnotu „Hodnota: 10“. Může se stát, že kompilátor přepne linku uvnitř funkce t1. Pokud pak t2 provede, může to být, že _flag má hodnotu 5, ale _value má 0. Takže očekávaná logika může být přerušena. 

Chcete-li to opravit, můžete použít volatile keyword, které můžete použít na pole. Tento příkaz zakáže optimalizace kompilátoru, takže můžete vynutit správné pořadí v kódu.

private static volatile int _flag = 0;

Měli byste použít volatile pouze pokud to opravdu potřebujete, protože zakáže určité optimalizace kompilátoru, bude to bolet výkon. Není také podporován všemi .NET jazyky (Visual Basic nepodporuje), takže brání jazykové interoperabilitě.

0
Aliaksei Maniuk

Abychom to shrnuli, správná odpověď na otázku je: Pokud je váš kód spuštěn v runtime 2.0 nebo novějším, je volatilní klíčové slovo téměř nikdy nepotřebné a pokud je zbytečně používáno, dělá více škody než užitku. TJ. Nikdy ji nepoužívejte. BUT v dřívějších verzích runtime, to IS potřebné pro správné dvojité kontroly zamykání na statických polích. Konkrétně statická pole, jejichž třída má inicializační kód statické třídy.

0
Paul Easter