Synchronizacja Wątków 101

Poprzednio pisałem bardzo prosty, wielowątkowy kod i zawsze byłem świadomy, że w każdej chwili może być przełącznik kontekstowy w samym środku tego, co robię, więc zawsze strzeżałem dostępu do współdzielonych zmiennych poprzez klasę CCriticalSection, która wchodzi do krytycznej sekcji budowy i pozostawia ją na zniszczeniu. Wiem, że jest to dość agresywne i wchodzę i opuszczam sekcje krytyczne dość często, a czasami skandalicznie (np. na początku funkcji kiedy mogłem umieścić CCriticalSection w ciaśniejszym bloku kodu), ale mój kod nie zawiesza się i działa wystarczająco szybko.

W pracy mój wielowątkowy kod musi być ciaśniejszy, tylko blokowanie / synchronizacja na najniższym wymaganym poziomie.

W pracy próbowałem debugować jakiś wielowątkowy kod i natknąłem się na to:

EnterCriticalSection(&m_Crit4);
m_bSomeVariable = true;
LeaveCriticalSection(&m_Crit4);

Teraz, m_bSomeVariable jest Win32 BOOL (nie Lotny), który z tego co wiem jest zdefiniowany jako int, a na x86 odczyt i zapis tych wartości jest pojedynczym instrukcji, a ponieważ przełączniki kontekstowe występują na granicy instrukcji, nie ma potrzeby synchronizowania tej operacji z sekcją krytyczną.

Poszperałem w Internecie, aby sprawdzić, czy ta operacja nie wymaga synchronizacji, i wymyśliłem dwa scenariusze, które zrobiły:

  1. procesor implementuje niewykonanie polecenia lub drugi wątek działa na innym rdzeniu, a zaktualizowana wartość nie jest zapisywana do pamięci RAM, aby drugi rdzeń mógł zobaczyć; oraz
  2. int nie jest wyrównany 4-bajtowo.

Uważam, że numer 1 można rozwiązać używając słowa kluczowego "Lotny". W VS2005 i późniejszych kompilator C++ otacza dostęp do tej zmiennej za pomocą barier pamięci, zapewniając, że zmienna jest zawsze całkowicie zapisana/odczytana do głównej pamięci systemowej przed jej użyciem.

Numer 2 nie mogę zweryfikować, Nie wiem dlaczego wyrównanie bajtów miałoby coś zmienić. Nie znam zestawu instrukcji x86, ale czy mov Należy podać 4-bajtowy adres wyrównany? Jeśli nie, czy musisz użyć kombinacji instrukcji? To by wprowadziło problem.

Więc...

Pytanie 1: czy użycie słowa kluczowego "volatile" (implicity używanie barier pamięci i podpowiadanie kompilatorowi, aby nie optymalizować tego kodu) zwalnia programistę z konieczności synchronizacji 4-bajtowej/8-bajtowej zmiennej x86/x64 pomiędzy operacjami odczytu/zapisu?

Pytanie 2: czy istnieje wyraźny wymóg, aby zmienna była 4-bajtowe/8-bajtowe wyrównane?

Trochę poszperałem w naszym kodzie i zmiennych zdefiniowanych w klasie:

class CExample
{

private:

    CRITICAL_SECTION m_Crit1; // Protects variable a
    CRITICAL_SECTION m_Crit2; // Protects variable b
    CRITICAL_SECTION m_Crit3; // Protects variable c
    CRITICAL_SECTION m_Crit4; // Protects variable d

    // ...

};
Wydaje mi się to przesadne. Myślałem, że sekcje krytyczne synchronizują wątki między procesem, więc jeśli masz taki, możesz go wprowadzić i żaden inny wątek w tym procesie nie może wykonać. Nie ma potrzeby tworzenia sekcji krytycznej dla każdej zmiennej, którą chcesz chronić, jeśli jesteś w sekcji krytycznej, nic innego nie może Ci przeszkodzić.

