Co oznacza parametr volatile?

0

Czym różnią się poniższe funkcje?

void USART_Puts(volatile char *s)

void USART_Puts(char *s)

Co zmienia parametr volatile?

Chciałbym jeszcze zaimplementować coś w rodzaju komend AT. Najpierw będę odbierał pojedyncze znaki w przerwaniu. Zastanawiam się czy jest potrzeba dodawać je do bufora kołowego i dopiero potem je parsować? Czy może bufor kołowy jest zbędny?

0
  1. Chyba bufor cykliczny, a nie kołowy.
  2. Volatile oznacza by nie trzymać kopii zmiennej by używać jej w kolejnych instrukcjach. Zmienna volatile jest ładowana z pamięci przy każdej instrukcji, która z niej korzysta. Zmienne bez volatile mogą mieć np kopie przechowywane w rejestrach. Operacje działają wtedy na tych kopiach, ale tak, by nie zmienić semantyki programu w przypadku wykonania jednowątkowego.
0

@Wibowit

Zmienna volatile jest ładowana z pamięci przy każdej instrukcji, która z niej korzysta.

To nie prawda. Obiekty volatile jak najbardziej mogą być cache'owane (https://en.wikipedia.org/wiki/Cache_invalidation).
volatile oznacza, że każdy dostęp do obiektu jest traktowany jako dostęp z side effects, stąd wyłączona zostaje optymalizacja as-if. Wynika z tego, że instrukcje na obiektach z volatile nie podlegają optymalizacji, nie mogą mieć zmienionej przez optymalizator semantyki czy porządku. Należy pamiętać, że taki obiekt nie jest jednak obiektem thread safe.

0

To nie prawda. Obiekty volatile jak najbardziej mogą być cache'owane (https://en.wikipedia.org/wiki/Cache_invalidation).

Mogą sobie siedzieć w pamięci podręcznej procesora, ale nadal na poziomie instrukcji maszynowych mamy do czynienia ze zwykłym ładowaniem z pamięci z oryginalnej lokalizacji.

0

Nie. Siedzą sobie w "pamięci podręcznej procesora" (L2 cache) i stamtąd są ładowane. Inaczej jaki sens miałoby ich cache'owanie?

0

Napisałem:

na poziomie instrukcji maszynowych mamy do czynienia ze zwykłym ładowaniem z pamięci z oryginalnej lokalizacji.

Logiczne, że chodziło mi o o to, że w przypadku volatile za każdym razem mamy ładowanie z pamięci za pomocą:

mov rejestr, [oryginalny adres]

a nie korzystamy z kopii, która np już siedzi sobie w jakimś rejestrze.

Coś nie pasi?

0

To nie prawda. Obiekty volatile jak najbardziej mogą być cache'owane (https://en.wikipedia.org/wiki/Cache_invalidation).

Szczegół implementacyjny danego komputera. Instrukcja jest generowana, i o to w volatile chodzi.

Należy pamiętać, że taki obiekt nie jest jednak obiektem thread safe.

Jest albo nie jest. Przeznaczeniem volatile nie jest synchronizacja wątków - gwarancje które daje samo w sobie są za słabe. Ostrożnie użyte może jednak do tego posłużyć.

0

W kontekście deklaracji funkcji znaczy także, że możesz przekazać volatile * jako parametr (w przeciwnym razie dostaniesz błąd kompilacji).

0

@Wibowit chyba faktycznie mówimy o tym samym. Istotne jednak, że obiekty volatile mogą być cache'owane.

@Azarien za Scott Meyers' Effective Modern C++:

volatile has nothing to do with concurrent programming. [...] It's thus wortwhile to discuss volatile in a chapter on concurrency if for no other reason than to dispel the confision surrounding it. [...] In contrast, the corresponding code using volatile guarantees virtually nothing in a multitheading context. [...] During execution of this code, if other threads are reading the value vi, they may see anything. Such code would have undefined behaviour, because these statements modify vi, so if other threads are reading vi at the same time, there are somultaneous readers and writers of memory that's neither std::atomic nor protected by a mutex, and that's the definition of a data race.

0

Ja dodam jeszcze że volatile wykorzystuje się przede wszystkim w obiektach, które mogą się spontanicznie zmienić bez wiedzy kompilatora.

