Body zdarma Java týden
Využij podzimních slev a získej od nás až 40 % bodů zdarma! Více zde
Pouze tento týden sleva až 80 % na Java e-learning!

Diskuze: Použití volatile

Aktivity (2)
Avatar
xmms
Člen
Avatar
xmms:20. září 23:49

Snažím se pochopit, kdy používat volatile. Na stránkách https://barrgroup.com/…tile-Keyword popisují Multithreaded Applications.

Zkusil jsem: Zkoušel jsem tenhle kus kódu zkompilovat v Microsoft Visual C++ a taky překladačem g++ s různými nastaveními optimalizace i bez nich. Funguje to vždycky správně dle očekávání. Ale prý by to tak správně nemělo fungovat, když se zapnou optimalizace a proměnná gn_bluetask_runs není volatile?

g++.exe test.cpp -static -O
g++.exe test.cpp -static -O2
g++.exe test.cpp -static -O3
g++.exe test.cpp -static -Ofast

#include <thread>
#include <Windows.h>

using namespace std;

//neni volatile
uint8_t gn_bluetask_runs = 0;

void red_task(void)
{
        while (4 > gn_bluetask_runs)
        {
                printf("gn_bluetask_runs jeste neni 4\n");
                Sleep(60);
        }
}

void blue_task(void)
{
        for (;;)
        {

                gn_bluetask_runs++;
                printf("gn_bluetask_runs %d:\n", gn_bluetask_runs);
                Sleep(500);
        }
}

int main()
{

        thread t1(blue_task);
        thread t2(red_task);

        t1.join();
        t2.join();
        return 0;
}

Chci docílit: Kdy se tedy v multithreadových programech skutečně musí používat volatile a kdy ne?

 
Odpovědět 20. září 23:49
Avatar
DarkCoder
Člen
Avatar
Odpovídá na xmms
DarkCoder:21. září 2:32

Globální proměnná gn_bluetask_runs je použita v obou vláknech. Pokud by tato proměnná nebyla deklarována jako volatile, mohl by překladač optimalizovat kód tak, že by nebyla vždy zjišťována skutečná hodnota proměnné. Jednoduše, pokud překladač neví, že se obsah proměnné může měnit způsobem, který není v programu explicitně specifikován, nemusí pracovat s obsahem proměnné při každém jejím použití. Použitím slova volatile v deklaraci proměnné říkáme překladači, že k obsahu proměnné může přistupovat ještě nějaký jiný proces, než ten, na kterém kód aktuálně běží a zabraňuje tak překladači použít optimalizaci. Může to být hardware, hardwarové přerušení nebo třeba souběžně běžící vlákno jako v tomto případě.

Ve výše uvedeném případě bez použití volatile a zapnuté optimalizaci nebude překladač zjišťovat hodnotu proměnné gn_bluetask_runs a vlákno red_task poběží do nekonečna. Pokud by bylo doplněno klíčové slovo volatile k deklaraci proměnné, bude překladač vždy kontrolovat obsah proměnné gn_bluetask_runs při jejím užití, podmínka ve vláknu red_task se po čase stane nepravdivou a vlákno skončí.

Nahoru Odpovědět  +1 21. září 2:32
"„Učíš-li se proto, aby sis zapamatoval, zapomeneš. Učíš-li se proto, abys porozuměl, zapamatuješ si."
Avatar
xmms
Člen
Avatar
Odpovídá na DarkCoder
xmms:21. září 12:27

Ale to vlákno red_task neběží do nekončena. V mém případě vypadá výstup programu jako na obrázku. Můžeš uvést nějaký konkrétní příklad kompilace kompilace v g++ nebo visual studiu, ve kterém se projeví, že vlákno red_task poběží do nekonečna?

