Udostępnij za pośrednictwem


Wielowątkowość z językiem C i podsystemem Win32

Kompilator języka Microsoft C/C++ (MSVC) zapewnia obsługę tworzenia aplikacji wielowątków. Rozważ użycie więcej niż jednego wątku, jeśli aplikacja musi wykonywać kosztowne operacje, które mogłyby spowodować, że interfejs użytkownika przestanie odpowiadać.

W środowisku MSVC istnieje kilka sposobów programowania z wieloma wątkami: możesz użyć języka C++/WinRT i biblioteki środowisko wykonawcze systemu Windows, biblioteki klasy Microsoft Foundation (MFC), języka C++/interfejsu wiersza polecenia i środowiska uruchomieniowego platformy .NET albo biblioteki czasu wykonywania języka C i interfejsu API Win32. Ten artykuł dotyczy wielowątków w języku C. Przykładowy kod można znaleźć w temacie Przykładowy program wielowątkowy w języku C.

Programy wielowątkowe

Wątek jest w zasadzie ścieżką wykonywania za pośrednictwem programu. Jest to również najmniejsza jednostka wykonywania, którą harmonogramy Win32. Wątek składa się ze stosu, stanu rejestrów procesora CPU i wpisu na liście wykonywania harmonogramu systemu. Każdy wątek współudzieli wszystkie zasoby procesu.

Proces składa się z co najmniej jednego wątku oraz kodu, danych i innych zasobów programu w pamięci. Typowe zasoby programu to otwarte pliki, semafory i dynamicznie przydzielana pamięć. Program jest wykonywany, gdy harmonogram systemu daje jedną z jego kontroli wykonywania wątków. Harmonogram określa, które wątki powinny być uruchamiane i kiedy powinny być uruchamiane. Wątki o niższym priorytcie mogą czekać, gdy wątki o wyższym priorytcie zakończą swoje zadania. Na maszynach wieloprocesorowych harmonogram może przenosić poszczególne wątki do różnych procesorów, aby zrównoważyć obciążenie procesora CPU.

Każdy wątek w procesie działa niezależnie. Jeśli nie uwidocznisz ich nawzajem, wątki są wykonywane indywidualnie i nie są świadomi innych wątków w procesie. Wątki współużytkujące wspólne zasoby muszą jednak koordynować swoją pracę przy użyciu semaforów lub innej metody komunikacji międzyprocesowej. Aby uzyskać więcej informacji na temat synchronizowania wątków, zobacz Pisanie wielowątkowego programu Win32.

Obsługa bibliotek na potrzeby wielowątkowości

Wszystkie wersje CRT obsługują teraz wielowątkowość, z wyjątkiem nieblokujących wersji niektórych funkcji. Aby uzyskać więcej informacji, zobacz Wydajność bibliotek wielowątkowych. Aby uzyskać informacje na temat wersji CRT dostępnych do połączenia z kodem, zobacz Funkcje biblioteki CRT.

Uwzględnianie plików na potrzeby wielowątkowości

Standardowe CRT obejmują pliki deklarujące funkcje biblioteki czasu wykonywania języka C, ponieważ są one implementowane w bibliotekach. Jeśli opcje kompilatora określają __fastcall lub __vectorcall konwencje wywoływania, kompilator zakłada, że wszystkie funkcje powinny być wywoływane przy użyciu konwencji wywoływania rejestru. Funkcje biblioteki czasu wykonywania używają konwencji wywoływania języka C, a deklaracje w standardzie zawierają pliki informują kompilator o generowaniu poprawnych odwołań zewnętrznych do tych funkcji.

Funkcje CRT do sterowania wątkami

Wszystkie programy Win32 mają co najmniej jeden wątek. Każdy wątek może tworzyć dodatkowe wątki. Wątek może szybko zakończyć swoją pracę, a następnie zakończyć pracę lub może pozostać aktywny na całe życie programu.

Biblioteki CRT zapewniają następujące funkcje tworzenia i kończenia wątków: _beginthread, _beginthreadex, _endthread i _endthreadex.

Funkcje _beginthread i _beginthreadex tworzą nowy wątek i zwracają identyfikator wątku, jeśli operacja zakończy się pomyślnie. Wątek kończy się automatycznie, jeśli zakończy wykonywanie. Może też zakończyć się wywołaniem metody _endthread lub _endthreadex.

Uwaga

Jeśli wywołasz procedury czasu wykonywania języka C z poziomu programu utworzonego za pomocą biblioteki libcmt.lib, musisz uruchomić wątki za pomocą _beginthread funkcji or _beginthreadex . Nie używaj funkcji ExitThread Win32 i CreateThread. Użycie SuspendThread może prowadzić do zakleszczenia, gdy więcej niż jeden wątek jest zablokowany, czekając na zawieszony wątek, aby ukończyć dostęp do struktury danych czasu wykonywania języka C.

Funkcje _beginthread i _beginthreadex