Rozważmy kod, który odbiera 3 bajtu z kontrolera uart

{
uint8_t buf[3] = {0};
uint8_t *dr = (uint8_t *)0x40001005; // rejestr w którym zapisywany jest bajt odebrany po uart
buf[0] = *dr;
buf[1] = *dr;
buf[2] = *dr;
}

Po odczycie rejestru dr, kontroler zapisuje do rejestru dr następny bajt w kolejce (pomijam tutaj ustawienia flag). Kompilator tutaj może stwierdzić, że skoro w całym scopie, dr się nie zmienia, to on sobie tylko raz odczyta to co jest pod dr, a następnie będzie tylko brał wartość z rejestru. I dostajemy 3 takie same bajty - błędne.

Aby temu zaradzić do dr dodajemy słówko volatile. W takim przypadku, kompilator wie, że dr może się zmienić spontanicznie, bez udziału programu i każde odwołanie do komórki wskazującej przez dr, będzie zawsze pobierane bezpośrednio spod tego adresu.

0

@Azarien: Do programowania współbieżnego służą niskopoziomowe mechanizmy, takie jak atomowe operacje logiczne i arytmetyczne oraz grupy specjalizowanych instrukcji takich jak test-and-set, compare-and-swap oraz load-link/store-conditional. Do tego mogą dochodzić instrukcje monitora. Wszystko dostępne w C z poziomu rozszerzeń kompilatora z prefixami __builtin. W oparciu o te niskopoziomowe mechanizmy implementuje się to, co programiści na co dzień używają: semafory, blokady, blokady wirujące i inne im podobne, dostępne już w bibliotece C albo w innych bibliotekach systemowych.

0

taa, wiedziałem że zaraz ktoś się czepi. napisałem że DA SIĘ wykorzystać volatile w wielowątkowości. ale napisałem też że nie do tego służy.

0

A możesz pokazać "ostrożne użycie volatile" do synchronizacji? Serio, powiedziałeś coś, co może być naprawdę interesujące ;)

2

@kapojot niektóre kompilatory mają rozszerzenie, które wprowadza synchronizacje na poziomie volatile. Na przykład w MSVC masz opcję /volatile:ms https://msdn.microsoft.com/en-us/library/jj204392.aspx https://msdn.microsoft.com/en-us/library/12a04hfd.aspx
Generalnie jednak nie powinno się na tym polegać/używać tego, o ile nie piszesz kodu tylko na jedną platformę i nie kompilujesz go tylko jednym kompilatorem i masz pewność, że to się raczej nie zmieni w przyszłości.

0

@satirev: dzięki za wyjaśnienie. Nie używam VS, a gcc/clang nie dają takich gwarancji.

0
kapojot napisał(a):

A możesz pokazać "ostrożne użycie volatile" do synchronizacji? Serio, powiedziałeś coś, co może być naprawdę interesujące ;)

Jeżeli wszystkie dane współdzielone między dwoma wątkami są oznaczone jako volatile to jest to bezpieczne.
Łatwo jednak o pomyłkę. Nie polecam.

2

Prosty test:

#include <assert.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

#define N 100000

volatile int sum = 0;

void *
thr_add(void *_unused)
{
	int i;

	for (i = 0; i < N; i++)
		sum += 1;

	return (NULL);
}

void *
thr_sub(void *_unused)
{
	int i;

	for (i = 0; i < N; i++)
		sum -= 1;

	return (NULL);
}

int
main()
{
	int ret;
	pthread_t t1, t2;

	ret = pthread_create(&t1, NULL, thr_add, NULL);
	assert(ret == 0);
	ret = pthread_create(&t2, NULL, thr_sub, NULL);
	assert(ret == 0);

	pthread_join(t1, NULL);
	pthread_join(t2, NULL);

	printf("%d\n", sum);

	return (0);
}

Daje:

▶ ./test    
8484

▶ ./test
-3066

▶ ./test
29127

▶ ./test
-24111

▶ ./test
12583

▶ ./test
27027

Poprawna wersja (atomowe operacje) zawsze daje 0:

#include <assert.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

#define N 100000

volatile int sum = 0;