gn_bluetask_runs 1:
gn_bluetask_runs jeste neni 4
gn_bluetask_runs jeste neni 4
gn_bluetask_runs jeste neni 4
gn_bluetask_runs jeste neni 4
gn_bluetask_runs jeste neni 4
gn_bluetask_runs jeste neni 4
gn_bluetask_runs jeste neni 4
gn_bluetask_runs jeste neni 4
gn_bluetask_runs 2:
gn_bluetask_runs jeste neni 4
gn_bluetask_runs jeste neni 4
gn_bluetask_runs jeste neni 4
gn_bluetask_runs jeste neni 4
gn_bluetask_runs jeste neni 4
gn_bluetask_runs jeste neni 4
gn_bluetask_runs jeste neni 4
gn_bluetask_runs 3:
gn_bluetask_runs jeste neni 4
gn_bluetask_runs jeste neni 4
gn_bluetask_runs jeste neni 4
gn_bluetask_runs jeste neni 4
gn_bluetask_runs jeste neni 4
gn_bluetask_runs jeste neni 4
gn_bluetask_runs jeste neni 4
gn_bluetask_runs jeste neni 4
gn_bluetask_runs 4:
gn_bluetask_runs 5:
gn_bluetask_runs 6:
gn_bluetask_runs 7:

 
Nahoru Odpovědět 21. září 12:27
Avatar
Martin Dráb
Redaktor
Avatar
Odpovídá na xmms
Martin Dráb:21. září 15:57

Tvoje zkušenost nic neříká o tom, že řešení bez použití volatile je správně. Překladač přístupy k dané proměnné optimalizovat může, ale také nemusí. Kdysi se mi stalo, že jsem měl proměnnou

bool x = false;

a v prvním vlákně bylo

while (!x) {
 ...
}

a v druhém někde v kódu

x = true;

Jelikož x nebyla volatile, překladač se rozhodl, že ji ani nebude umisťovat do paměti, ale hodil ji přímo do registrů. Což ve výsledku znamenalo, že první vlákno běželo donekonečna (nastavení x na true se do jeho registru samozřejmě nemohlo promítnout).

Už si nepamatuju, zda ta x byla globální, nebo třeba jen součástí nějaké datové struktury předané tomu vláknu.

Obecně lze říci, že když nebudeš správně používat volatile, tak ti to bude často fungovat (zejména na x86/x64, protože tam se snadno pracuje s pamětí), ale občas narazíš na záhadné chování (často náhodně se vyskytující) a po několikadenním vyšetřování zjistíš, že jsi měl někde použít volatile.

Nahoru Odpovědět  +1 21. září 15:57
2 + 2 = 5 for extremely large values of 2
Avatar
DarkCoder
Člen
Avatar
Odpovídá na xmms
DarkCoder:21. září 17:10

Jak píše Martin, překladač přístupy k dané proměnné optimalizovat může, ale také nemusí.
Podívej na následující trochu upravený příklad:

#include <iostream>
#include <thread>
#include <Windows.h>

BOOL x = false;

void red(void){
        while (!x) {
                // Sleep(100);
        };
        std::cout << "Konec vlakna red\n";
}

void blue(void){
        Sleep(1000);
        x = true;
        std::cout << "x = 1\n";
        Sleep(1000);
        std::cout << "Konec vlakna blue\n";
}

int main(){
        std::thread t1(red);
        std::thread t2(blue);

        t1.join();
        t2.join();
        return 0;
}

Pokud optimalizace bude Zakázáno (/Od):

Zobrazí se:

x = 1
Konec vlakna red
Konec vlakna blue

nebo

Konec vlakna red
x = 1
Konec vlakna blue

Zde je jedno zda proměnná bude deklarována jako volatile či nikoli, optimalizace se neprovádí.

Pokud optimalizace bude Preferovat rychlost (/Ox) a proměnná nebude volatile:

Zobrazí se:

x = 1
Konec vlakna blue

V tomto případě překladač provede optimalizaci cyklu while na:

while(1){}

Kód vlákna red zůstane v nekonečné smyčce, proto se text (Konec vlakna red) nezobrazí.

Pokud optimalizace bude Preferovat rychlost (/Ox) a proměnná bude volatile:

Zobrazí se:

x = 1
Konec vlakna red
Konec vlakna blue

nebo

Konec vlakna red
x = 1
Konec vlakna blue

V tomto případě překladač optimalizaci neprovede ikdyž je nastavena a bude si při každém použití proměnné x získávat její hodnotu.

Pokud ale v cyklu while odstraním komentář, příkaz Sleep(100); nabyde v platnost, překladač neoptimalizuje cyklus while narozdíl od prázdného cyklu a zobrazí se obsah:

Zobrazí se:

x = 1
Konec vlakna red
Konec vlakna blue

