W jednym z poprzednich artykułów opisałem ideę działania operacji asynchronicznych. Poza wieloma zaletami operacje asynchroniczne mają jedną poważną wadę – są skomplikowane. Stosunkowo trudno jest zaimplementować dużą, multiplatformowa aplikację. Oczywiście możliwe jest obudowanie funkcji do obsługi operacji asynchronicznych klasami, które znacznie uproszczą nam korzystanie asynchronicznych operacji. Dzięki podejściu obiektowemu i zastosowaniu rozwiązań wyższego poziomu można utworzyć prosty i łatwy w użyciu interfejs.

Zastanówmy się najpierw, jak powinien działać takich szkielet aplikacji. Na pewno potrzebujemy mieć klasę, która obuduje korzystanie z funkcji do obsługi operacji asynchronicznych (select() lub WaitForMultipleObjects() itd. zależnie od platformy). Klasa ta powinna przechowywać kolekcję zarejestrowanych w niej źródeł danych. Po każdym wyjściu z funkcji do obsługi operacji asynchronicznych powinna ona automatycznie obsłużyć wywołanie pochodzące z systemu operacyjnego, a następnie wrócić do czekania na informacje pochodzące z innych źródeł. Nazwijmy tę klasę Reactor, ponieważ reaguje ona na sygnały pochodzące z systemu operacyjnego.

Oczywiście klasa Reactor nie wie, w jaki sposób należy obsłużyć dane. Załóżmy, że przyszły właśnie jakieś dane do gniazda i oczekują na przetworzenie. Decyzja, co zrobić z tymi danymi nie powinna zależeć od klasy Reactor. Np. w przypadku stosowania kolejek komunikatów, które na systemach operacyjnych opartych o Unix implementowane są najczęściej przy pomocy pipe, reakcją na pojawienie się danych nie powinno być odczytanie tych danych, tylko zawołanie funkcji z kolejki komunikatów, która wyciągnie nam te dane.

Lepszym i bardziej ogólnym rozwiązaniem jest stworzenie interfejsu (lub klasy abstrakcyjnej), który będzie umożliwiał odebranie od klasy Reactor informacji o tym, że przyszły jakieś dane lub że możliwy jest zapis do bufora wyjściowego. Klasy dziedziczące z tego interfejsu będą mogły zdefiniować własne reakcje na przychodzące informacje. Nazwijmy tę klasę EventHandler. Ponieważ musi ona odbierać przynajmniej dwa rodzaje komunikatów (dane do odczytania oraz możliwość zapisu do bufora), więc klasa EventHandler musi mieć przynajmniej dwie wirtualne lub abstrakcyjne metody. Przykład takiej klasy przedstawiony jest poniżej:

class EventHandler
{
friend class Reactor;
private:
virtual void handleInput()=0;
virtual void handleOutput()=0;
};

Klasa Reactor współpracująca z klasą EventHandler musi mieć w sobie kolekcję zarejestrowanych w niej par: źródło informacji oraz odpowiadającą mu instancję klasy EventHandler. Musi także udostępniać metody umożliwiające dodanie oraz usunięcie pary, a także metody uruchamiające i zatrzymujące pętlę reagowania na wywołania systemu operacyjnego.

Przykładowy interfejs klasy Reactor został przedstawiony poniżej. Handle reprezentuje uchwyt specyficzny dla danego systemu operacyjnego (który zazwyczaj przykrywa się własnym typem definiowanym poprzez typedef, co umożliwia usunięcie z interfejsu elementów specyficznych dla systemu operacyjnego).

//fragment interfejsu klasy Reactor
class Reactor
{
void registerHandler (Handle handle, EventHandler * eventHandler); //dodaje nową parę.
void removeHandler(Handle handle); // usuwa parę. Aby zidentyfikować parę, zamiast Handle mógłby być także podawany EventHandler *.
void startEventLoop(); //blokuje wątek w oczekiwaniu na przyjście asynchronicznych wywołań od systemu operacyjnego.
void stopEventLoop(); //odblokowuje wątek
};

Teraz możemy napisać klasę, która będzie obsługiwać gniazdo. Odziedziczy po EventHandler i nadpisze wszystkie metody potrzebne do obsługi wydarzeń. Pierwszą operacją na takiej klasie będzie otwarcie gniazda. Następnie klasa będzie rejestrowana w klasie Reactor i będzie otrzymywać od niej sygnały informujące o przyjściu danych lub dostępności bufora wyjściowego. W odpowiedzi na te wydarzenia klasa ta może pobierać dane z połączenia oraz wysyłać jakieś własne dane.