Myślę, że jedynym rzeczą, która może zmienić zmienne spoza sekcji krytycznej, jest to, że proces dzieli stronę pamięci z innym procesem (czy możesz to zrobić?), a drugi proces zaczyna zmieniać wartości. Mutexy również by tu pomogły, nazwane mutexy są współdzielone między procesami, czy tylko procesy o tej samej nazwie?

Pytanie 3: czy moja analiza sekcji krytycznych jest poprawna i czy ten kod należy przepisać tak, aby używał muteksów? Przyjrzałem się innym obiektom synchronizacji (semafory i spinlocki), czy są tu lepiej dopasowane?

Pytanie 4: gdzie najlepiej nadają się sekcje krytyczne / muteksy / semafory / spinlocki? Czyli do którego problemu synchronizacji należy je zastosować. Czy istnieje ogromna kara za wykonanie wyboru jednego nad drugim?

A skoro już o tym mowa, czytałem, że spinlocki nie powinny być używane w środowisku jednordzeniowym wielowątkowym, tylko w środowisku wielordzeniowym wielowątkowym. Więc, pytanie 5: czy to źle, a jeśli nie, to dlaczego tak jest?

Z góry dzięki za wszelkie odpowiedzi:)

Author: AshleysBrain, 2010-03-31

6 answers

Q1: użycie słowa kluczowego "Lotny"

W VS2005 i Później kompilator C++ otacza dostęp do tej zmiennej za pomocą barier pamięci, zapewniając, że zmienna jest zawsze całkowicie zapisana/odczytana do głównej pamięci systemowej przed jej użyciem.

Dokładnie. Jeśli nie tworzysz kodu przenośnego, Visual Studio implementuje go dokładnie w ten sposób. Jeśli chcesz być przenośny, Twoje opcje są obecnie "ograniczone". Until C++0x there is no portable way how to specific atomic operacje z gwarantowaną kolejnością odczytu/zapisu i musisz wdrożyć rozwiązania per-platformowe. To powiedziawszy, boost już zrobił brudną robotę za Ciebie i możesz użyć jego atomowych prymitywów .

Q2: zmienna musi być wyrównana 4-bajtowo/8-bajtowo?

/ Align = "left" / Jeśli nie, reguły są skomplikowane (linie pamięci podręcznej, ...), dlatego najbezpieczniejszym sposobem jest ich wyrównanie, ponieważ jest to łatwe do osiągnięcia.

Q3: czy ten kod powinien być przepisany, aby użyć muteksy?

Sekcja krytyczna to lekki mutex. Jeśli nie trzeba synchronizować między procesami, użyj sekcji krytycznych.

P4: gdzie najlepiej nadają się sekcje krytyczne / muteksy / semafory / spinlocki?

Sekcje krytyczne mogą nawet wykonać dla Ciebie spin waity.

Q5: Spinlocki nie powinny być używane w jednym rdzeniu

Spin lock wykorzystuje fakt, że podczas obracania oczekującego procesora inny procesor może zwolnić blokadę. Nie może się to zdarzyć z tylko jeden procesor, dlatego jest to tylko strata czasu. Na multi-CPU spin locks może być dobrym pomysłem, ale zależy to od tego, jak często Spin wait zakończy się sukcesem. Pomysł czeka na krótki czas jest o wiele szybszy niż robi przełącznik kontekstowy tam iz powrotem, dlatego jeśli czekać może być krótki, lepiej poczekać.

 7
Author: Suma,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-09-03 12:13:37

1) NO volatile po prostu mówi ponownie załadować wartość z pamięci za każdym razem, gdy jest jeszcze możliwe, aby być w połowie zaktualizowane.

Edytuj: 2) Windows zapewnia pewne funkcje atomowe. Sprawdź " Interlocked " functions .

Komentarze skłoniły mnie do lektury. Po przeczytaniu Intel System Programming Guide widać, że odczyt i zapis są atomowe.

