Trzecią i ostatnią część artykułu poświęcam rzadkiemu ale bardzo trudnemu w analizie problemowi, który może pojawić się w czasie adaptacji API w wykorzystaniem TaskCompletionSource.
Zacznę od takiego niewinnego kawałku kodu:
class Adapter { public Task<int> GetNextInt() { var api = new CallbacksApi(); var taskCompletionSource = new TaskCompletionSource<int>(); api.GetNextInt((result, exception) => { if (exception != null) { taskCompletionSource.SetException(exception); } else { taskCompletionSource.SetResult(result); } }); return taskCompletionSource.Task; } }
Kod wydaje się poprawny. Co lepsze będzie działać w czasie testów dymnych, prawdopodobnie przejdzie testy funkcjonalne. Kod ten może jednak powodować pojawianie się błędów typu: „Ta operacja czasami nie kończy się przez pół godziny”, „aplikacja chodzi wolno” albo „program się zawiesza”. Co gorsza nikt nie będzie w stanie dostarczyć powtarzalnego scenariusza.
Winnym okazuje się być Garbage Collector, który może w tym przypadku błędnie stwierdzić, że nikt nie potrzebuje obiektu CallbacksApi, bo nikt nie posiada do niego referencji.
Pierwszym sposobem „obejścia” tego problemu może być „przytrzymanie” referencji do API w jakimś obiekcie, który żyje dłużej. W kodzie powyżej może być to klasa obiekt typu Adapter.
class Adapter { private CallbacksApi _api = new CallbacksApi(); public Task<int> GetNextInt() { var taskCompletionSource = new TaskCompletionSource<int>(); _api.GetNextInt((result, exception) => { if (exception != null) { taskCompletionSource.SetException(exception); } else { taskCompletionSource.SetResult(result); } }); return taskCompletionSource.Task; } }
Sprawa się komplikuje gdy nowe CallbacksApi musi być tworzone z każdym wywołaniem GetNextInt. Brzydkim ale szybkim obejściem tego problemu jest użycie kolekcji:
class Adapter { private List<CallbacksApi> _apiReferences = new List<CallbacksApi>(); private object _apiReferencesLockRoot = new object(); public Task<int> GetNextInt() { var api = new CallbacksApi(); var taskCompletionSource = new TaskCompletionSource<int>(); lock(_apiReferencesLockRooti) { _apiReferences.Add(api); } api.GetNextInt((result, exception) => { lock(_apiReferencesLockRooti) { _apiReferences.Add(api); } if (exception != null) { taskCompletionSource.SetException(exception); } else { taskCompletionSource.SetResult(result); } }); return taskCompletionSource.Task; } }
Ten kod działa ale wszyscy się chyba zgodzą że najładniejszy to on nie jest. W dodatku jeżeli referencja do obiektu typu Adapter nie jest nigdzie przetrzymywana przez dłuższy czas to powyższe rozwiązania i tak nie zadziałają. Na szczęście Microsoft pomyślał o tym problemie i TaskCompletionSource posiada dodatkowy parametr konstruktora „state”, którego można przekazać cokolwiek. Obiekt ten jest przypisany do właściwości AsyncState wynikowego Taska, przez co GC zostawi go w spokoju.
class Adapter { public Task<int> GetNextInt() { var api = new CallbacksApi(); var taskCompletionSource = new TaskCompletionSource<int>(api); api.GetNextInt((result, exception) => { if (exception != null) { taskCompletionSource.SetException(exception); } else { taskCompletionSource.SetResult(result); } }); return taskCompletionSource.Task; } }
Kod klas, które stworzyłem na potrzeby tego wpisu znajduje się w tym punkie Githuba, natomiast testy do nich tutaj. Testy zwracają oczekiwany wynik tylko po zbudowaniu projektu w trybie Release, a właściwie to nie do końca zwracają. Test dla klasy FixedAdapter przechodzi i tak ma być. Test dla klasy BrokenAdapter nie przechodzi i to także jest zgodne z moimi oczekiwaniami.
Mam natomiast problem z testem dla klasy WorkedAroundAdapter, teoretycznie linijka Assert.NotNull(adapter) powinna sprawić że test przejdzie, ten jednak przejść nie chce i nie mam pojęcia dlaczego. Postanowiłem więc postawić znaczną ilość piwa, zgrzewkę 24 Tyskiego albo jego równowartość w innym gatunku, pierwszej osobie która mi to wytłumaczy.
Bardzo pomocne dzieki 😀