Funkcje _beginthread i _beginthreadex tworzą nowy wątek. Wątek współużytkuje kod i segmenty danych procesu z innymi wątkami w procesie, ale ma własne unikatowe wartości rejestru, przestrzeń stosu i bieżący adres instrukcji. System daje procesor CPU na każdy wątek, dzięki czemu wszystkie wątki w procesie mogą być wykonywane współbieżnie.

_beginthread i _beginthreadex są podobne do funkcji CreateThread w interfejsie API Win32, ale mają następujące różnice:

  • Inicjują pewne zmienne biblioteki czasu wykonywania języka C. Jest to ważne tylko wtedy, gdy używasz biblioteki czasu wykonywania języka C w wątkach.

  • CreateThread pomaga zapewnić kontrolę nad atrybutami zabezpieczeń. Za pomocą tej funkcji można uruchomić wątek w stanie wstrzymania.

_beginthread i _beginthreadex zwróć uchwyt do nowego wątku, jeśli działanie powiodło się lub kod błędu, jeśli wystąpił błąd.

Funkcje _endthread i _endthreadex

Funkcja _endthread kończy wątek utworzony przez _beginthread program (i podobnie _endthreadex przerywa wątek utworzony przez _beginthreadexprogram ). Wątki kończą się automatycznie po zakończeniu. _endthread i _endthreadex są przydatne w przypadku kończenia warunkowego z poziomu wątku. Wątek przeznaczony do przetwarzania komunikacji może na przykład zakończyć działanie, jeśli nie może uzyskać kontroli nad portem komunikacji.

Pisanie wielowątkowego programu Win32

Podczas pisania programu z wieloma wątkami należy koordynować ich zachowanie i używać zasobów programu. Upewnij się również, że każdy wątek otrzymuje własny stos.

Udostępnianie typowych zasobów między wątkami

Uwaga

Podobne omówienie z punktu widzenia MFC można znaleźć w temacie Multithreading: Programming Tips and Multithreading: When to Use the Synchronization Classes (MFC: Porady dotyczące programowania i wielowątkowość: kiedy należy używać klas synchronizacji).

Każdy wątek ma własny stos i własną kopię rejestrów procesora CPU. Inne zasoby, takie jak pliki, dane statyczne i pamięć stert, są współużytkowane przez wszystkie wątki w procesie. Wątki korzystające z tych typowych zasobów muszą być synchronizowane. Win32 oferuje kilka sposobów synchronizowania zasobów, w tym semaforów, sekcji krytycznych, zdarzeń i muteksów.

Gdy dostęp do danych statycznych uzyskuje wiele wątków, program musi zapewnić możliwe konflikty zasobów. Rozważmy program, w którym jeden wątek aktualizuje statyczną strukturę danych zawierającą współrzędne x,y dla elementów, które mają być wyświetlane przez inny wątek. Jeśli wątek aktualizacji zmienia współrzędną x i jest wywłaszczone przed zmianą współrzędnej y , wątek wyświetlania może być zaplanowany przed zaktualizowaną współrzędną y . Element będzie wyświetlany w niewłaściwej lokalizacji. Można uniknąć tego problemu, używając semaforów do kontrolowania dostępu do struktury.

Mutex (skrót od mutual exclusion) to sposób komunikowania się między wątkami lub procesami, które wykonują asynchronicznie siebie. Ta komunikacja może służyć do koordynowania działań wielu wątków lub procesów, zazwyczaj przez kontrolowanie dostępu do udostępnionego zasobu przez blokowanie i odblokowywanie zasobu. Aby rozwiązać ten problem z aktualizacją współrzędnych x,y, wątek aktualizacji ustawia mutex wskazujący, że struktura danych jest używana przed wykonaniem aktualizacji. Pozwoliłoby to wyczyścić mutex po przetworzeniu obu współrzędnych. Wątek wyświetlania musi czekać, aż mutex będzie jasny przed zaktualizowaniem wyświetlacza. Ten proces oczekiwania na mutex jest często nazywany blokowaniem na mutexie, ponieważ proces jest blokowany i nie może kontynuować, dopóki mutex nie wyczyści.

Program Bounce.c pokazany w przykładowym wielowątkowym programie C używa mutexu o nazwie ScreenMutex w celu koordynowania aktualizacji ekranu. Za każdym razem, gdy jeden z wątków wyświetlania jest gotowy do zapisu na ekranie, wywołuje WaitForSingleObject uchwyt do ScreenMutex i stałą nieskończoność, aby wskazać, że WaitForSingleObject wywołanie powinno blokować na mutexie, a nie przekroczenie limitu czasu. Jeśli ScreenMutex jest jasne, funkcja wait ustawia mutex, aby inne wątki nie mogły zakłócać wyświetlania i kontynuować wykonywanie wątku. W przeciwnym razie wątek blokuje się do momentu wyczyszczenia mutexu. Po zakończeniu aktualizacji wyświetlania wątek zwalnia mutex przez wywołanie metody ReleaseMutex.

