WCF AwaitPisząc ostatnio wpis o WCF zauważałem ,że Visual Studio 2012 domyślnie generuje klasy proxy dla usługi w stylu “Task”. Ten styl wymaga użycia słów kluczowych async i await.
Pomyślałem, dlaczego nie zrobić o tym wpisu i przy okazji same siebie przyzwyczaić do nowej składni języka C# 5.0.
Na początek utworzymy usługę sieciową w WCF.
Do zabawy w async i await będą nam potrzebne następujące metody.
[ServiceContract]
public interface IService1
{
[OperationContract]
string GetString();
[OperationContract]
string GetAnotherString();
[OperationContract]
string GetMyThought();
[OperationContract]
string GiveStringIReverse(string s);
[OperationContract]
string GetException();
}
Prawie każda z tych metod zwraca jakiś napis. Jedna z nich odwróci napis otrzymywany w parametrze. Ostatnia metoda zwróci wyjątek “NotImplementedException”.
public class Service1 : IService1
{
public string GetString()
{
return "Heellloooo Nursseeee";
}
public string GetAnotherString()
{
return "Zdzisła Bohater Galaktyki";
}
public string GetMyThought()
{
return "Gram właśnie w Assasina i nie mogę przestać";
}
public string GiveStringIReverse(string s)
{
return new string(s.Reverse().ToArray());
}
public string GetException()
{
throw new NotImplementedException();
}
}
Mając już usługę WCF na serwerze stworzymy teraz klienta. Dla tradycji niech to będzie aplikacja konsolowa.
Do aplikacji konsolowej oczywiście dodajemy referencje do usługi WCF. Aby to zrobić wystarczy kliknąć prawym przyciskiem myszki na projekt i wybrać z menu kontekstowego “Add Service Reference”.
Teraz przejdźmy do rzeczy specyficznych w Visual Studio 2012 i C# 5.0. Tak jak napisałem wcześniej Visual Studio domyślnie generuje kod oparty na Task-ach.
W opcjach konfiguracji referencji usługi WCF możemy to oczywiście zmienić. Jednak najnowszy wynalazek C# zwany async i await skraca wywoływanie metod proxy oraz obsługę rezultatu, gdyż nie mamy teraz rozbicia na zdarzenia “X-Completed”.
Jak widać kod klienta nie ma teraz zdarzeń X-Completed istnieje tylko metoda synchroniczna “X” i metoda asynchroniczna zwracająca Task “X-Async”.
Jak więc wywołać.metodę “X-Async”?
static async void Main(string[] args)
{
var client = new RefAwait.Service1Client();
string x = await client.GetStringAsync();
Console.WriteLine(x);
Console.ReadLine();
}
Pisząc aplikację konsolową zdałem sobie sprawę ,że nie mogę użyć słowa kluczowego “await” wewnątrz głównej metody “Main”. Użycie słów kluczowych async i await zdecydowanie jest wygodniejsze w aplikacjach zdarzeniowych jak n.p WPF.
Przez chwilę myślałem ,że udało mi się oszukać kompilator ,ale rzeczywiście nie można użyć modyfikatora “async” w metodzie Main. Dla pewności sprawdziłem inne źródła i nie można tego obejść. Chociaż poniższy kod był możliwy w async CTP asynchroniczność głównej metody Main być może nie jest dobry pomysłem.
static async Task Main(string[] args)
{
var client = new RefAwait.Service1Client();
string x = await client.GetStringAsync();
Console.WriteLine(x);
Console.ReadLine();
}
Okej skoncentrujmy się na wywołaniach metod proxy w WCF w kliencie konsolowym. Skoro nie mogę użycia modyfikatora “async” w metodzie Main to znaczy ,że muszę wywołać metodę w innym miejscu.
Jak widzisz poniżej metoda GetString() jest oznaczona słowem kluczowym “async”. Rola tego słowa kluczowego sprowadza się do oznaczenia. Oznaczona tak metoda określa nam to ,że w którymś miejscu w tej metodzie skorzystamy ze słowa “await”.
static void Main(string[] args)
{
Console.WriteLine(GetString());
Console.ReadLine();
}
public static async Task<string> GetString()
{
var client = new RefAwait.Service1Client();
string x = await client.GetStringAsync();
return x;
}
Co robi słowo “await” i dlaczego return zwracamy “string”, gdy metoda wyraźnie zwraca obiekt Task<string>. Kod się kompiluje więc wszystko jest w porządku.
Słowo kluczowe “await” oznacza kompilatorowi dane zadanie w tym wypadku “GetString()” jako asynchroniczny kod. Kod, który musi wykonać się w inny wątku bez blokowania wątku głównego. Gdy zadanie asynchroniczne zostanie wykonane do właściwości “Result” obiektu Task<string> zostanie przypisany rezultat.
Obiekt Task więc jest czymś w rodzaju obietnicy, która być może kiedyś się spełni. Metoda zwraca obiekt Task ,ale sam wynik typu string zostanie asynchronicznie zwrócony później.
Obecny kod zwraca tylko obiekt Task ,a nie wynik.
Patrząc na właściwości tego obiektu możemy ustalić:
- Dane zdanie nie zostało jeszcze uruchomione. Właściwość “Status” jest ustawiona na “WaitingForActivation”.
- Nie ma rezultatu. Właściwość “Result” jest null ,a IDE Visual Studio mówi ,że dany rezultat nie jest jeszcze obliczony.
Jak więc uruchomić zadanie?
Czy istnieje jakaś metoda statyczna niestatyczna “Start”? W sumie to istnieje taka metoda ,ale ona nie robi tego co teraz chcemy. Na jednym ze spotkań oficjalnej warszawskiej grupy .NET “Lucian Wischik”omówił działanie słów kluczowych async i await. Jako główny kurator Visual Basica mniej więcej starał nam się wytłumaczyć, dlaczego nie mamy metody, która by wywoływała dane zadanie.
Podobno najwięksi guru stwierdzili ,że wywoływanie tej jakieś magicznej metody “start” jest tylko stratą linijki kodu. Z drugiej strony tworzy to też problem co się stanie jeśli zapomnimy o wywołaniu metody “start”.
Dlatego zadanie zacznie się wykonywać, wtedy gdy dana obietnica musi być znana i użyta w tym wypadku zmienna “x” jest użyta w Console.WriteLine(). A sam rezultat musi być znany przy przypisaniu do zmiennej “string”.
Program w takim wypadku będzie czekać na tą obietnicę.
static void Main(string[] args)
{
string x = GetString().Result;
Console.WriteLine(x);
Console.ReadLine();
}
public static async Task<string> GetString()
{
var client = new RefAwait.Service1Client();
string x = await client.GetStringAsync();
return x;
}
Czyli w trakcie tego przypisania metoda asynchroniczna zostaje uruchomiona.
string x = GetString().Result;
Łatwo to sprawdzić używając właściwości “Status”.
static void Main(string[] args) { Task<string> x = GetString(); Console.WriteLine(x.Status); Console.WriteLine(x.Result); Console.WriteLine(x.Status); Console.ReadLine(); }
Jak widzisz sam obiekt Task jeszcze nie robi nic ,ale gdy chcemy wyświetlić już zawartość wtedy zadanie zostaje uruchomione.
Oto wszystkie statusy obiekty Task wraz z komentarzami wyjęte z bibliotek .NET przy pomocy Resharpera.
public enum TaskStatus
{
/// <summary>
/// The task has been initialized but has not yet been scheduled.
/// </summary>
Created,
/// <summary>
/// The task is waiting to be activated and scheduled internally by the .NET Framework infrastructure.
/// </summary>
WaitingForActivation,
/// <summary>
/// The task has been scheduled for execution but has not yet begun executing.
/// </summary>
WaitingToRun,
/// <summary>
/// The task is running but has not yet completed.
/// </summary>
Running,
// /// <summary>
// /// The task is currently blocked in a wait state.
// /// </summary>
// Blocked,
/// <summary>
/// The task has finished executing and is implicitly waiting for
/// attached child tasks to complete.
/// </summary>
WaitingForChildrenToComplete,
/// <summary>
/// The task completed execution successfully.
/// </summary>
RanToCompletion,
/// <summary>
/// The task acknowledged cancellation by throwing an OperationCanceledException with its own CancellationToken
/// while the token was in signaled state, or the task's CancellationToken was already signaled before the
/// task started executing.
/// </summary>
Canceled,
/// <summary>
/// The task completed due to an unhandled exception.
/// </summary>
Faulted
}
Zaraz coś tu robimy nie tak
Oczywiście zapomniałem tutaj o czymś powiedzieć. Wywołane zadanie asynchroniczne w ten sposób blokuje jednak konsole.
W końcu mówimy programowi ,że potrzebujemy wyniku w wątku główny tu i teraz…więc czekamy.
Możemy uruchomić zadanie asynchronicznie bez blokowania wątku głównego. w ten sposób. Inny Task asynchroniczny czeka na wykonanie asynchronicznego zadania “GetString()”. Brzmi to, jak Incepcja xD.
Zadanie wykonuje się asynchronicznie ,ale nie zmienia to faktu ,że na zadanie poziom niżej on musi czekać.
static void Main(string[] args)
{
Task.Run(() =>
{
Console.WriteLine(GetString().Result);
});
Console.WriteLine("Nie blokuje");
Console.ReadLine();
}
Rozwiązać problem można jeszcze prościej.Ta metoda jest zdecydowanie najlepsza.Oto metoda async, która nic nie zwraca. Metody async niezwracające nic nie są oznaczane przez void tylko przez klasę niegeneryczną Task.
Wątek główny nie będzie blokowany.
static void Main(string[] args)
{
GetString2();
Console.WriteLine("Nie blokuje");
Console.WriteLine("Nie blokuje");
Console.ReadLine();
}
public static async Task GetString2()
{
var client = new RefAwait.Service1Client();
string x = await client.GetStringAsync();
Console.WriteLine(x);
}
Pamiętasz może rozwiązanie proxy WCF bazujące na zdarzeniach X-Completed.
Szczerze mówiąc jakby pozbyć się tej całej magii kompilatora to byś zrozumiał ,że mniej więcej kod zachowuje się tak samo. Metoda przy napotkaniu await zwróci obiekt Task. Gdy serwer zwróci rezultat analogicznie do zdarzenia X-Completed wrócimy znowu do metody GetString2() i dalsza część kodu zacznie się wykonywać.
By to lepiej zrozumieć przyjrzy się jeszcze raz wywoływaniu zdarzeniowym.
static void Main(string[] args)
{
GetString2();
Console.WriteLine("Nie blokuje");
Console.WriteLine("Nie blokuje");
Console.ReadLine();
}
public static async Task GetString2()
{
var client = new RefAwait.Service1Client();
client.GetStringCompleted += client_GetStringCompleted;
client.GetStringAsync();
}
static void client_GetStringCompleted(object sender, RefAwait.GetStringCompletedEventArgs e)
{
Console.WriteLine(e.Result);
}
Oto ilustracja pomocnicza. Jak widzisz async i await nie zmienia zachowania usługi sieciowej i wszystko działa tak samo, jak wcześniej.
Inne ciekawostki Task
Obiekt Task posiada także metodę “IsCompleted”. Użycie tej właściwości także powoduje odpalenie zadania asynchronicznego. Oto prosty przykład użycia właściwości “IsCompleted” wraz z pętlą “while”. Jak widać właściwość IsCompleted odpaliło zadanie ,ale nie zablokowało wątku głównego konsoli, ponieważ pętla while się wykonuje.
Trochę to logiczne, ponieważ konsola nie musi znać teraz wyniku.
static void Main(string[] args)
{
Task<string> task = GetString();
while (!task.IsCompleted)
{
Console.WriteLine("...Loading...");
Thread.Sleep(100);
}
Console.WriteLine(task.Result);
Console.ReadLine();
}
Oto co się dzieje w aplikacji konsolowej.
Gdy chcemy wykonywać coś w wątku główny ,a zarazem zrobić coś, gdy zadanie się zakończy wtedy możemy skorzystać z metody “ContinueWith”. Oto 3 różne składnie LINQ dodane do metody ContinueWith wszystkie trzy są poprawne.
static void Main(string[] args)
{
Task<string> task = GetString();
task.ContinueWith((Task<string> x) => { Console.WriteLine("Done: " + x.Result); });
task.ContinueWith((x) => { Console.WriteLine("Done: "+ x.Result); });
task.ContinueWith((x) => Console.WriteLine("Done: " + x.Result));
DoSomethingElseAndWait();
}
public static void DoSomethingElseAndWait()
{
Console.ReadLine();
}
Konsola może być nawet zamknięta przed wyświetleniem wyników.
Zobaczmy jakie użyteczne statyczne metody ma klasa Task.
Na przykład powiedzmy ,że chcemy wywołać trzy metody i otrzymywać wynik tylko z tej, która wykona się najszybciej.
Do tego zadania mamy metodę “WhenAny”, która przyjmuje listę obiektów typu Task oraz zwraca typ generyczny Task w Tasku.
TASKOCEPCJA
static void Main(string[] args)
{
List<Task<string>> list = new List<Task<string>>();
list.Add(GetString());
list.Add(GetAnotherString());
list.Add(GetMyThoought());
Task<Task<string>> s = Task.WhenAny(list);
Console.WriteLine(s.Result.Result);
Console.ReadLine();
}
public static async Task<string> GetString()
{
var client = new RefAwait.Service1Client();
string x = await client.GetStringAsync();
return x;
}
public static async Task<string> GetAnotherString()
{
var client = new RefAwait.Service1Client();
string x = await client.GetAnotherStringAsync();
return x;
}
public static async Task<string> GetMyThoought()
{
var client = new RefAwait.Service1Client();
string x = await client.GetMyThoughtAsync();
return x;
}
Tak ja wcześniej mogę uzyskać wynik poprzez odwołanie się do rezultatu rezultatu. REZULTOCEPCJA
Oczywiście w ten sposób blokuje konsole ,ale chyba już wiesz jak poradzić sobie z tym problemem.
Istnieje także metoda statyczna “WhenAll”. Przyjmuje ona listę obiektów Task. Wykonuje ona więc Taska którym celem jest wykonanie wszystkich innych podrzędnych Tasków.
static void Main(string[] args)
{
List<Task<string>> list = new List<Task<string>>();
list.Add(GetString());
list.Add(GetAnotherString());
list.Add(GetMyThoought());
Task<string[]> s = Task.WhenAll<string>(list);
foreach (string vs in s.Result)
{
Console.WriteLine(vs);
}
Console.ReadLine();
}
Znowu blokuje wątek główny ,ale chce wam pokazać jak ta metoda działa.
Wyjątki
Wyjątki to trochę kłopotliwa sprawa zwłasza, wtedy gdy uruchamiamy kilka zadań asynchronicznych równocześnie jako grupa przypisana do jednego obiektu Task.
Okej, gdy jedno z zadań się nie wykona zostanie wyrzucony wyjątek “AggreagateException”. Ma on bardzo pomocną wiadomość “Jedno lub więcej zadań zwróciło wyjątek”.
Mając listę zadań asynchronicznych zapewne zadajesz sobie sprawę jak możemy sprawdzić pojedyncze wyrzucone wyjątki.
Oto kod, który uruchomi kilka zadań. Zostaną wyrzucone trzy wyjątki i wyświetlimy je w konsoli.
static void Main(string[] args)
{
List<Task<string>> list = new List<Task<string>>();
list.Add(GetMyThoought());
list.Add(GetException());
list.Add(Task<string>.Run(() =>
{
throw new DivideByZeroException();
return "s";
}));
list.Add(Task<string>.Run(() =>
{
throw new AccessViolationException();
return "s";
}));
Task<string[]> s = Task.WhenAll<string>(list);
try
{
Console.WriteLine(s.Result.First());
}
catch (AggregateException ex)
{
Console.WriteLine(ex.Message + "\n");
foreach (var e in ex.InnerExceptions)
{
Console.WriteLine(e.Message + "\n");
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
Console.ReadLine();
}
Oto co powinieneś wiedzieć o WCF i o async i await.