Od jakiegoś czasu programiści platformy .NET, a właściwie jej wersji nr 4.0 mają możliwość zastosowania biblioteki Task Parallel Library. Jej najpopularniejszym elementem jest współbieżna pętla For, którą bardzo intuicyjnie się obsługuje. Taką równoległą pętlę otrzymujemy dzięki metodzie Parallel.For().
Okazuje się, że eksploracja publicznie dostępnych bibliotek jest bardzo kształcąca. Podczas dopisywania własnych funkcji do biblioteki Accord.net związanych z segmentacją SLIC zauważyłem, że bardzo często autorzy używają równoległych pętli. To logiczne, że proste operacje wykonywane na każdym pikselu z osobna będą o wiele szybsze jeśli uruchomimy je równolegle. Dotychczas trochę obawiałem się stosowania współbieżności, gdyż jest to jednak związane z wieloma zagrożeniami a poza tym nigdy nie było takiej potrzeby. Zatem w ramach projektu Flover czas się z tym zaprzyjaźnić, bo już wiem, że się bardzo opłaca.
Najczęściej używana przeze mnie wersja metody Parallel.For ma następującą definicję
public static ParallelLoopResult For( int fromInclusive, int toExclusive, Action<int> body )
Dwa pierwsze parametry określają zakres zmiany indeksu pętli. Trzeci parametr jest delegatem, który bardzo często podaje się w postaci wyrażenia lambda. To tam znajduje się właściwa część pętli, której zawartość jest identyczna do normalnego Fora. Więc jeśli chcielibyśmy zamienić klasyczną pętlę for, która np. dodaje 1 do każdego elementu tablicy:
for (int i = 0; i < table.Length; i++) { table[i]++; }
nasza zrównoleglona pętla będzie wyglądała tak:
Parallel.For(0, table.Length, i => { table[i]++; });
Dla tak prostej operacji może nie będzie się to zbytnio opłacało czasowo, ale to tylko przykład. Spójrzmy jak wygląda implementacja Parallel.For w moim projekcie. Chodzi tu o to, żeby przypisać każdemu pikselowi z tablicy data znacznik segmentu z tablicy label. Następnie, dla centrów segmentów z tablicy newCentroids kumulujemy wartości pikseli do nich należących.
Parallel.For(0, data.Length, i => { double[] point = data[i]; double weight = weights[i]; int label = labels[i]; double[] centroid = newCentroids[label]; lock (syncObjects[label]) { count[label] += weight; for (int j = 0; j < point.Length; j++) centroid[j] += point[j] * weight; } });
Zauważmy, że użyto tutaj wyrażenia lock (syncObjects[label]). Oznacza to, że operacje wykonywane w klamrach po wyrażeniu lock nie będą się wykonywały jednocześnie dla tej samej instancji obiektu label.
Na koniec zobaczmy jak użycie Parallel.For wpływa na moją segmentację obrazu. W jej implementacji znajduje się kilka takich pętli. Gdy wszystkie są klasycznymi Forami czas przetwarzania obrazu (419×500 pikseli) wynosi ok. 3.5 sekundy. Po zamianie pętli na ich współbieżne odpowiedniki czas skrócił się do 1 sekundy.
Niedługo umieszczę cały kod segmentacji na GitHubie. Niestety na razie konieczne są jeszcze małe poprawki, aby segmentacja działała niezawodnie.