Koncepcje programowania/Wielowątkowość

Z Wikibooks, biblioteki wolnych podręczników.

Wielowątkowość to sposób na to, aby Twój komputer robił wiele rzeczy w tym samym czasie. Wyobraź sobie, że masz pudełko na zabawki z wieloma zabawkami w środku i chcesz bawić się wszystkimi zabawkami naraz. Wielowątkowość jest jak posiadanie wielu rąk, które mogą jednocześnie bawić się różnymi zabawkami.

W programowaniu wątek jest jak zestaw instrukcji, które komputer wykonuje, aby wykonać zadanie. Wielowątkowość umożliwia jednoczesne uruchamianie wielu wątków, dzięki czemu komputer może wykonywać wiele czynności jednocześnie, podobnie jak bawienie się wieloma zabawkami jednocześnie.

Załóżmy na przykład, że chcesz pobrać duży plik i jednocześnie grać w grę wideo. Dzięki wielowątkowości możesz utworzyć jeden wątek do pobierania pliku i inny wątek do grania w grę wideo. W ten sposób oba zadania mogą działać jednocześnie i możesz cieszyć się grą wideo, podczas gdy plik jest pobierany w tle.

Wielowątkowość może sprawić, że programy będą działać szybciej i wydajniej, zwłaszcza w przypadku zadań wymagających oczekiwania lub przetwarzania dużych ilości danych. Jednak może to być również trudne w użyciu, ponieważ wiele wątków może kolidować ze sobą, jeśli nie są odpowiednio zsynchronizowane.

Aby korzystać z wielowątkowości w swoich programach, musisz nauczyć się tworzyć wątki i zarządzać nimi, a także synchronizować ich wykonywanie. Należy również zdawać sobie sprawę z potencjalnych problemów, takich jak warunki wyścigu i zakleszczenia, które mogą wystąpić, gdy wiele wątków próbuje uzyskać dostęp do tych samych zasobów w tym samym czasie.

Ogólnie rzecz biorąc, wielowątkowość to potężne narzędzie do poprawy wydajności i szybkości reakcji programów. Pozwala Twojemu komputerowi robić wiele rzeczy naraz, tak jakbyś bawił się wieloma zabawkami jednocześnie. Ucząc się prawidłowego korzystania z wielowątkowości, możesz sprawić, że Twoje programy będą wydajniejsze i przyjemniejsze w użyciu.

Zaletą takiego rozwiązania jest to, że może poprawić wydajność i responsywność aplikacji, oczywiście pod warunkiem że odpowiednio ją zaprojektujemy i że nasz język programowania to wspiera, ponieważ nie zawsze jest wbudowany w język programowania i zostaje osiągany różnymi obejściami tego problemu.

Wadą, oczywiście oprócz bardziej skomplikowanego kodu, to fakt że wątki mogą czasami konkurować o zasoby. Na przykład, masz 2 różne wątki, główny i dodatkowy i obie jednocześnie próbują zapisać dane do tego samego pliku - w rezultacie może pojawić się konflikt i żaden z nich nie uzyska dostępu do zasobu.

Python[edytuj]

Przykład wielowątkowości w języku Python:

import threading

def print_numbers():
    for i in range(1, 11):
        print(i)

def print_letters():
    for letter in 'abcdefghij':
        print(letter)

if __name__ == '__main__':
    t1 = threading.Thread(target=print_numbers)
    t2 = threading.Thread(target=print_letters)
    
    t1.start()
    t2.start()
    
    t1.join()
    t2.join()

W tym przykładzie definiujemy dwie funkcje print_numbers i print_letters, z których każda drukuje odpowiednio sekwencję cyfr lub liter. Następnie tworzymy dwa wątki t1 i t2 przy użyciu klasy Thread z modułu Threading. Każdemu wątkowi przekazujemy funkcję docelową do uruchomienia, którą jest print_numbers lub print_letters.

Uruchamiamy oba wątki za pomocą metody start(), która każe każdemu wątkowi rozpocząć wykonywanie swojej funkcji docelowej. Wątki działają niezależnie od siebie, dzięki czemu mogą jednocześnie wykonywać swoje funkcje docelowe. Następnie używamy metody join() w celu oczekiwania na zakończenie obu wątków przed wyjściem z programu.

Kiedy uruchomimy ten program, powinniśmy zobaczyć liczby od 1 do 10 i litery od a do j wydrukowane w przeplatanej kolejności, demonstrując równoległe wykonywanie dwóch wątków.

