Wydajność wątków w C#

W języku C# mamy kilka mechanizmów tworzenia wątków. Różnią się one zarówno wydajnością jak i przeznaczeniem.

Zacznijmy więc od najpopularniejszego sposobu a mianowicie klasy System.Threading.Thread. Stworzenie wątku polega na inicjalizacji klasy oraz wywołania metody Start:

public class ThreadExample
{
    public CreateThread()
    {            
        System.Threading.Thread thread = new System.Threading.Thread(ThreadMethod);
        thread.Start(null);
    }
    private void ThreadMethod(object parameters)
    {
        while(true)
        {
            // jakiś kod
        }
    }
}

ThreadMethod zawiera kod, który chcemy wykonać współbieżnie. Warto także wspomnieć o właściwości Thread.IsBackground. Gdy ustawiamy ją na true(domyślnie jest false), wtedy wątek stanie się tzw. “background thread” – wątkiem zależnym od rodzica. W sytuacji gdy użytkownik zamknie naszą aplikację, automatycznie zostaną zamknięte wszystkie wątki “background”. Gdybyśmy nie ustawili IsBackground na true, po zamknięciu aplikacji, stworzone przez nas wątki nadal by pracowały, uniemożliwiając tym samym prawidłowe zamknięcie programu.

Najważniejszą jednak rzeczą, którą chciałem poruszyć w poście jest wydajność. Tworzenie klas Thread jest bardzo czasochłonne. O ile nie piszemy programu z naprawdę dużą ilością wątków, to podejście polegające na użyciu klasy Thread jest jednym z najgorszych rozwiązań. Związane jest to z długim czasem wymaganym na inicjalizacje tej klasy. Użycie Thread jest dobre gdy tworzymy pule wątków, które działają przez cały czas działania aplikacji.

Gdy tworzymy często nowe wątki, znacznie lepszym rozwiązaniem jest skorzystanie z gotowej puli wątków, które czekają już zainicjowane na użycie:

public class ThreadPoolExample
{
    public ThreadPoolExample()
    {        
        System.Threading.ThreadPool.QueueUserWorkItem(ThreadPoolMethod);
    }
    private void ThreadPoolMethod(object state) 
    {
        while(true)
        {
            // jakiś kod
        }
    }
}

Wywołując metodę QueueUserWorkItem system sprawdza czy w puli wątków jest wolny wątek .Jeśli tak, zwraca nam go i przechodzi do wykonywania kodu współbieżnego. Wątki zatem nie są tworzone za każdym razem od nowa – zawsze zwracane nam są już gotowe wątki do użycia. Gdy skończymy wykonywanie kodu współbieżnego, wątek nie zostaje zniszczony a wyłącznie zwrócony z powrotem do puli.

Dodatkowo warto wspomnieć, że wszystkie wątki z puli są typu “background”.

Gdy tworzymy interfejs użytkownika i chcemy jakaś operację wykonać w tle(np. połączenie się z WCF) warto zastanowić się nad klasą  BackgroundWorker. Przede wszystkim jest ona bardzo łatwo w użyciu(można ją nawet przeciągnąć z toolbox’a jako zwykłą kontrolkę) oraz posiada kilka zdarzeń, które są istotne dla GUI – np. powiadomienie o postępie prac. Krótki przykład:

public partial class Form1 : Form
{
   public Form1()
   {
       InitializeComponent();
       BackgroundWorker backgroundWorker = new BackgroundWorker();
       backgroundWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(backgroundWorker_RunWorkerCompleted);
       backgroundWorker.ProgressChanged += new ProgressChangedEventHandler(backgroundWorker_ProgressChanged);
       backgroundWorker.RunWorkerAsync();
   }

   void backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
   {
       // e.ProgressPercentage
   }

   void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
   {
       while (true)
       {
           // jakiś kod kod
       }
   }        
}

Jeśli chcemy wykonywać pewną operacje co jakiś czas np. aktualizację lokalnej bazy danych z danymi otrzymywanymi przez usługę sieciową warto użyć timer’a:

public partial class Form1 : Form
{
   public Form1()
   {
       InitializeComponent();
       System.Threading.Timer timer = new System.Threading.Timer(ThreadMethod,null,0,1000);
       
   }
   private void ThreadMethod(object state)
   {
        // jakiś kod
   }
}

Jak widać ten specjalny timer znajduje się w przestrzeni nazw System.Threading. Metoda ThreadMethod będzie wykonywana współbieżnie co określony czas, przekazany w ostatnim parametrze konstruktora.

3 thoughts on “Wydajność wątków w C#”

  1. – użycie ThreadPool, ma jedną wadę, nie można ustawić
    ApartmentState dla wątku
    – w artykule warto by też wspomnieć o asynchronicznym wywoływaniu delegatów za pomocą BeginInvoke
    – po za tym warto by przytoczyć jakieś wyniki dot. wydajności skoro o tym mowa w artykule

  2. Witam,
    Co do pierwszego punktu to oczywiście racja.
    Nie pisałem o BeginInvoke bo stwierdziłem, że nie ma tam nic nadzwyczajnego do pokazania.
    Co do ostatniego punktu to zachęcam do zajrzenia do książki „More Effective C# 50 Specific Ways to Improve Your C#”. Autor przeprowadził tam kilka badań i dostarcza również wykresy wydajności.
    To tylko post – nie artykuł.

    Dzięki za sugestie!

  3. Dzięki za polecenie książki, choć ostatnio jakiś złośliwiec powiedział mi że nie warto czytać książek, bo to tylko zebrane artykuły i teraz tak sobie myślę że może artykuł to tylko zebrane posty 😉

Leave a Reply

Your email address will not be published.