Możemy także utworzyć kilka instancji klasy reprezentującej gniazdo. Następnie wszystkie takie klasy możemy zarejestrować w klasie Reactor i dopiero wtedy uruchomić pętlę obsługi asynchronicznych wydarzeń. Daje to możliwość obsługiwania wielu gniazd w jednym wątku. Innym typowym rozwiązaniem jest stworzenie serwera, tzn. otwarcie gniazda w oczekiwaniu na nowe przychodzące połączenia i tworzenie nowej klasy reprezentującej połączenie przez gniazdo dopiero w momencie pojawienia się nowego połączenia.

Ponieważ każdy wątek, jaki będziemy wykorzystywać, będzie korzystać z dokładnie jednej instancji klasy Reactor, więc dobrym pomysłem jest skorzystanie z pamięci specyficznej dla wątku, aby przechowywać tam jedną instancję klasy Reactor. W ten sposób każda klasa uruchamiana w określonym wątku (np. uruchamiany serwer oczekujący na przychodzące połączenia) może pobrać sobie wskaźnik do instancji specyficznej dla wątku i się w niej zarejestrować.

Tak więc mamy już wszystko, co jest nam potrzebne do obsługi asynchronicznych operacji… ale czy na pewno? Okazuje się, że jednak nie. Poza operacjami wejścia-wyjścia istnieje także kilka innych typów asynchronicznych informacji, które także mogłyby być obsługiwane przez nasz schemat Reactor – EventHandler.

Asynchroniczne źródła informacji

Ta część ma za zadanie przedstawienie wszystkich typowych asynchronicznych źródeł informacji, jakie można obsługiwać w schemacie Reactor – EventHandler.

Operacje wejścia-wyjścia

Pierwszym i najbardziej oczywistym elementem, jaki chcielibyśmy obsługiwać, są asynchroniczne operacje wejścia-wyjścia. Temat ten opisałem już w innym artykule, wiec pomijam tutaj szczegóły.

System międzywątkowych powiadomień

Wyobraźmy sobie taką sytuację, że mamy w naszej aplikacji kilka wątków. Każdy z tych wątków obsługuje w sposób asynchroniczny jakieś gniazda, pliki czy też inne asynchroniczne źródła danych. Powiedzmy, że jeden z wątków chce przesłać do innego jakąś wiadomość. Jak może to zrobić? Oczywiście, rozwiązaniem są synchronizowane kolejki komunikatów. To bardzo eleganckie rozwiązanie, stosowane w większości poważnych systemów. Jest tylko pewien problem – wątek, który oczekuje na przyjście informacji z asynchronicznych źródeł, blokuje się. To znaczy, że dopóki nie dostanie jakiegoś wzbudzenia (nie pojawią się jakieś informacje lub możliwość zapisu do bufora wyjściowego), nie podejmie żadnej akcji. Nasza wiadomość wprowadzona do kolejki komunikatów pozostanie w tej kolejce nieobsłużona. Aby tego uniknąć, musimy mieć możliwość przekazania informacji o nowej wiadomości i wzbudzenia tego wątku, aby przeprowadzić obsługę wiadomości.

Limity czasowe

Powiedzmy, że czekamy na przyjście jakiejś wiadomości przez gniazdo. Np. wysłaliśmy jakąś wiadomość po łączu telekomunikacyjnym i oczekujemy, że druga strona potwierdzi jej otrzymanie. Oczywiście spodziewamy się tego potwierdzenia w jakimś skończonym czasie. Jeśli ono nie dotrze, to możemy się spodziewać, że nasze dane nie dotarły i musimy przeprowadzić retransmisję. Problem polega na tym, że wątek zablokowany w oczekiwaniu na operację wejścia-wyjścia nie wie, kiedy mija określony czas. Po prostu jest on zablokowany i – o ile nie dostanie jakiejś informacji – będzie tak trwać przez wieki.

Tak więc potrzebujemy mieć taką funkcjonalność, aby blokada w wątku mogła zostać przerwana w z góry określonym momencie. Np. zanim zaczniemy oczekiwać na przyjście wiadomości po łączu telekomunikacyjnym, informujemy nasz system obsługi asynchronicznych wydarzeń: Hej, ja teraz będę czekać na przyjście danych na tym łączu, ale jeśli minie więcej niż X sekund, to przerwij to i daj mi znać, że ten czas minął.