nebo

Konec vlakna red
x = 1
Konec vlakna blue

A to u proměnné deklarované jako volatile nebo bez ní.

Je tedy na překladači jak se rozhodne optimalizovat tvůj program. Pak pokud nebudeš deklarovat proměnnou jako volatile, bude tvůj program někdy pracovat správně, jindy nikoli.

Jinak ještě doplním, že od C++11 není deklarace proměnné jako volatile vhodná pro vícevláknové aplikace. Používá se zejména pro komunikaci s hardwarem. Pro vícevláknové aplikace se zde užívají mutexy a atomic, ale to už je jiný příběh..

Nahoru Odpovědět  +1 21. září 17:10
"„Učíš-li se proto, aby sis zapamatoval, zapomeneš. Učíš-li se proto, abys porozuměl, zapamatuješ si."
Avatar
xmms
Člen
Avatar
Odpovídá na Martin Dráb
xmms:21. září 17:26

OK, chápu. Zkoušel jsem tu proměnnou předělat na bool a taky se mi to neprojevilo.
Ještě by mě zajímalo, jestli se musí tohle napsat i v parametrech funkcí, viz můj příklad.
Jsou funkce fce1, fce2, fce3 korektní nebo to nemusí správně fungovat?

#include <thread>
#include <Windows.h>

using namespace std;

volatile int ggg;
int ppp;


void fce1(int f)
{
        printf("fce() %d\n", f);
}

void fce2(volatile int f)
{
        printf("fce() %d\n", f);
}

void fce3(volatile int f)
{
        printf("fce() %d\n", f);
}

int main()
{
        ggg=10;
        ppp=20;
        thread t1(fce1, ggg);
        thread t2(fce2, ppp);
        thread t3(fce3, ggg);

        t1.join();
        t2.join();
        t3.join();

        system("pause");
        return 0;
}
 
Nahoru Odpovědět 21. září 17:26
Avatar
Martin Dráb
Redaktor
Avatar
Odpovídá na xmms
Martin Dráb:21. září 17:37

I v parametrech funkcí může dávat volatile smysl, protože tím opět omezíš optimalizace překladače. Mně se to osvědčilo v případě, kdy jsem na vytvořenou binárku ještě pouštěl program, který ji dál upravoval (měnil nějaké konstanty a tak), takže jsem potřeboval, aby konstanty nepropagoval do volání funkcí.

K propagování dochází, pokud překladač usoudí, že daná proměnná bude mít v daném okamžiku danou hodnotu. Obvykle se děje na konstanty. Například pokud máš globální proměnnou

static const int x = 4;

a někde pak uděláš

printf("%d", x);

překladač z toho může přímo udělat

printf("%d", 4);

Ale to je hodně speciální případ. Jinak jsem snad nikdy volatile k parametrům psát nemusel.

volatile jen překladači říká, že přístup k dané proměnné nemá nijak optimalizovat (tzn. vždy se bude jednat o přístup do paměti), nic víc.

Nahoru Odpovědět 21. září 17:37
2 + 2 = 5 for extremely large values of 2
Avatar
xmms
Člen
Avatar
Odpovídá na Martin Dráb
xmms:21. září 18:10

A proč nějaká binárka mění konstanty, k čemu se to používá? Proč to jsou konstanty, když je potřebuješ měnit?

 
Nahoru Odpovědět 21. září 18:10
Avatar
Martin Dráb
Redaktor
Avatar
Odpovídá na xmms
Martin Dráb:21. září 18:19
  • obfuskace/šifrování binárky (např. za účelem snížení její velikosti či kvůli ochraně proti crackování),
  • binárka má natvrdo nastavené nějaké konstanty, které by se ti hodily změnit pro tvé použití (např. má natvrdo nastavené absolutní cesty a ty bys potřeboval relativní apod.); tady tedy obvykle není k dispozici zdroják, takže musíš doufat, že překladač případně moc neoptimalizoval :-).
Nahoru Odpovědět 21. září 18:19
2 + 2 = 5 for extremely large values of 2
Avatar
xmms
Člen
Avatar
xmms:21. září 22:19