Należy pamiętać, że podczas korzystania z wielowątkowości należy zachować ostrożność, aby uniknąć warunków wyścigu i innych problemów z współbieżnością, które mogą wystąpić, gdy wiele wątków uzyskuje dostęp do współdzielonych zasobów. W tym przykładzie nie mamy żadnych współdzielonych zasobów, więc nie musimy się martwić o synchronizację. Jednak w bardziej złożonych programach odpowiednia synchronizacja między wątkami jest niezbędna do zapewnienia poprawnego działania.

JavaScript[edytuj]

Przykład wielowątkowości w języku javascript:

// Main thread
console.log('Starting main thread');

// Create a new worker thread
const worker = new Worker('worker.js');

// Send a message to the worker thread
worker.postMessage('Hello from the main thread!');

// Listen for messages from the worker thread
worker.onmessage = function(event) {
    console.log('Received message from worker thread:', event.data);
};

// Worker thread
console.log('Starting worker thread');

// Listen for messages from the main thread
self.onmessage = function(event) {
    console.log('Received message from main thread:', event.data);
    
    // Do some work
    const result = event.data + ' - this is the worker thread!';
    
    // Send a message back to the main thread
    self.postMessage(result);
};

W tym przykładzie mamy dwa wątki: wątek główny i wątek roboczy. Główny wątek tworzy nowy wątek roboczy przy użyciu konstruktora Worker, przekazując nazwę pliku JavaScript (worker.js), który zawiera kod do uruchomienia w wątku roboczym. Następnie główny wątek wysyła komunikat do wątku roboczego za pomocą metody postMessage.

Wątek roboczy nasłuchuje komunikatów z wątku głównego za pomocą procedury obsługi zdarzeń onmessage. Po otrzymaniu wiadomości wykonuje pewną pracę (w tym przypadku dołącza ciąg znaków do wiadomości), a następnie odsyła wiadomość z powrotem do głównego wątku za pomocą metody postMessage.

Wątek główny nasłuchuje komunikatów z wątku roboczego za pomocą procedury obsługi zdarzeń onmessage i rejestruje komunikaty w konsoli.

Kiedy uruchomimy ten program, powinniśmy zobaczyć następujące dane wyjściowe:

Starting main thread
Starting worker thread
Received message from main thread: Hello from the main thread!
Received message from worker thread: Hello from the main thread! - this is the worker thread!

Pokazuje to, że wątek główny i wątek roboczy działają jednocześnie i mogą komunikować się ze sobą za pomocą komunikatów. Należy zauważyć, że wątek roboczy działa w oddzielnym kontekście JavaScript, co oznacza, że nie może uzyskać dostępu do zmiennych globalnych lub funkcji zdefiniowanych w głównym wątku i odwrotnie.

Emacs lisp[edytuj]

Emacs Lisp nie ma wbudowanej obsługi wielowątkowości.

Współbieżność kooperacyjna[edytuj]

Istnieją jednak dostępne biblioteki, które umożliwiają symulowanie wielowątkowości przy użyciu współbieżności kooperacyjnej.

Oto przykładowy program, który symuluje wielowątkowość przy użyciu biblioteki cl-lib w Emacs Lisp:

