Ten artykuł pochodzi z serii przygotowań do egzaminu 70-503: Windows Communication Foundation.
Ci z Was, którzy obsługiwali już wątki w .NET wiedzą, że nie jest to specjalnie skomplikowane. Najczęściej problemy występują przy obsłudze kontrolek Windows Forms, ponieważ ich właściwości mogą być zmieniane tylko w wątku, który je stworzył. Innym problemem jest wykorzystywanie lokalnej pamięci wątków do przechowywania informacji o kontekście, gdy proces nieoczekiwanie zmienia wątki, te dane mogą zniknąć. Z tej lekcji dowiemy się jak powyższe problemy są obsługiwane w WCFie.
Kontekst synchronizacji
W .NET 2.0 została wprowadzona rzadko używana funkcjonalność zwana kontekstem synchronizacji (klasa SynchronizationContext). Umożliwia ona sprawdzenie czy aktualnie wykonywany kod znajduje się w odpowiednim wątku. Aktualny kontekst możemy otrzymać odwołując się do statycznego pola SynchronizationContext.Current. Gdy jakaś metoda ma być wywołana w wątku, który nie jest bieżącym wątkiem, wywołujący wątek tworzy delegat typu SendOrPostCallback odwołujący się do żądanej metody a następnie jest przekazywany do metody Post (wywołanie asynchroniczne) lub Send (wywołanie synchroniczne) obiektu SynchronizationContext.
WCF i synchronizacja
A co ma kontekst synchronizacji do WCF’a? Nie wiem czy wiecie, że jeśli nie jest ustawione inaczej, każde wywołanie metod obiektu serwisu jest wykonywane przez wątki wejścia/wyjścia, z których żaden nie należy do naszej aplikacji. Gdybyśmy chcieli teraz zaktualizować coś w interfejsie użytkownika to napotkamy problem.
Z pomocą przychodzi nam właściwość UseSynchronizationContext klasy ServiceBehavior:
[ServiceBehavior(UseSynchronizationContext=true)]
public class UpdateService : IUpdateService
Ustawienie tego atrybutu na true spowoduje, że WCF będzie sprawdzał wątek uruchamiający hosta i jeśli wątek ten posiada kontekst synchronizacji i metoda serwisu jest wywołana z innego wątku, to będzie ona przekazywana do właściwego wątku i przez niego uruchomiona.
W przypadku hostowania serwisu w aplikacji Windows Forms/WPF, jeśli najpierw zostanie utworzony host przed oknem, nie zostanie utworzony żaden kontekst synchronizacji i każda aktualizacja kontrolek spowoduje błąd. Najpierw należy utworzyć okno a potem dopiero hosta serwisu.
Własny kontekst synchronizacji
WCF udostępnia tylko jedną klasę umożliwiającą obsługę kontekstu synchronizacji, dzięki której możemy np. aktualizować kontrolki w oknie aplikacji hostującej serwis. Możemy także utworzyć własne konteksty synchronizacji. Klasa kontekstu synchronizacjo odpowiada za wykonywanie metod serwisu przez konkretne wątki. Możemy wykorzystać ją np. do priorytetowania obsługi. Nasz kontekst synchronizacji może przekazywać wykonanie ważniejszych metod do wątków o wyższym priorytecie a pozostałych do innych wątków. Możemy właściwie zrobić o wiele więcej bazując na tym co udostępniają nam wątki.
Na początek potrzebna jest klasa bazująca na SynchronizationContext.
1: public class ThreadPoolSynchronizer : SynchronizationContext, IDisposable
2: {
3: Queue<WorkItem> workItemQueue;
4: WorkerThread[] workerThreads;
5: Semaphore itemAdded;
6:
7: public ThreadPoolSynchronizer(int poolSize)
8: {
9: if (poolSize <= 0)
10: throw new InvalidOperationException("Pool size cannot be zero");
11:
12: workItemQueue = new Queue<WorkItem>();
13:
14: workerThreads = new WorkerThread[poolSize];
15: for (int index = 0; index < poolSize; index++)
16: workerThreads[index] = new WorkerThread(index + 1, this);
17: }
18:
19: public void Close()
20: {
21: foreach (WorkerThread thread in workerThreads)
22: thread.Abort();
23: }
24:
25: public void Abort()
26: {
27: foreach (WorkerThread thread in workerThreads)
28: thread.Abort();
29: }
30:
31: public void Dispose()
32: {
33: this.Close();
34: }
35:
36: public override void Post(SendOrPostCallback method, Object state)
37: {
38: WorkItem workItem = new WorkItem(method, state);
39: QueueWorkItem(workItem);
40: }
41:
42: public override void Send(SendOrPostCallback method, Object state)
43: {
44: if (SynchronizationContext.Current == this)
45: {
46: method(state);
47: return;
48: }
49: WorkItem workItem = new WorkItem(method, state);
50: QueueWorkItem(workItem);
51: workItem.AsyncWaitHandle.WaitOne();
52: }
53:
54: protected Semaphore ItemAdded
55: {
56: get
57: {
58: if (itemAdded == null)
59: itemAdded = new Semaphore(0, Int32.MaxValue);
60:
61: return itemAdded;
62: }
63: set
64: {
65: itemAdded = value;
66: }
67: }
68:
69: virtual internal void QueueWorkItem(WorkItem workItem)
70: {
71: lock (workItemQueue)
72: {
73: workItemQueue.Enqueue(workItem);
74: ItemAdded.Release();
75: }
76: }
77:
78: protected virtual bool QueueEmpty
79: {
80: get
81: {
82: lock (workItemQueue)
83: {
84: if (workItemQueue.Count > 0)
85: {
86: return false;
87: }
88: return true;
89: }
90: }
91: }
92: internal virtual WorkItem GetNext()
93: {
94: ItemAdded.WaitOne(1000);
95: lock (workItemQueue)
96: {
97: if (workItemQueue.Count == 0)
98: {
99: return null;
100: }
101: return workItemQueue.Dequeue();
102: }
103: }
104:
105: }
Mamy tutaj prostą implementację własnej obsługi wątków. Dodatkowo utworzone są klasy WorkItem i WorkerThread (nazwy mówią same za siebie, poniżej pokażę ich kod).
Za funkcjonalność synchronizacji odpowiada pięć metod podzielonych na dwie grupy. Trzy metody operacyjne (Close, Abort, Dispose – linie 19-34) odpowiadają za zatrzymywanie wątku obsługującego żądanie. Dwie metody funkcyjne (Post i Send – linie 36-52) używane są przez WCF do uruchomienia metody. W metodzie Send (linie 44-48) sprawdzamy czy aktualny kontekst nie jest naszym kontekstem aby nie spowodować zakleszczenia.
Dla pełności przedstawiam jeszcze klasy WorkItem i WorkerThread:
1: [Serializable]
2: internal class WorkItem
3: {
4: object state;
5: SendOrPostCallback method;
6: ManualResetEvent asyncWaitHandle;
7:
8: public WaitHandle AsyncWaitHandle
9: {
10: get
11: {
12: return asyncWaitHandle;
13: }
14: }
15:
16: internal WorkItem(SendOrPostCallback method, object state)
17: {
18: this.method = method;
19: this.state = state;
20: asyncWaitHandle = new ManualResetEvent(false);
21: }
22:
23: internal void CallBack()
24: {
25: method(state);
26: asyncWaitHandle.Set();
27: }
28: }
1: internal class WorkerThread
2: {
3: ThreadPoolSynchronizer context;
4: public Thread threadObj;
5: bool endLoop;
6:
7: public int ManagedThreadId
8: {
9: get
10: {
11: return threadObj.ManagedThreadId;
12: }
13: }
14:
15: internal WorkerThread(int threadNumber, ThreadPoolSynchronizer context)
16: {
17: this.context = context;
18:
19: endLoop = false;
20: threadObj = null;
21:
22: threadObj = new Thread(Run);
23: threadObj.IsBackground = true;
24: threadObj.Name = "Tread-" + threadNumber.ToString();
25: threadObj.Start();
26: }
27:
28: bool EndLoop
29: {
30: set
31: {
32: lock (this)
33: {
34: endLoop = value;
35: }
36: }
37: get
38: {
39: lock (this)
40: {
41: return endLoop;
42: }
43: }
44: }
45:
46: void Start()
47: {
48: Debug.Assert(threadObj != null);
49: Debug.Assert(threadObj.IsAlive == false);
50: threadObj.Start();
51: }
52:
53: void Run()
54: {
55: Debug.Assert(SynchronizationContext.Current == null);
56: SynchronizationContext.SetSynchronizationContext(context);
57:
58: while (EndLoop == false)
59: {
60: WorkItem workItem = context.GetNext();
61: if (workItem != null)
62: {
63: workItem.CallBack();
64: }
65: }
66: }
67:
68: public void Abort()
69: {
70: Debug.Assert(threadObj != null);
71: if (threadObj.IsAlive == false)
72: {
73: return;
74: }
75: EndLoop = true;
76:
77: threadObj.Join();
78: }
79: }
80:
Teraz musimy przypisać nowy kontekst synchronizacji, może to wyglądać tak (przypisanie w linii 2):
1: ThreadPoolSynchronizer syncContext = new ThreadPoolSynchronizer(3);
2: SynchronizationContext.SetSynchronizationContext(syncContext);
3: try
4: {
5: ServiceHost host = new ServiceHost(typeof(UpdateService));
6: host.Open();
7: // Block until ready to quit
8: host.Close();
9: }
10: finally
11: {
12: syncContext.Dispose();
13: }
Innym lepszym sposobem jest udekorowanie klasy serwisu własnym atrybutem (tworzenie atrybutów wykracza poza ramy tego kursu). Może to wyglądać np. tak (atrybut z dwoma parametrami rozmiar puli oraz nazwa klasy serwisu):
[ThreadPoolSynchronization(3, typeof(UpdateService))]
[ServiceBehavior(typeof(IUpdateService))]
public class UpdateService : IUpdateService
Atrybut ten musi implementować interfejs IContractBehavior a w nim następujące metody:
- AddBindingParameters – modyfikuje bindingi,
- ApplyClientBehavior – modyfikuje lub rozszerza zachowanie serwisu dla wybranych lub wszystkich wiadomości,
- ApplyDispatchBehavior - “wersja od strony serwisu” metody ApplyClientBehavior – rozszerza zachowanie dla przychodzących wiadomości,
- Validate – potwierdza że kontrakt i punkt końcowy mogą obsłużyć zachowanie zaimplementowane w obiekcie.
Ponieważ chcemy zmienić zachowanie po stronie serwisu, interesować nas będzie metoda AppliDispatchBehavior, która może być zaimplementowana np tak:
void ApplyDispatchBehavior(ContractDescription
description, ServiceEndpoint endpoint, DispatchRuntime dispatchRuntime)
{
// ...
if (dispatchRuntime.SynchronizationContext == null)
dispatchRuntime.SynchronizationContext = new ThreadPoolSynchronizer(3);
// ...
}
Synchronizacja i wywołania zwrotne
Obsługując wywołania zwrotne (ang. callback) także musimy brać pod uwagę problemy z współbieżnością. Podobnie jak w przypadku serwisu, u klienta możemy ustawić odpowiednie tryby synchronizacji, zarówno imperatywnie jak i deklaratywnie:
[CallbackBehavior(ConcurrencyMode = ConcurrencyMode.Single)]
class CallbackClient : ICallback
{
// Implementation code
}
Tak jak w ConcurrencyMode w klasie ServiceBehavior tak i tutaj mamy trzy możliwe wartości:
- Single – tylko jedno wywołanie zwrotne jest możliwe w danym czasie, to gwarantuje nam że WCF nie wywoła metody więcej niż raz w tym samym czasie, nie gwarantuje natomiast że inne wątki klienta będą się odwoływać do zasobów używanych w tej metodzie, o to musimy sie już sami martwić;
- Multiple – dozwolone jest wielokrotne wywołanie metody, musimy sami postarać się o obsługę dostępu wielowątkowego;
- Reentrant – metoda może być wywoływana ponownie przez serwis w tym samym wątku.
Wywołania zwrotne i konteksty synchronizacji
Możliwe jest także korzystanie z kontekstów synchronizacji w wywołaniach zwrotnych, wystarczy odpowiednio oznaczyć metodę:
[CallbackBehavior(UseSynchronizationContext=true)]
public class CallbackClient : ICallback
{
// Implementation code
}
Na koniec jeszcze ważna informacja na temat wątków, wywołań zwrotnych i zakleszczeń. Załóżmy, że mamy przycisk uruchamiający metodę serwisu. Serwis dokonuje wywołania zwrotnego do aplikacji (dokładniej do wątku interfejsu użytkownika bo stamtąd pochodziło wywołanie serwisu). Wątek interfejsu użytkownika jest teraz zajęty bo czeka na odpowiedź od serwisu… No i mamy zakleszczenie. Jedynym rozwiązaniem tego problemu jest zrezygnowanie z kontekstu synchronizacji i ustawienie UseSynchronizationContext na false.
Na tym kończymy kurs.
Pozostańcie jeszcze z nami gdyż przygotowaliśmy małą niespodziankę.
Do zobaczenia na egzaminach :)