8.1.1 Gwarantowane Operacje Atomowe Procesor Intel486 (i nowsze przetwórców od) gwarantuje, że następujące podstawowe operacje pamięci będą zawsze wykonywane atomicznie:
* Odczyt lub zapis bajtu
• Odczyt lub zapis słowa wyrównanego na granicy 16-bitowej
• Odczyt lub zapis podwójnego słowa wyrównanego na granicy 32-bitowej
Procesor Pentium (i nowsze procesory od tego czasu) gwarantuje, że następujące dodatkowe operacje pamięci będą zawsze wykonywane atomicznie:
* Odczyt lub zapis quadword wyrównany na 64-bitowym granica
• 16-bitowy dostęp do nieusuniętych miejsc pamięci, które mieszczą się w 32-bitowej szynie danych
Procesory z rodziny P6 (i nowsze od tego czasu) gwarantują, że następujące dodatkowa operacja pamięci będzie zawsze wykonywana atomicznie:
* Niepodpisany 16 -, 32-i 64-bitowy dostęp do pamięci podręcznej mieszczącej się w pamięci podręcznej linia
Dostęp do pamięci podręcznej, która jest podzielona na szerokości magistrali, linie pamięci podręcznej i granice stron nie są gwarantowane przez Intel Core 2 Duo, Intel Atom, Intel Core Duo, Pentium M, Pentium 4, Intel Xeon, rodzina P6, Pentium i Procesory Intel486. Intel Core 2 Duo, Intel Atom, Intel Core Duo, Pentium M, Procesory z rodziny Pentium 4, Intel Xeon i P6 dostarczają sygnały sterujące magistrali, które zezwalać zewnętrznym podsystemom pamięci na dzielenie się dostępami; jednak, niezaliczany dostęp do danych poważnie wpłynie na wydajność procesora i należy unikać. Instrukcja x87 lub Instrukcja SSE, która uzyskuje dostęp do danych większy niż czworokąt może być zaimplementowany przy użyciu wielu dostępów do pamięci. Jeśli taka instrukcja przechowuje do pamięci, niektóre dostępy mogą się zakończyć (zapis do pamięci), podczas gdy inne powoduje błąd operacji ze względów architektonicznych (np. z powodu wpisu page-table oznaczony jako "nie obecny"). W tym przypadku skutki zakończonych dostępu może być widoczny dla oprogramowania, nawet jeśli ogólna Instrukcja spowodowała usterkę. If TLB unieważnienie zostało opóźnione( patrz punkt 4.10.3.4), takie mogą wystąpić błędy strony nawet jeśli wszystkie dostępy są na tej samej stronie.

Więc w zasadzie tak, jeśli robisz 8-bitowy Odczyt/Zapis z dowolnego adresu 16-bitowy Odczyt/Zapis z 16-bitowego wyrównanego adresu itp itd, otrzymujesz operacje atomowe. Interesujące jest również to, że można zrobić unaligned memory read / write w pamięci podręcznej na nowoczesnej maszynie. Zasady wydają się dość skomplikowane, więc na Twoim miejscu nie polegałbym na nich. Pozdro dla komentujących to dobre doświadczenie w nauce dla ja ten:)

3) sekcja krytyczna spróbuje spin lock dla swojego zamka kilka razy, a następnie zamyka mutex. Spin Locking może ssać moc procesora nie robiąc nic, a mutex może trochę potrwać, aby zrobić swoje rzeczy. Krytyczne sekcje są dobrym wyborem, jeśli nie możesz korzystać z zablokowanych funkcji.

4) są kary za wykonanie wyboru jednego nad drugim. To dość duża prośba, aby przejść przez korzyści ze wszystkiego tutaj. Pomoc MSDN zawiera wiele dobrych informacji na temat każdego z to. Czytam je.

5) możesz użyć blokady spin lock w środowisku z pojedynczym gwintem nie jest to zwykle konieczne, ponieważ zarządzanie wątkami oznacza, że nie możesz mieć 2 procesorów korzystających jednocześnie z tych samych danych. To niemożliwe.

 13
Author: Goz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-03-31 12:52:50

