Obliczenia równoległe w Microsoft Visual Basic Środowisko Microsoft Visual Studio 2010 i nowsze umożliwia zrównoleglenie obliczeń na wiele rdzeni procesorów. Służą do tego odpowiednie metody dostępne wraz z Framework 4.0 i nowszym. Przed rozpoczęciem tworzenia aplikacji umożliwiającej obliczenia równoległe, należy zaimportować odpowiednią przestrzeń nazw (będzie łatwiej odwoływać się do metod, kod poniżej). Imports System.Threading.Tasks Public Class Form1 Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click Import przestrzeni nazw: System.Threading.Tasks Poniżej przedstawione zostaną z sposób ogólny dwie istotne funkcje: Parallel.For oraz Parallel.ForEach 1. Wykonanie każdej sekwencji poleceń przez osobny wątek jest równoważne z pojedynczą iteracją tych pętli. Pętla For Poniżej znajdują się przykłady z wykorzystaniem pętli Parallel.For. Imports System.Threading.Tasks Public Class Form1 Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click Parallel.For(0, 3, Sub(x) MsgBox("Wywołany numer iteracji: " & x) End Sub End Class Użycie pętli Parallel.For 1 Funkcje pętli można wywoływać również bez odwoływania się do klasy Parallel. Należy w tym celu zaimportować ścieżkę: System.Threading.Tasks.Parallel.
Efekt użycia pętli Parallel.For dla powyższego kodu Powyższe okienka pojawiły się naraz w kolejności: 1, 0, 2. Po powtórnym wykonaniu kodu ta kolejność może być inna. Nie oznacza to jednak, że iteracja nie przebiegała w kolejności od zera do dwóch lub, że przebiegła. Uzależnione jest to m.in. od czasu wykonania kodu, ale nie tylko. Przykład poniżej przedstawia efekt tego zjawiska. Parallel.For(0, 3, Sub(x) Użycie pętli Parallel.For If x = 0 Then Threading.Thread.Sleep(2000) MsgBox("Wywołany numer iteracji: " & x & ", czas: " & Now.Hour & ":" & Now.Minute & ":" & Now.Second) Komunikat iteracji numer 0, pojawił się ostatni, ponieważ najdłużej trwało wykonanie kodu w tej iteracji zastosowanie funkcji Sleep() Pętla Parallel.For przyjmuje w argumentach następujące wartości: wartość początkową, wartość końcową (której nie osiąga 2 ), procedurę która zostanie wywołana odpowiednio dla poszczególnych rdzeni. Pierwszym parametrem 2 Jeżeli K oznacza parametr wartości końcowej pętli, to pętla osiągnie K-1 iterację.
wchodzącym do procedury jest numer iteracji (typ Integer). Drugim parametrem jest obiekt typu System.Threading.Tasks.ParallelLoopState32 umożliwiający korzystanie z dodatkowych opcji zarządzających pętlą. Przykłady poniżej prezentują kilka z nich. Parallel.For(0, 8, Sub(x, k) If x = 4 Then k.break() While k.lowestbreakiteration Is Nothing : End While MessageBox.Show("Informacja z iteracji nr " & x & _ ". Przerwano pętlę z iteracji nr " _ & k.lowestbreakiteration, "Przerwanie pętli", MessageBoxButtons.OK) Przykład użycia funkcji Break() i zmiennej LowestBreakIteration Funkcja Break() wywołana ze zmiennej k 3 przerywa działanie pętli po wykonaniu iteracji, w której ma miejsce wywołanie funkcji przerwania. Zmienna LowestBreakIteration umożliwia odczytanie najniższego numeru iteracji, z której nastąpiło przerwanie. Parallel.For(0, 3, Sub(x, k) k.stop() If k.isexceptional Then MsgBox("Wystąpił błąd", MsgBoxStyle.Critical) If k.isstopped Then MsgBox("Pętla została zatrzymana", vbinformation) MsgBox("Numer iteracji: " & x, vbinformation) Przykład użycia funkcji Stop() i zmiennych: IsStopped, IsExceptional Funkcja Stop() również zatrzymuje działanie pętli. Różnice pomiędzy obiema metodami są następujące: Break() nie pozwala na uruchomienie się iteracji wyższej od tej, w której została wywołana funkcja. Jeżeli iteracja numer 40 wywoła funkcję Break(), to numery dalsze iteracji 41,42,... nie zostaną wywołane, jednak niższe numery tak. Stop() nie pozwala na uruchomienie kolejnych iteracji niezależnie od ich numerów. Jeżeli istnieje potrzeba przerwania wykonującej się już iteracji, 3 Nazwy zmiennych (x i k) użyte w omawianych funkcjach mogą posiadać również inne nazwy zależy to od programisty.
można użyć zmiennej IsStopped (odpowiednio sprawdzając jej wartość logiczną) umieszczonej w wykonującej się iteracji. Ważną zmienną w pętli, jest zmienna IsExceptional, dzięki której istnieje możliwość sprawdzenia, czy w którejś z iteracji nie wystąpił nieprzechwycony błąd (wyjątek). Jeżeli wartość zmiennej wynosi True, to oznacza, że wystąpił. W takim przypadku w działającej pętli można podjąć odpowiednie kroki. Zamiast umiejscowienia procedury w trzecim parametrze można również wprowadzić adres procedury zadeklarowanej w innym miejscu w projekcie. Procedura, która jest wywoływana musi przyjmować parametr określający numer iteracji. Sub Proc(x As Integer) Console.WriteLine("Numer iteracji: " & x) End Sub Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click Parallel.For(0, 3, AddressOf Proc) End Sub Wywołanie procedury przez pętlę Parallel.For. Przekazanie adresu procedury
Pętla ForEach Kolejnym typem pętli służącej do zrównoleglenia obliczeń jest pętla Parallel.ForEach. Zasadnicza kwestia wykorzystania pętli jest taka sama jak pętli Parallel.For, natomiast sposób jej działania jest trochę inny. Dim tabl() As Integer = {3, 6, 9} Parallel.ForEach(tabl, Sub(w As Integer, k As ParallelLoopState, x As Long) Console.WriteLine("Wartość tabl(" & x & "): " & w _ & ". Numer iteracji: " & k.tostring) Przykład użycia pętli Parallel.ForEach Każda iteracja pętli wykonywana jest dla poszczególnych elementów tablicy (kolekcji). Za pośrednictwem pierwszego argumentu przekazanego do procedury istnieje możliwość pobrania wartości (zmienna w) elementu tablicy o indeksie równym numerze iteracji (zmienna x). Zmienna k umożliwia wykonywanie tych samych metod, co w przykładzie pętli Parallel.For. Środowisko umożliwia również sprawdzenie powodzenia wykonania pętli (dotyczy to obu omówionych pętli). Służy do tego zmienna typu: System.Threading.Tasks.ParallelLoopResult. Dim tabl() As Integer = {3, 6, 9} Dim wynik As ParallelLoopResult wynik = Parallel.ForEach(tabl, Sub(w, k, x) If wynik.iscompleted = False Then Console.WriteLine("Przerwano w iteracji nr: " & wynik.lowestbreakiteration) Else Console.WriteLine("Pętla wykonała się z powodzeniem!") End If Przykład sprawdzenia poprawności wykonania się pętli Jeżeli wartość zmiennej IsCompleted jest równa True to oznacza, że wykonywanie pętli zakończyło się powodzeniem. W przeciwnym wypadku zmienna przybierze wartość False. Ponadto istnieje możliwość pobrania wartości iteracji, w której nastąpiło wywołanie przerwania.
Problem dostępu do wspólnych danych Podczas budowy programów, w których do danej zmiennej ma dostęp w jednym czasie wiele wątków może powodować liczne błędy obliczeniowe jeżeli program zostanie źle napisany (tj. bez uwzględnienia charakteru obliczeń współbieżnych/równoległych). Kod poniżej jest przykładem błędnie napisanego programu. Dim zmienna As Integer = 0 Parallel.For(0, 2, Sub(x) If x = 0 Then For c = 1 To 500 : zmienna = zmienna + 1 : Next Else For c = 1 To 500 : zmienna = zmienna - 1 : Next End If Console.WriteLine("Zmienna wynosi: " & zmienna) Przykład błędnie napisanego programu Na pierwszy rzut oka ostatecznym wynikiem zwracanym w zmiennej zmienna powinna być wartość 0, gdyż pierwszy wątek inkrementuje wartość o jeden 500 razy, a drugi w tej samej chwili obniża wartość o jeden również 500 razy. Ostatecznie wynik może być inny. Poniżej znajduje się lista wyników, powstała poprzez wielokrotne uruchomienie po sobie procedury zawierającej powyższy kod. Zmienna wynosi: 11 Zmienna wynosi: -97 Zmienna wynosi: -3 Dlaczego tak się dzieje? Wyobraźmy sobie sytuację, w której mamy po jednej pechowej iteracji w każdym wątku, tzn. równolegle wykonywane są instrukcje: zmienna + 1 (wątek x=0) oraz zmienna 1 (wątek x=1), a następnie równolegle przypisywana jest nowa obliczona wartość do zmiennej zmienna, tzn.: zmienna = (wątek x=0) oraz zmienna = (wątek x=1). Jeżeli przyjmiemy, że wartość początkowa zmiennej wynosiła 0 (przed uruchomieniem obu iteracji), to po zakończeniu obu iteracji, wartość zmiennej będzie wynosiła: -1 lub 1, a nie 0! Problem można rozwiązać na kilka sposobów, np. poprzez sekcję krytyczną, o której będzie mowa w rozdziale poświęconym wątkom (jednakże ten sposób jest mniej efektywny i po części wypacza ideę obliczeń współbieżnych/równoległych) albo poprzez użycie tablicy, tak jak w przykładzie poniżej.
Dim zmienna() As Integer = {0, 0} Parallel.For(0, 2, Sub(x) If x = 0 Then For c = 1 To 500 : zmienna(x) = zmienna(x) + 1 : Next Else For c = 1 To 500 : zmienna(x) = zmienna(x) - 1 : Next End If Console.WriteLine("Zmienna wynosi: " & zmienna.sum) Przykład poprawnie napisanego programu w ramach omawianego problemu Artur Niewiarowski Wydział Fizyki, Matematyki i Informatyki Politechnika Krakowska Kraków 2014