Pula wątków rozwiązuje dwa dosyć poważne problemy związane ze współbieżnością w .NET:
- każdy wątek domyślnie wykorzystuje około 1MB pamięci
- utworzenie nowego wątku wraz z organizacją pamięci dla niego trwa setki milisekund
Dzięki puli wątków zyskujemy automatyczne współdzielenie i odzyskiwanie już pracujących wątków. Ponadto kontrolowana jest maksymalna ilość wątków typu "worker" jakie mogą pracować w danej chwili. Zbyt duże ilości aktywnych wątków mogą zaburzać pracę systemu operacyjnego, co staje się niewygodne dla użytkownika. Gdy zastosuje się Thread Pool, to w przypadku przekroczenia maksymalnej liczby wątków, nowe wątki są kolejkowane i uruchamiane dopiero w momencie, gdy któreś z aktywnych wątków skończą swoją pracę.
Przed rozpoczęciem pracy z pulą wątków należy pamiętać, że będą one zawsze wątkami typu "background".
Dostęp do puli wątków można uzyskać na kilka sposobów:
Pula wątków poprzez Task Parallel Library (TPL)
TPL dostarcza specjalną klasę Task, która dostępna jest pod dwoma postaciami: generyczną i niegeneryczną. Wątek z Thread Pool można wystartować w poniższy sposób:
public static void Run()
{
Task.Factory.StartNew(NonGenericTask);
}
public static void NonGenericTask()
{
Console.WriteLine("This is thread from thread pool");
}
lub w sposób generyczny, gdzie możemy zwracać wartość z wątku.
public static void Run()
{
Task<string> downloader = Task.Factory.StartNew(
() => GenericTask("http://www.wisla.krakow.pl"));
Console.WriteLine("Some operations...");
var result = downloader.Result;
Console.WriteLine(result);
}
public static string GenericTask(string uri)
{
Console.WriteLine("This is thread from thread pool");
using (var wc = new System.Net.WebClient())
return wc.DownloadString(uri);
}
Warto pamiętać, że w momencie, gdy odwołujemy się do property
Result, wątek główny zostanie zawieszony do momentu wywołania wątku z puli.
TPL został wprowadzony w .NET 4.0. W starszych wersjach zachodzi konieczność korzystania z puli wątków w nieco inny sposób.
Pula wątków poprzez klasę ThreadPool
Tutaj mamy do dyspozycji klasę
ThreadPool, gdzie wywołując wątek możemy przez parametr podać stan (jako typ
object).
public static void Run()
{
Console.WriteLine("Without TPL");
ThreadPool.QueueUserWorkItem(QueueUserWorkItemFcn,
DateTime.Today.ToShortDateString());
}
public static void QueueUserWorkItemFcn(object dateTime)
{
Console.WriteLine("This is thread from thread pool, at {0}", dateTime);
}
Jeżeli chcemy zwracać obiekty z wątku nie używając
TPL, należy wykorzystać tzw. asynchronous delegates.
public static void Run()
{
Console.WriteLine("Asynchronous delegate");
Func<string, string> func = new Func<string, string>(AsynchronousDelegate);
IAsyncResult value = func.BeginInvoke("this is some text", null, null);
Console.WriteLine("Some operations...");
var str = func.EndInvoke(value);
Console.WriteLine(str);
}
public static string AsynchronousDelegate(string letters)
{
Console.WriteLine("This is thread from thread pool");
IEnumerable<char> rev = letters.Reverse();
string result = "";
foreach (var @char in rev)
{
result += @char;
}
return result;
}
Operacja
EndInvoke czeka aż asynchroniczny delegat wykona swoje zadanie, zawieszając główny wątek, odbiera rezultat przetwarzania, a także "przerzuca" wyjątek z wątku w tle do wątku głównego.
Wywołując
BeginInvoke można jako drugi parametr przekazać callback, który wykona się po zakończeniu pracy wątku. Callback jako parametr przyjmuje typ
IAsyncResult.
public static void Run()
{
var method = new Func<string, string>(AsynchronousDelegate);
method.BeginInvoke("My text", Callback, method);
}
public static void Callback(IAsyncResult result)
{
var target = (Func<string, string>)result.AsyncState;
string res = target.EndInvoke(result);
Console.WriteLine(res);
}