Vydělávej až 160.000 Kč měsíčně! Akreditované rekvalifikační kurzy s garancí práce od 0 Kč. Více informací.
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.
Avatar
xmms
Člen
Avatar
xmms:20.9.2019 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.9.2019 23:49
Avatar
DarkCoder
Člen
Avatar
Odpovídá na xmms
DarkCoder:21.9.2019 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
21.9.2019 2:32
"I ta nejlepší poučka postrádá na významu, není-li patřičně předána." - DarkCoder
Avatar
xmms
Člen
Avatar
Odpovídá na DarkCoder
xmms:21.9.2019 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.9.2019 12:27
Avatar
Martin Dráb
Tvůrce
Avatar
Odpovídá na xmms
Martin Dráb:21.9.2019 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
21.9.2019 15:57
2 + 2 = 5 for extremely large values of 2
Avatar
DarkCoder
Člen
Avatar
Odpovídá na xmms
DarkCoder:21.9.2019 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
21.9.2019 17:10
"I ta nejlepší poučka postrádá na významu, není-li patřičně předána." - DarkCoder
Avatar
xmms
Člen
Avatar
Odpovídá na Martin Dráb
xmms:21.9.2019 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.9.2019 17:26
Avatar
Martin Dráb
Tvůrce
Avatar
Odpovídá na xmms
Martin Dráb:21.9.2019 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.9.2019 17:37
2 + 2 = 5 for extremely large values of 2
Avatar
xmms
Člen
Avatar
Odpovídá na Martin Dráb
xmms:21.9.2019 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.9.2019 18:10
Avatar
Martin Dráb
Tvůrce
Avatar
Odpovídá na xmms
Martin Dráb:21.9.2019 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.9.2019 18:19
2 + 2 = 5 for extremely large values of 2
Avatar
xmms
Člen
Avatar
xmms:21.9.2019 22:19

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

 
Nahoru Odpovědět
21.9.2019 22:19
Avatar
Martin Dráb
Tvůrce
Avatar
Odpovídá na xmms
Martin Dráb:22.9.2019 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.9.2019 11:17
2 + 2 = 5 for extremely large values of 2
Avatar
xmms
Člen
Avatar
Odpovídá na DarkCoder
xmms:8.10.2019 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.10.2019 15:42
 
Nahoru Odpovědět
8.10.2019 15:40
Avatar
DarkCoder
Člen
Avatar
Odpovídá na xmms
DarkCoder:8.10.2019 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.10.2019 20:12
"I ta nejlepší poučka postrádá na významu, není-li patřičně předána." - DarkCoder
Avatar
xmms
Člen
Avatar
Odpovídá na DarkCoder
xmms:17.10.2019 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.10.2019 0:17
Avatar
Martin Dráb
Tvůrce
Avatar
Odpovídá na xmms
Martin Dráb:17.10.2019 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.10.2019 19:18
2 + 2 = 5 for extremely large values of 2
Avatar
DarkCoder
Člen
Avatar
Odpovídá na xmms
DarkCoder:17.10.2019 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.10.2019 20:35
"I ta nejlepší poučka postrádá na významu, není-li patřičně předána." - DarkCoder
Avatar
xmms
Člen
Avatar
Odpovídá na DarkCoder
xmms:17.10.2019 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.10.2019 22:31
Avatar
DarkCoder
Člen
Avatar
Odpovídá na xmms
DarkCoder:18.10.2019 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.10.2019 0:05
Nahoru Odpovědět
18.10.2019 0:04
"I ta nejlepší poučka postrádá na významu, není-li patřičně předána." - DarkCoder
Avatar
DarkCoder
Člen
Avatar
Odpovídá na xmms
DarkCoder:18.10.2019 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.10.2019 0:49
"I ta nejlepší poučka postrádá na významu, není-li patřičně předána." - DarkCoder
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.