(require 'cl-lib)

(defun print-numbers ()
  (cl-loop for i from 1 to 10 do
           (message "Number: %d" i)
           (sleep-for 1)))

(defun print-letters ()
  (cl-loop for letter from ?a to ?j do
           (message "Letter: %c" letter)
           (sleep-for 1)))

;; Define a cooperative concurrency function
(defun run-in-thread (fn)
  (lexical-let ((fn fn))
    (cl-flet ((run () (funcall fn)))
      (cl-letf ((inhibit-message t))
        (cl-loop while (condition-case nil (run) (quit nil))
                 do (accept-process-output nil 0.1))))))

;; Run the functions in separate threads
(run-in-thread #'print-numbers)
(run-in-thread #'print-letters)

;; Wait for the threads to complete
(sit-for 11)

W tym przykładzie definiujemy dwie funkcje print-numbers i print-letters, które drukują sekwencję odpowiednio cyfr lub liter. Następnie definiujemy kooperacyjną funkcję współbieżności uruchamianą w wątku, która przyjmuje funkcję jako argument i uruchamia ją w osobnym „wątku” (właściwie w oddzielnej pętli) za pomocą makra cl-loop.

Aby zasymulować wielowątkowość, uruchamiamy obie funkcje w osobnych „wątkach” za pomocą run-in-thread. Wątki działają wspólnie, co oznacza, że na zmianę wykonują swój kod. Aby wątki miały czas na wykonanie, w każdej pętli wstawiamy 1-sekundowe opóźnienie (sleep-for 1).

Następnie czekamy na zakończenie wątków, dzwoniąc (sit-for 11), który czeka 11 sekund (dłużej niż najdłuższe opóźnienie w wątkach) przed powrotem.

Gdy uruchomimy ten program w Emacsie, powinniśmy zobaczyć cyfry od 1 do 10 i litery od a do j wydrukowane w przeplatanej kolejności, demonstrując kooperacyjną współbieżność dwóch „wątków”. Należy zauważyć, że ponieważ współbieżność kooperacyjna nie jest prawdziwą wielowątkowością, wykonywanie dwóch „wątków” nie jest naprawdę równoległe i nie mogą one działać jednocześnie.

asynchroniczne wejście/wyjście[edytuj]

Emacs udostępnia kilka funkcji i bibliotek dla asynchronicznego wejścia/wyjścia, które umożliwiają inicjowanie operacji wejścia/wyjścia bez blokowania głównej pętli zdarzeń. Oto przegląd niektórych kluczowych funkcji i bibliotek dla asynchronicznego wejścia/wyjścia w Emacsie:

async-start: Ta funkcja umożliwia uruchomienie podprocesu asynchronicznego i uruchomienie funkcji wywołania zwrotnego po zakończeniu podprocesu. Podprocesem może być polecenie powłoki, funkcja Emacs Lisp lub program zewnętrzny.

start-file-process, make-network-process i make-process: Te funkcje umożliwiają tworzenie procesów asynchronicznych do wykonywania operacji we/wy na plikach, gniazdach sieciowych lub innych programach zewnętrznych. Możesz udostępnić funkcję filtrującą do przetwarzania danych wyjściowych procesu oraz opcjonalną funkcję wartowniczą, która będzie wywoływana po zakończeniu procesu.

process-send-string: Ta funkcja umożliwia asynchroniczne wysyłanie łańcucha do procesu, bez blokowania głównej pętli zdarzeń. Możesz użyć tej funkcji do wysyłania poleceń lub danych wejściowych do zewnętrznego programu lub procesu.

network-stream: ta biblioteka zapewnia interfejs wyższego poziomu do tworzenia połączeń sieciowych i wykonywania na nich operacji wejścia/wyjścia asynchronicznie. Możesz utworzyć strumień sieciowy za pomocą make-network-process, a następnie użyć funkcji filter-process do przetworzenia danych wyjściowych ze strumienia.

Oto prosty przykład pokazujący, jak używać asynchronicznego wejścia/wyjścia w Emacs Lisp do asynchronicznego odczytywania zawartości pliku:

;; Define a callback function to process the contents of the file.
(defun process-file-contents (contents)
  (message "File contents: %s" contents))

;; Start reading the file asynchronously.
(async-start
 (lambda ()
   (with-temp-buffer
     (insert-file-contents "filename.txt")
     (buffer-string)))
 process-file-contents)

W tym przykładzie definiujemy funkcję wywołania zwrotnego o nazwie „process-file-contents”, która przetwarza zawartość pliku asynchronicznie. Następnie używamy funkcji async-start, aby rozpocząć asynchroniczne odczytywanie pliku w oddzielnym procesie. Pierwszym argumentem funkcji async-start jest funkcja, która odczytuje zawartość pliku i zwraca ją jako łańcuch znaków. Drugim argumentem jest funkcja wywołania zwrotnego, która zostanie wywołana z zawartością pliku, gdy plik zostanie odczytany.

Po wykonaniu tego kodu plik zostanie odczytany asynchronicznie w tle, a funkcja Process-File-Contents zostanie wywołana wraz z zawartością pliku, gdy będzie ona dostępna. W międzyczasie główna pętla zdarzeń będzie nadal przetwarzać inny kod Lispa i zdarzenia wejściowe, zapewniając responsywność Emacsa.

przetwarzanie wieloprocesowe[edytuj]

Emacs udostępnia kilka funkcji do uruchamiania procesów zewnętrznych i komunikowania się z nimi, które można wykorzystać do wykonywania długotrwałych obliczeń lub operacji wejścia/wyjścia w oddzielnym procesie i uniknięcia blokowania głównej pętli zdarzeń. Oto przegląd niektórych kluczowych funkcji przetwarzania wieloprocesowego w Emacsie:

start-process: Ta funkcja umożliwia uruchomienie zewnętrznego procesu i opcjonalnie podłączenie go do bufora wejściowego i wyjściowego. Możesz udostępnić funkcję filtrującą do przetwarzania danych wyjściowych procesu oraz opcjonalną funkcję wartowniczą, która będzie wywoływana po zakończeniu procesu.

process-send-string: Ta funkcja umożliwia asynchroniczne wysyłanie ciągu znaków do procesu zewnętrznego, bez blokowania głównej pętli zdarzeń.

process-send-region i process-send-region-and-detect-eof: Te funkcje umożliwiają asynchroniczne wysyłanie regionu tekstu do procesu zewnętrznego.

process-query-on-exit-flag: Ta funkcja pozwala sprawdzić, czy proces zewnętrzny zakończył działanie.

make-pipe-process: Ta funkcja umożliwia utworzenie procesu potokowego, który łączy wejście i wyjście dwóch procesów zewnętrznych.

Oto prosty przykład pokazujący, jak używać przetwarzania wieloprocesowego w Emacs Lisp do wykonywania długotrwałych obliczeń w osobnym procesie:

;; Define a function that performs a long-running computation.
(defun long-computation (arg)
  (sleep-for 10)  ; simulate a long computation
  (* arg arg))

;; Start the computation in a separate process.
(let ((process (start-process "long-computation" "*long-computation*" "emacs" "--batch" "--eval" "(prin1-to-string (long-computation 1234))")))
  (set-process-sentinel process (lambda (process event)
                                  (when (string= event "finished\n")
                                    (with-current-buffer (process-buffer process)
                                      (goto-char (point-min))
                                      (let ((result (read (current-buffer))))
                                        (message "Result: %s" result)))))))

W tym przykładzie definiujemy funkcję o nazwie „długie obliczenia”, która wykonuje długotrwałe obliczenia (w tym przypadku uśpienie przez 10 sekund, po którym następuje podniesienie argumentu do kwadratu). Następnie używamy funkcji start-process, aby rozpocząć obliczenia w oddzielnym procesie Emacsa. Funkcja start-process przyjmuje kilka argumentów: nazwę procesu, bufor do wejścia i wyjścia (w tym przypadku tworzymy nowy bufor) oraz polecenie i argumenty do wykonania w procesie zewnętrznym (w tym przypadku , uruchamiamy Emacsa w trybie wsadowym i oceniamy wyrażenie Lispa, które wywołuje funkcję „długich obliczeń” i wyświetla wynik). Następnie ustawiamy funkcję wartowniczą dla procesu, która zostanie wywołana, gdy proces się zakończy. W funkcji wartowniczej sprawdzamy, czy proces zakończył się pomyślnie (poprzez sprawdzenie zdarzenia „zakończony\n”), a jeśli tak, odczytujemy wynik z bufora procesu i wyświetlamy go w komunikacie.

Kiedy ten kod zostanie wykonany, długotrwałe obliczenia zostaną uruchomione w oddzielnym procesie Emacsa, a główna pętla zdarzeń będzie kontynuować przetwarzanie innego kodu Lispa i zdarzeń wejściowych, zapewniając responsywność Emacsa. Po zakończeniu obliczeń wynik zostanie wyświetlony w komunikacie.

Zadania[edytuj]

  1. Napisz program, który używa wielowątkowości do obliczania sumy dużej listy liczb. Podziel listę na równe części i niech każdy wątek zsumuje swoją część listy. Połącz wyniki każdego wątku, aby uzyskać ostateczną sumę.
  2. Napisz program, który wykorzystuje wielowątkowość do jednoczesnego pobierania wielu plików z Internetu. Utwórz osobny wątek dla każdego pobrania i użyj synchronizacji, aby upewnić się, że każdy wątek zakończy pobieranie przed przejściem do następnego pliku.
  3. Napisz program, który używa wielowątkowości do wyszukiwania określonego podłańcucha w dużym pliku tekstowym. Podziel plik na równe części i poproś każdy wątek o przeszukanie swojej części pliku. Połącz wyniki każdego wątku, aby określić, czy podłańcuch został znaleziony iw jakiej pozycji.
  4. Napisz program, który używa wielowątkowości do wykonywania złożonych obliczeń na dużym zbiorze danych. Użyj procesów roboczych sieci Web, aby utworzyć osobne wątki i przekazać zestaw danych między wątkami. Połącz wyniki każdego wątku, aby uzyskać ostateczne dane wyjściowe.
  5. Napisz program, który wykorzystuje wielowątkowość do przetwarzania dużej ilości danych z bazy danych. Użyj puli wątków, aby ograniczyć liczbę tworzonych wątków i aby każdy wątek przetwarzał partię danych. Użyj kolejki do zarządzania elementami pracy i synchronizacji dostępu do bazy danych.