void *
thr_add(void *_unused)
{
	int i;

	for (i = 0; i < N; i++)
		__sync_fetch_and_add(&sum, 1);

	return (NULL);
}

void *
thr_sub(void *_unused)
{
	int i;

	for (i = 0; i < N; i++)
		__sync_fetch_and_sub(&sum, 1);

	return (NULL);
}

int
main()
{
	int ret;
	pthread_t t1, t2;

	ret = pthread_create(&t1, NULL, thr_add, NULL);
	assert(ret == 0);
	ret = pthread_create(&t2, NULL, thr_sub, NULL);
	assert(ret == 0);

	pthread_join(t1, NULL);
	pthread_join(t2, NULL);

	printf("%d\n", sum);

	return (0);
}
0
        sum += 1;


        sum -= 1;

Źle i dobrze wiesz dlaczego źle.

0

Ten kod

volatile int val = 0;

void fun(void) {
	++val;
}

kompiluje się do:

val:
        .zero   4
fun():
        pushq   %rbp
        movq    %rsp, %rbp
        movl    val(%rip), %eax
        addl    $1, %eax
        movl    %eax, val(%rip)
        nop
        popq    %rbp
        ret

Wyciągając z tego kod operujący na zmiennej val

movl    val(%rip), %eax
addl    $1, %eax
movl    %eax, val(%rip)

Operacja inkrementowania zmiennej ma 3 instrukcje, nawet dla volatile. Osobiście nie widzę przeciwwskazań aby przyszło przerwanie po załadowaniu wartości zmiennej do rejestru, a przed dodaniem 1 i przełączeniem na inny wątek, który też operuje na val. W związku z tym nie widzę możliwości aby móc okreslić volatile jako thread-safe.

Chyba, że jest jakaś sztuczka, która na to pozwala, ale kompilowałem z -std=c11, więc na pewno nie będzie to zachowanie zgodne ze standardem, więc nie można by na nim za bardzo polegać.

0

No bo tak się nie da.

"synchronizacja" to za dużo powiedziane. "współdzielenie danych" bardziej.

volatile int progress;

void thread1()
{
    for (progress=1; progress<=100; progress++)
    {
        // długie obliczenia
    }
}

void thread2()
{
    do
    {
        showProgress(progress);
        delay(1000);
    }
    while (progress!=100);
}

Bez volatile teoretycznie zmienna progress może skoczyć z 0 na 100 dopiero pod koniec obliczeń.

Kod powyżej oczywiście ma race condition, bo nie wiemy jakie wartości progress zostaną wyświetlone. Ale to normalne we wszelkiego rodzaju paskach postępu.

0

Zgadza się ale należy dodać, że takie współdzielenie ma sens tylko w przypadku kiedy jeden wątek ma prawa rw do zmiennej a wszystkie inne wątki mają prawa r, a w dodatku reszta wątków nie polega za bardzo na aktualnej wersji pola. Progress bar jest tutaj całkiem dobrym przykładem. Jak użyjemy słowa "współdzielenie", zamiast "synchronizacji" danych to w ograniczonych przypadkach można przyznać rację.

0
Azarien napisał(a)

Źle i dobrze wiesz dlaczego źle.

Mam nadzieję, że nie podejrzewasz mnie, że konstruując kontrprzykład dla tezy o zastosowaniu volatile do synchronizacji napisałem przypadkowo kod, w którym sam nie widziałbym błędów. Pewnie też z tego samego powodu pokazałem od razu poprawny kod z właściwym komentarzem, żeby ktoś nieświadomie nie użył tego śmiecia w praktyce :).

Azarien napisał(a)

Bez volatile teoretycznie zmienna progress może skoczyć z 0 na 100 dopiero pod koniec obliczeń.

A to raczej nie jest kwestia współbieżności, co raczej faktu, że to volatile w Twoim kodzie uwala kompilatorowi parę optymalizacji. Moim zdaniem problem w kodzie powyżej jest wyłącznie na poziomie optymalizacji funkcji thr1, gdzie kompilator może stwierdzić "nic nie robię z progress w pętli - rozwinę pętlę i podstawię za progress 100". Thr2 zawsze będzie ładowało wartość ze zmiennej globalnej niezależnie od volatile.

1 użytkowników online, w tym zalogowanych: 0, gości: 1