Ekran wyświetla i dane statyczne to tylko dwa zasoby wymagające starannego zarządzania. Na przykład program może mieć wiele wątków, które uzyskują dostęp do tego samego pliku. Ponieważ inny wątek mógł przenieść wskaźnik pliku, każdy wątek musi zresetować wskaźnik pliku przed odczytaniem lub zapisem. Ponadto każdy wątek musi upewnić się, że nie jest on wywłaszany między momentem, w którym umieszcza wskaźnik i czas uzyskiwania dostępu do pliku. Te wątki powinny używać semafora do koordynowania dostępu do pliku przez nawiasy dostępu do każdego pliku za WaitForSingleObject pomocą metod i ReleaseMutex wywołań. Poniższy przykład kodu ilustruje tę technikę:

HANDLE    hIOMutex = CreateMutex (NULL, FALSE, NULL);

WaitForSingleObject( hIOMutex, INFINITE );
fseek( fp, desired_position, 0L );
fwrite( data, sizeof( data ), 1, fp );
ReleaseMutex( hIOMutex);

Stosy wątków

Cała domyślna przestrzeń stosu aplikacji jest przydzielana do pierwszego wątku wykonywania, który jest znany jako wątek 1. W związku z tym należy określić ilość pamięci do przydzielenia dla oddzielnego stosu dla każdego dodatkowego wątku wymaganego przez program. W razie potrzeby system operacyjny przydziela dodatkowe miejsce na stos dla wątku, ale należy określić wartość domyślną.

Pierwszym argumentem wywołania _beginthread jest wskaźnik do BounceProc funkcji, która wykonuje wątki. Drugi argument określa domyślny rozmiar stosu wątku. Ostatnim argumentem jest numer identyfikatora, który jest przekazywany do BounceProcelementu . BounceProc używa numeru ID, aby zainicjować generator liczb losowych i wybrać atrybut koloru wątku i znak wyświetlania.

Wątki, które tworzą wywołania biblioteki czasu wykonywania języka C lub interfejsu API Win32, muszą zezwalać na wystarczającą ilość miejsca na wywoływane funkcje biblioteki i interfejsu API. Funkcja C printf wymaga więcej niż 500 bajtów przestrzeni stosu i powinna mieć 2K bajtów miejsca na stosie dostępne podczas wywoływania procedur interfejsu API Win32.

Ponieważ każdy wątek ma własny stos, można uniknąć potencjalnych kolizji elementów danych przy użyciu jak najmniejszej ilości danych statycznych. Zaprojektuj program tak, aby używał automatycznych zmiennych stosu dla wszystkich danych, które mogą być prywatne dla wątku. Jedynymi zmiennymi globalnymi w programie Bounce.c są mutexes lub zmienne, które nigdy nie zmieniają się po ich zainicjowaniu.

Win32 udostępnia również magazyn lokalny wątku (TLS) do przechowywania danych poszczególnych wątków. Aby uzyskać więcej informacji, zobacz Magazyn lokalny wątku (TLS).

Unikanie obszarów problemów z programami wielowątkowymi

Istnieje kilka problemów, które mogą wystąpić podczas tworzenia, łączenia lub wykonywania wielowątkowego programu C. Niektóre z bardziej typowych problemów opisano w poniższej tabeli. (Aby zapoznać się z podobną dyskusją z punktu widzenia MFC, zobacz Wielowątkowość: Porady dotyczące programowania).

Problem Prawdopodobna przyczyna
Zostanie wyświetlone okno komunikatu z informacją, że program spowodował naruszenie ochrony. Wiele błędów programowania Win32 powoduje naruszenia ochrony. Częstą przyczyną naruszeń ochrony jest pośrednie przypisanie danych do wskaźników o wartości null. Ponieważ powoduje to, że program próbuje uzyskać dostęp do pamięci, która nie należy do niego, zostanie wystawione naruszenie ochrony.

Łatwym sposobem wykrycia przyczyny naruszenia ochrony jest skompilowanie programu przy użyciu informacji debugowania, a następnie uruchomienie go za pośrednictwem debugera w środowisku programu Visual Studio. Gdy wystąpi błąd ochrony, system Windows przenosi kontrolkę do debugera, a kursor jest umieszczony w wierszu, który spowodował problem.
Program generuje wiele błędów kompilacji i linków. Możesz wyeliminować wiele potencjalnych problemów, ustawiając poziom ostrzeżenia kompilatora na jedną z jego najwyższych wartości i wyświetlając komunikaty ostrzegawcze. Korzystając z opcji poziomu 3 lub poziomu 4, można wykrywać niezamierzone konwersje danych, brakujące prototypy funkcji i korzystać z funkcji innych niż ANSI.

Zobacz też

Obsługa wielowątkowości w przypadku starszego kodu (Visual C++)
Przykładowy program wielowątkowy w języku C
Magazyn lokalny wątku (TLS)
Współbieżność i operacje asynchroniczne za pomocą języka C++/WinRT
Wielowątkowość z C++ i MFC