1: Volatile samo w sobie jest praktycznie bezużyteczne dla wielowątkowości. Gwarantuje, że odczyt/zapis zostanie wykonany, a nie zapisany w rejestrze, i gwarantuje, że odczyt/zapis nie zostanie zmieniony w odniesieniu do innych volatile odczytów/zapisów . Ale nadal może być zmieniona kolejność w odniesieniu do nieulotnych, co jest w zasadzie 99.9% Twojego kodu. Microsoft przedefiniował volatile, aby również zawinąć wszystkie dostępy w bariery pamięci, ale nie jest to gwarantowane bądź w ogóle. Po prostu po cichu złamie się na każdym kompilatorze, który definiuje volatile tak jak standard. (Kod będzie kompilowany i uruchamiany, po prostu nie będzie już bezpieczny dla wątków)

Poza tym Odczyt/Zapis do obiektów o rozmiarze całkowitym jest atomowy na x86, o ile obiekt jest dobrze wyrównany. (Nie masz jednak gwarancji , Kiedy nastąpi zapis. Kompilator i procesor mogą zmienić jego kolejność, więc jest atomowy, ale nie bezpieczny dla wątków)

2: tak, obiekt musi być wyrównany aby odczyt/zapis był atomowy.

Nie bardzo. Tylko jeden wątek może wykonać kod wewnątrz danej sekcji krytycznej na raz. Inne wątki mogą nadal wykonywać inny kod. Więc możesz mieć cztery zmienne, każda chroniona przez inną sekcję krytyczną. Gdyby wszystkie dzieliły tę samą sekcję krytyczną, nie byłbym w stanie manipulować obiektem 1, podczas gdy ty manipulujesz obiektem 2, co jest nieefektywne i ogranicza równoległość bardziej niż to konieczne. Jeśli są one chronione przez różne sekcje krytyczne, nie możemy jednocześnie manipulować tym samym obiektem.

4: Spinlocki są rzadko dobrym pomysłem. Są one przydatne, jeśli oczekujesz, że wątek będzie musiał poczekać tylko bardzo krótki czas, zanim będzie w stanie uzyskać blokadę, i absolutnie potrzebujesz minimalnego opóźnienia. Unika przełącznika kontekstowego systemu operacyjnego, który jest stosunkowo powolnym działaniem. Zamiast tego wątek po prostu siedzi w pętli, ciągle przepytywając zmienną. Więc większe zużycie procesora (rdzeń nie jest po uruchomieniu kolejnego wątku w oczekiwaniu na spinlock), ale wątek będzie mógł kontynuować jak tylko Po zwolnieniu blokady.

Jeśli chodzi o pozostałe, charakterystyka wydajności jest prawie taka sama: po prostu użyj tego, który ma semantykę najlepiej dopasowaną do Twoich potrzeb. Zazwyczaj sekcje krytyczne są najwygodniejsze do ochrony współdzielonych zmiennych, a mutexy mogą być łatwo używane do ustawiania "flagi" umożliwiającej kontynuowanie innych wątków.

Jak nie używać spinlocki w środowisku jednordzeniowym pamiętaj, że spinlock nie ustępuje. Wątek a oczekiwanie na spinlock nie jest w rzeczywistości zawieszone, umożliwiając systemowi operacyjnemu zaplanowanie uruchomienia wątku B. Ale ponieważ A czeka na ten spinlock, jakiś inny wątek będzie musiał zwolnić ten zamek. Jeśli masz tylko jeden rdzeń, ten inny wątek będzie mógł działać tylko wtedy, gdy A jest wyłączony. Z rozsądnym OS, to się stanie prędzej czy później i tak w ramach zwykłego kontekstu przełączam się. Ale ponieważ wiemy, że A nie będzie w stanie uzyskać blokady, dopóki B nie będzie miał czasu na wykonanie i zwolnienie blokady, byłoby lepiej, gdyby A natychmiast się poddał, został umieszczony w kolejce oczekiwania przez system operacyjny i uruchomiony ponownie, gdy B zwolni blokadę. I tak robią wszystkieinne typy zamków. Spinlock nadal będzie działał w środowisku z jednym rdzeniem (zakładając system operacyjny z prewencyjną wielozadaniowością), będzie po prostu bardzo nieefektywny.

 8
Author: jalf,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-03-31 12:25:55