Obsługa sygnałów pochodzących z systemu operacyjnego

Ostatnim elementem, który chcielibyśmy asynchronicznie obsługiwać, są sygnały otrzymywane od systemu operacyjnego. Chodzi o sygnały typu SIGINT na systemach opartych o POSIX oraz wciśnięcie Ctrl-C na Windows. Są to sygnały każące się aplikacji zamknąć. Aczkolwiek istnieje możliwość ich przechwycenia i obsłużenia. Zazwyczaj są one wykorzystywane do rozpoczęcia procedury wyłączania aplikacji, czyli zwolnienia zasobów, posprzątania po sobie, zapiania konfiguracji i wyłączenia się. Normalnie sygnały te są obsługiwane inaczej niż normalny proces, np. Windows tworzy nowy wątek uruchamiając funkcję, którą podaliśmy mu rejestrując się na określony sygnał. Jednak najlepszym rozwiązaniem byłoby, aby sygnały te były obsługiwane przez jeden z normalnych wątków. A więc chcielibyśmy się zapisać w danym wątku na otrzymanie informacji o przyjściu sygnału. Wiedza o tym, że wewnętrznie będzie to polegać na tworzeniu nowych wątków, jest nam zupełnie zbędna. Obsługa przyjścia sygnału może w naszej aplikacji rozpocząć wyłączanie aplikacji.

Problem złożoności

Po pełnej analizie dochodzimy do wniosku, że przedstawiony system jest stosunkowo skomplikowany. Poziom komplikacji rośnie jeszcze, jeśli dodamy, że chcielibyśmy, aby nasze aplikacje wykorzystujące schmat Reactor – EventHandler były przenośne na różne platformy. Multiplatformowe programowanie oznacza, że musielibyśmy stworzyć typy opakowujące specyficzny dla systemu operacyjnego uchwyt, obudować funkcje select()/WaitForMultipleObjects() w taki sposób, aby zawsze wołana była prawidłowa funkcja. Musielibyśmy także w różny sposób zaimplementować limity czasowe (sposób ich obsługi jest zależny od platformy), różne sposoby powiadomień między wątkami (sposób ten jest mocno zależny od platformy) oraz różne sposoby otrzymywania sygnałów od systemu operacyjnego (np. na platformie Windows SIGINT nie istnieje, ale jego odpowiednikiem jest wciśnięcie Ctrl-C, które jednak jest inaczej obsługiwane). Jeśli więc zdecydowalibyśmy się na implementację takiego systemu, to musielibyśmy się liczyć z wieloma miesiącami implementacji i testów, zanim osiągnęlibyśmy jakieś sensowne rezultaty. Dobrą wiadomością jest to, że wcale nie musimy tego robić. Ktoś już odwalił za nas całą robotę tworząc darmowe biblioteki o otwartym kodzie. Nam pozostaje opanować sposób korzystania z nich i oddać cześć tym, którym tak skomplikowany projekt się udał.

Biblioteki do obsługi asynchronicznych operacji wejścia-wyjścia

Asio z boost