Zajímavé, díky. Ty děláš cracking? Na obfuskaci jsem udělal nové vlákno.

 
Nahoru Odpovědět 21. září 22:19
Tento výukový obsah pomáhají rozvíjet následující firmy, které dost možná hledají právě tebe!
Avatar
Martin Dráb
Redaktor
Avatar
Odpovídá na xmms
Martin Dráb:22. září 11:17

Zajímavé, díky. Ty děláš cracking? Na obfuskaci jsem udělal nové vlákno.

Nějakou dobu jsem se o to zajímal.

Nahoru Odpovědět 22. září 11:17
2 + 2 = 5 for extremely large values of 2
Avatar
xmms
Člen
Avatar
Odpovídá na DarkCoder
xmms:8. října 15:40

A když dám proměnnou do mutexu, tak potom nemusím použít volatile a bude to fungovat spolehlivě? Třeba když potřebuju změnit několik proměnných najednou, aby byly dostupné pro ostatní vlákna až po nastavení všech.

Editováno 8. října 15:42
 
Nahoru Odpovědět 8. října 15:40
Avatar
DarkCoder
Člen
Avatar
Odpovídá na xmms
DarkCoder:8. října 20:12

Ano, bude. Mutex je jedním z prostředků jak synchronizovat vlákna. Těmi dalšími jsou podmínkové proměnné a semafory. Jestli se bude jednat o jednu nebo více proměnných, je jedno. Smyslem je zajistit aby se k proměnné přistupovalo vždy a pouze jen z jednoho vlákna.

A protože se téma týká klíčového slova volatile, podívej se na následující příklad, jak to může být důležité.

#include <iostream>
#include <thread>

int x;
int y = 0;

void red(void) {
        while (!y) x = 0;
}

void blue(void) {
        while (!y) x = 1;
}

void green(void) {
        int count = 0;
        while (count < 1000) {
                std::cout << x;
                count++;
        }
        y = 1;
}

int main() {
        std::thread t1(red);
        std::thread t2(blue);
        std::thread t3(green);

        t1.join();
        t2.join();
        t3.join();
        return 0;
}

Pokud překladač bude optimalizovat, pak tento program nemusí dělat to co od něj očekáváme. Aby program pracoval správně, je třeba, aby obě proměnné x a y byly volatile. x proto, aby se střídaly její hodnoty dle nastavení uvnitř vláken red a blue. A y proto, aby se vlákna red a blue ukončila a ukončil se tak i celý program. Všimni si, jak nastavení proměnné y uvnitř vlákna green je signálem aby se vlákna red a blue ukončila.

Nahoru Odpovědět 8. října 20:12
"„Učíš-li se proto, aby sis zapamatoval, zapomeneš. Učíš-li se proto, abys porozuměl, zapamatuješ si."
Avatar
xmms
Člen
Avatar
Odpovídá na DarkCoder
xmms:17. října 0:17

Našel jsem http://www.cplusplus.com/…omic/atomic/
Je tam lokální volatilní proměnná

for (volatile int i=0; i<1000000; ++i) {}

Jaký to má význam uvnitř lokálního bloku, když je uvnitř definovaná? Nebo to je zbytečné?

 
Nahoru Odpovědět 17. října 0:17
Avatar
Martin Dráb
Redaktor
Avatar
Odpovídá na xmms
Martin Dráb:17. října 19:18

Jaký to má význam uvnitř lokálního bloku, když je uvnitř definovaná? Nebo to je zbytečné?

Tipl bych si, že toto použití volatile donutí překladač danou smyčku neoptimalizovat použitím vektorových instrukcí (tzn. pokus o vykonání více iterací současně) či jejího odrolování (místo N iterací se bude postupně provádět třeba N/4, v každé iteraci budou za sebou (či nějak promíchané) 4 iterace původní).

Nahoru Odpovědět 17. října 19:18
2 + 2 = 5 for extremely large values of 2
Avatar
DarkCoder
Člen
Avatar
Odpovídá na xmms
DarkCoder:17. října 20:35

Jak napsal Martin, důvod použití volatile pro proměnnou i je aby překladač pro tuto proměnnou neoptimalizoval kód. Napíšu to sportovní terminologií, neboť se to pro tento příklad přímo nabízí.