Nie używaj lotnych. Nie ma to praktycznie nic wspólnego z bezpieczeństwem nici. Zobacz tutaj na dole.

Przypisanie do BOOLA nie wymaga żadnej synchronizacji. Będzie działać bez specjalnego wysiłku z twojej strony.

Jeśli chcesz ustawić zmienną, a następnie upewnić się, że inny wątek widzi nową wartość, musisz ustanowić jakiś rodzaj komunikacji między tymi dwoma wątkami. Samo blokowanie bezpośrednio przed przypisaniem nic nie daje, ponieważ inne wątki mogły się pojawiać i znikać, zanim zdobyłeś zamek.

Ostatnie słowo ostrzeżenia: gwintowanie jest niezwykle trudne do poprawienia. Najbardziej doświadczeni programiści wydają się być najmniej komfortowi w użyciu wątków, co powinno ustawić dzwonek alarmowy dla każdego, kto jest niedoświadczony w ich użyciu. Zdecydowanie sugeruję użycie prymitywów wyższego poziomu do zaimplementowania współbieżności w aplikacji. Przekazywanie niezmiennych struktur danych przez zsynchronizowane kolejki jest jednym z podejść, które znacznie zmniejsza niebezpieczeństwo.

 5
Author: Marcelo Cantos,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-03-31 10:58:16

Lotność nie implikuje barier pamięci.

Oznacza to tylko, że będzie częścią postrzeganego stanu modelu pamięci. Implikacją tego jest to, że kompilator nie może zoptymalizować zmiennej, ani nie może wykonywać operacji na zmiennej tylko w rejestrach procesora (faktycznie załaduje się i zapisze do pamięci).

Ponieważ nie ma żadnych barier pamięciowych, kompilator może dowolnie zmieniać kolejność instrukcji. Jedyną gwarancją jest to, że kolejność, w której różne zmienne zmienne są odczytywane/zapisywane tak samo jak w kodzie:

void test() 
{
    volatile int a;
    volatile int b;
    int c;

    c = 1;
    a = 5;
    b = 3;
}

Z powyższym kodem (zakładając, że c nie jest zoptymalizowana) aktualizacja do c może nastąpić przed lub po aktualizacji do a i b, zapewniając 3 możliwe wyniki. Aktualizacje a i b są gwarantowane w kolejności. c może być łatwo zoptymalizowany przez dowolny kompilator. Z wystarczającą ilością informacji kompilator może nawet zoptymalizować a i b (jeśli można udowodnić, że żaden inny wątki odczytują zmienne i nie są związane z macierzą sprzętową (więc w tym przypadku można je w rzeczywistości usunąć). Zauważ, że standard nie wymaga określonego zachowania, ale raczej postrzegalnego stanu z regułą as-if.

 3
Author: David Rodríguez - dribeas,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-03-31 11:07:07

Pytanie 3: CRITICAL_SECTIONs i Mutexes działają, prawie tak samo. Win32 mutex jest obiektem jądra, więc może być współdzielony między procesami i czekał z obiektami WaitForMultipleObjects, czego nie można zrobić z CRITICAL_SECTION. Z drugiej strony, CRITICAL_SECTION jest lżejsze, a przez to szybsze. Ale logika kodu nie powinna mieć wpływu na to, czego używasz.

Skomentowałeś również, że " nie ma potrzeby tworzenia sekcji krytycznej dla każdej zmiennej, którą chcesz Chroń, jeśli jesteś w krytycznej sekcji, nic innego nie może Ci przeszkodzić."To prawda, ale kompromis polega na tym, że dostęp do którejkolwiek ze zmiennych wymaga przytrzymania tej blokady. Jeśli zmienne mogą być znacząco aktualizowane niezależnie, tracisz możliwość równoległego wykonywania tych operacji. (Ponieważ są to jednak członkowie tego samego obiektu, zastanowiłbym się, zanim doszedłbym do wniosku, że naprawdę można do nich uzyskać dostęp niezależnie od siebie.)

 2
Author: Andy Mortimer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-03-31 12:28:47