Właściwie istnieją dwie biblioteki służące do obsługi asynchronicznych operacji wejścia-wyjścia. Pierwszą z nich jest biblioteka asio pochodząca z boost (http://bost.org). Boost jest największym standardem w programowaniu systemowym i dlatego najlepszym rozwiązaniem byłoby korzystać właśnie z tej biblioteki. Niestety, biblioteka ta nie obsługuje ani przechwytywania sygnałów od systemu operacyjnego, ani powiadomień między wątkami. Nie daje także możliwości bezpośredniego rejestrowania uchwytów z danego systemu operacyjnego. Jeśli więc pracujemy akurat na systemie Windows i mamy dostępny uchwyt specyficzny dla tego systemu (HANDLE), to nie ma możliwości skorzystania z niego. Jest to więc spory problem. Na chwilę obecną biblioteka asio nadaje się jedynie do obsługi aplikacji przetwarzających operacje wejścia-wyjścia i to tylko w jednym wątku, ponieważ pozostałe wątki nie będą już w stanie przekazywać informacji do wątku obsługującego operacje wejścia-wyjścia. Nie jest to satysfakcjonujące rozwiązanie. Na szczęście biblioteka boost nie jest jedyną, która obudowuje operacje asynchroniczne.

Adaptive Communication Environment

Rozwiązaniem idealnym jest biblioteka Adaptive Communication Environment (http://www.cs.wustl.edu/~schmidt/ACE.html). W przeciwieństwie do boost, obsługuje ona wszystkie istniejące asynchroniczne wywołania.

Właściwie biblioteka ta udostępnia dwa szkielety: Proactor oraz Reactor. Pierwszy z nich służy do znacznego uproszczenia operacji wejścia-wyjścia. W szkielecie tym podajemy jedynie pewną tablicę danych do wysłania i do odebrania. Jej przetworzenie może zająć wiele wywołań asynchronicznych, jednak klasy należące do szkieletu ukrywają te wywołania, informując użytkownika dopiero w momencie wysyłania lub odebrania całości. Szkielet ten nie udostępnia innych operacji poza operacjami wejścia-wyjścia, dlatego zostanie pominięty.

Drugi szkielet oparty jest na współpracy dwóch klas: ACE_Reactor oraz ACE_Event_Handler. Jest to implementacja dwóch klas, jakie wcześniej przedstawiałem jako zarys koncepcyjny. Aby opanować sposób obsługi, wystarczy wejść na stronę ACE i sprawdzić, jak w tej bibliotece nazwano konkretne metody (nazwy różnią się nieznacznie od przedstawionych przeze mnie, głównie ze względu na dosyć nietypową konwencję nazewnictwa stosowaną w ACE). Korzystanie z klas ACE_Reactor oraz ACE_Event_Handler jest stosunkowo proste i w 100% przenośne między platformami. Dodatkowo ACE_Reactor daje także możliwość korzystania z uchwytów specyficznych dla danej platformy.

Biblioteka ACE udostępnia także gotowe klasy obudowujące gniazda, pliki, serwery itd. Dzięki temu nie musimy poznawać każdego z istniejących systemów operacyjnych. Obudowanie daje nam także bezpieczeństwo typu (uchwyty na platformach obsługujących POSIX są typu int, co często prowadzi do pomyłek i np. wprowadzania numeru własnego buta zamiast identyfikatora źródła informacji). Za sprawą bardzo dobrej optymalizacji ACE nadaje się do zastosowań czasu rzeczywistego.

Aby przedstawić, jak złożony jest proces implementacji tego typu rozwiązań oraz jak duży poziom optymalizacji jest zastosowany w ACE, postanowiłem opisać bardziej szczegółowo, w jaki sposób realizuje się fizycznie poszczególne elementy klasy ACE_Reactor na poszczególnych platformach.

Szczegóły implementacji funkcjonalności asynchronicznych

Implementacja operacji wejścia-wyjścia

Każdy z systemów operacyjnych udostępnia funkcję dającą możliwość zablokowania wątku, aby oczekiwać na informacje płynące ze źródeł danych. W przypadku systemów opartych o POSIX jest to funkcja select(), na platformie Windows wykorzystywana jest funkcjaWaitForMultipleObjects(). Obie wykorzystują rodzaj kontenera, który przechowuje większą liczbę uchwytów. Zadaniem klasy ACE_Reactor jest m.in. przechowanie tych uchwytów, dodawanie i usuwanie ich w sposób zależny od platformy oraz wołanie odpowiedniej funkcji. W przeciwieństwie do systemów opartych o POSIX, gdzie na jednym uchwycie mogą się pojawić tylko trzy typy informacji (nowe dane, wolny bufor wyjściowy i błąd), Windows używa bardziej ogólnych sygnałów służących do synchronizacji międzywątkowej. Natomiast sposób zasygnalizowania uchwytu musi być tutaj jawnie sprawdzony i dopiero wtedy uruchomiona odpowiednia obsługa wydarzeń.

Zdecydowaną wadą Windows jest możliwość obsługi zaledwie 64 różnych źródeł informacji (HANDLE), przy czym ACE_Reactor wewnętrznie zużywa jeszcze kolejne dwa do obsługi limitów czasowych oraz systemu powiadomień między wątkami. Natomiast ogromną zaletą API Windows jest istnienie ogólnego sygnału (HANDLE) w miejsce źródła operacji wejścia-wyjścia. Daje to możliwość wykorzystania uchwytu także jako sposobu na synchronizację wątków oraz do systemu powiadomień między wątkami. Nie daje to możliwości tworzenia jednego wątku obsługującego więcej niż 62 źródła danych (per wątek) – na Windows konieczne jest posłużenie się dodatkowo pulą wątków.

Implementacja limitów czasowych

Zarówno systemy oparte o POSIX, jak i Windows, dają możliwość podania funkcji blokującej wątek czasu, po jakim jej zawołanie ma się zakończyć, jeśli wcześniej nie pojawią się jakieś asynchroniczne informacje. Niestety, funkcja ta obsługuje tylko jeden limit czasowy. Jeśli większa liczba limitów czasowych zostanie zarządzana od ACE_Reactor, to kolejne może on dodawać do wewnętrznej kolekcji. Wtedy podczas wołania funkcji blokującej wątek pobrana zostaje najmniejsza wartość z kolekcji. Jeśli funkcja blokująca wątek zakończy się z powodu upłynięcia podanego limitu czasowego, taka sytuacja jest wykrywana i odpowiednia metoda jest wywoływana w klasie ACE_EventHandler, która zażądała ustanowienia tego limitu czasowego.

Ten sposób wykorzystywany jest na platformach opartych o POSIX. Windows daje lepszy mechanizm, który wykorzystuje funkcję CreateTimer(). Zwraca ona uniwersalne źródło sygnału (HANDLE), które może być obsługiwane w taki sam sposób jak operacje wejścia-wyjścia, tzn. podane funkcji WaitForMultipleObjects().

Systemy oparte o POSIX także dają inne rozwiązanie, które jest oparte o przechwytywanie sygnałów (wołając funkcję alarm() lub bardziej precyzyjne odpowiedniki). Jednak specyfiką tych systemów jest zatrzymywanie wszystkich wątków, aby obsłużyć sygnał. Prowadzi to do spowolnienia aplikacji. Dlatego nie jest wykorzystywane.

Implementacja systemu powiadomień

Jak już zostało powiedziane wcześniej, Windows udostępnia uchwyt ogólnego zastosowania (HANDLE). Jest to bardzo wygodne narzędzie. Jeden wątek może po prostu zasygnalizować dany uchwyt. Wszystkie wątki, które oczekują na jego sygnalizację poprzez zwołanie funkcji WaitForMultipleObjects() odbiorą ten sygnał i uruchomią jego obsługę.

W systemach opartych o POSIX podobną funkcję pełnią semafory. Niestety, POSIX nie udostępnia funkcji, która byłaby w stanie czekać jednocześnie na sygnalizację semafora oraz uchwytów wejścia-wyjścia. To sprawia, że łączenie semaforów z obsługą operacji wejścia-wyjścia jest niemożliwe. Jedyną metodą na stworzenie systemu powiadomień między wątkami jest wykorzystanie pipe-ów. Każda instancja klasy ACE_Reactor tworzy sobie jednego pipe-a i daje możliwość zapisu do niego przez inne instancje tej klasy. Druga strona odbiera wtedy dane jako operację wejścia-wyjścia, więc może wykorzystać zwykłą funkcję select() i łączyć operacje wejścia-wyjścia z systemem powiadomień.

Implementacja przechwytywania sygnałów

Różne systemy operacyjne w różny sposób obsługują przesyłanie sygnałów. POSIX przekazuje sygnał asynchroniczny do pierwszego wątku, który nie blokuje danego sygnału. Jego obsługa przypomina trochę przerwanie systemowe, kiedy to dany wątek zatrzymuje pracę i zamiast przetwarzać dalej kolejne instrukcje, przestawia się nagle na obsługę sygnału. Windows natomiast tworzy nowy wątek, który działa na dokładnie takiej samej zasadzie jak pozostałe wątki i nie ma na nie bezpośredniego wpływu (z wyjątkiem poleceń, które mogą informować inne wątki o przyjściu sygnału). W obu przypadkach uruchamiany wątek przekazuje do instancji klasy ACE_Reactor informację o tym, że pojawił się określony sygnał. Wtedy wątki ruszają, a ACE_Reactor przekazuje informację o pojawieniu się sygnału odpowiedniej instancji klasy ACE_Event_Handler. Ponieważ typy sygnałów są zależne od platformy, wiec ACE_Reactor zwraca informację o błędzie przy próbie zapisania się na nie obsługiwany na danej platformie sygnał.

Przykład rzeczywistej aplikacji

Pracując w firmie Wind Mobile miałem okazję zajmować się aplikacją NaviVoice. Jest to prawie idealny przykład wykorzystania wszystkich elementów asynchronicznego szkieletu aplikacji, dlatego zdecydowałem się przybliżyć tę aplikację.

NaviVoice to typowe oprogramowanie telekomunikacyjne. Aplikacja ta korzysta z kart telekomunikacyjnych dostarczonych przez firmę Dialogic. Komunikacja między kartą telekomunikacyjną a użytkownikiem polega na wołaniu funkcji z API przy przekazywaniu informacji sterujących do karty oraz na odbieraniu informacji przez kolejkę komunikatów. Aby umożliwić odbieranie wiadomości, API karty telekomunikacyjnej udostępnia specyficzny dla systemu identyfikator. Na platformie Windows jest to HANDLE, na platformie Linux jest to identyfikator pipe, który jest wewnętrznie używany do przesłania wiadomości między wątkami. Uchwyt taki można bez problemu używać w ACE_Reactor i oczekiwać na sygnał przyjścia nowej wiadomości.

NaviVoice jest tak naprawdę pośrednikiem – jedną z warstw w stosie telekomunikacyjnym. Po odpowiednim przetworzeniu informacji sterujących od karty telekomunikacyjnej wysyła informacje w swoim własnym protokole do aplikacji w wyższych warstwach. Robi to przez gniazda (ang. socket). Poza operacjami wejścia-wyjścia służącymi do wysyłania i odbierania komunikatów, aplikacja obsługuje także strumieniowanie mediów po RTP. Biblioteka do RTP udostępnia specyficzne dla systemu operacyjnego gniazdo, co także umożliwia korzystanie z niej poprzez ACE_Reactor. Media te są strumieniowane pomiędzy kartą telekomunikacyjną a zewnętrzną aplikacją, NaviVoice pełni tutaj funkcję pośrednika.

Ze względu na lepsze wykorzystanie mocy obliczeniowej w przypadku podziały na więcej wątków (większość serwerów ma procesory wielordzeniowe) oraz chęć podziału aplikacji na części logiczne, całość została podzielona na kilka wątków. Taki układ wymusza komunikację między wątkami poprzez kolejkę komunikatów.

Dodatkowo cechą protokołów telekomunikacyjnych jest to, że muszą mieć ustawione limity czasowe. Jeśli druga strona nie odpowie w sensownym czasie na naszą próbę połączenia, to musimy założyć, że ramka z informacją o chęci połączenia została gdzieś zgubiona. W telekomunikacji taka strata informacji jest czymś normalnym i należy przeprowadzić retransmisję danych. Jeśli natomiast przez z góry określony czas nie uda się zestawić połączenia, to musi nastąpić rozłączenie. Widać więc, że jest także potrzebna obsługa limitów czasowych.

Kolejnym koniecznym elementem systemu jest możliwość czystego wyłączenia aplikacji na życzenie użytkownika. Przed wyłączeniem aplikacja musi jeszcze przeprowadzić proces restartowania kanałów oraz ich zamknięcia, aby pozwalniać zasoby i umożliwić innym aplikacjom korzystanie z systemu. Najprostszym sposobem jest przechwycenie od systemu operacyjnego informacji o tym, że użytkownik zażądał zamknięcia aplikacji.

Ze względu na bardzo duże wymagania wydajnościowe (oprogramowanie to we wdrożeniach u polskich operatorów telefonii komórkowej obsługuje z ponad milion rozmów telefonicznych dziennie) oraz jednoznaczne zalecenia producenta kart, jedynym prawidłowym sposobem obsługi operacji wejścia-wyjścia jest tryb asynchroniczny. Jak widać, NaviVoice jest typową aplikacją, w której powinien zostać użyty szkielet oparty o klasę ACE_Reactor. Tak tez się stało. Aplikacja jest obecnie wykorzystywana w tzw. usługach wartości dodanej, pierwsze wdrożenie nowej multiplatformowej wersji ma mieć miejsce u operatora Orange do systemu podmiany tradycyjnego systemu dzwonienia na dźwięki wybrane przez użytkownika. Szkielet ACE_Reactor – ACE_Event_Handler sprawdził się w tym przypadku w 100% i prawdopodobnie będzie wykorzystywany także w kilku innych aplikacjach.