Na trati je deset závodníků (vláken), kde si každý brousí zuby na vítězství (kdo jako první dosáhne mety 1000000). Aby závodník zvýšil své šance na vítězství, je pro to schopen udělat cokoli, třeba i podvádět tím, že by si vzal od doktora (překladač) zakázanou podpůrnou látku (optimalizace). Aby měli všichni závodníci stejné podmínky, prošel každý závodník dopingovou kontrolou (klíčové slovo volatile). Každý závodník na startu tak bude "čistý" (neoptimalizovaný). Vše tedy bude záviset jen na výkonu (přiřazení priorit vláken operačním systémem) každého závodníka.

Nahoru Odpovědět 17. října 20:35
"„Učíš-li se proto, aby sis zapamatoval, zapomeneš. Učíš-li se proto, abys porozuměl, zapamatuješ si."
Avatar
xmms
Člen
Avatar
Odpovídá na DarkCoder
xmms:17. října 22:31

A kdyby místo toho použil atomickou proměnnou std::atomic, dosáhl by stejného výsledku?

 
Nahoru Odpovědět 17. října 22:31
Avatar
DarkCoder
Člen
Avatar
Odpovídá na xmms
DarkCoder:18. října 0:04

Deklarace proměnné i jako atomic v daném programu nemá žádný smysl. Atomické proměnné nelze zaměňovat za volatile. Jejich význam je úplně odlišný.

volatile - potlačuje optimalizaci kódu navrženou překladačem a říká překladači, že proměnná se může měnit aniž by to bylo v programu specifikováno. Testuje hodnotu při každém jejím použití. Neslouží k synchronizaci vláken. Její využití je zejména při komunikaci s HW.

std::atomic<> - o optimalizaci nic neříká. Slouží k synchronizaci vláken, což je proces, aby se k proměnné v danou dobu přistupovalo vždy a pouze jen z jednoho vlákna. Smyslem synchronizace je vyhnout se nežádoucímu efektu zvaný souběh (angl. Race Condition).

Důvodem toho, proč by atomicita proměnné i neměla význam je ten, že nedochází k souběhu. Každé vlákno má svoji instanci proměnné i, viz. následující příklad:

#include <iostream>
#include <thread>

void func(int id);

int main(void) {
        std::thread t1(func, 1);
        std::thread t2(func, 2);

        t1.join();
        t2.join();
        return 0;
}

void func(int id) {
        for (int i = 0; i < 100; i++) {
                std::cout << i << '(' << id << ')' << '\n';
        }
}

Naproti tomu v následujícím příkladu:

#include <iostream>
#include <thread>
#include <atomic>

void func(int id);

std::atomic<bool> ready(false);

int main(void) {
        std::thread t1(func, 1);
        std::thread t2(func, 2);
        ready = true;

        t1.join();
        t2.join();
        return 0;
}

void func(int id) {
        while (!ready) std::this_thread::yield();
        for (int i = 0; i < 100; i++) {
                std::cout << i << '(' << id << ')' << '\n';
        }
}

dochází k souběhu, neboť ke globální proměnné je přistupováno z obou vláken, ta obě proměnnou čtou a hlavní vlákno ji přepisuje. Proto je deklarována proměnná ready jako atomic, aby se k ní vždy přistupovalo pouze z jednoho vlákna.

Editováno 18. října 0:05
Nahoru Odpovědět 18. října 0:04
"„Učíš-li se proto, aby sis zapamatoval, zapomeneš. Učíš-li se proto, abys porozuměl, zapamatuješ si."
Avatar
DarkCoder
Člen
Avatar
Odpovídá na xmms
DarkCoder:18. října 0:49

Abys viděl negativní účinky souběhu, podívej na následující příklad:

#include <iostream>
#include <thread>
#include <atomic>

void func(void);

std::atomic<int> acount(0);
int count = 0;

int main(void) {
        std::thread t1(func);
        std::thread t2(func);

        t1.join();
        t2.join();

        std::cout << acount << '\n';
        std::cout << count << '\n';

        return 0;
}

void func(void) {
        for (int n = 0; n < 100000; n++) {
                count++;
                acount++;
        }
}

Program zobrazí např.:
200000
164407

Nahoru Odpovědět 18. října 0:49
"„Učíš-li se proto, aby sis zapamatoval, zapomeneš. Učíš-li se proto, abys porozuměl, zapamatuješ si."
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 19 zpráv z 19.