A C++20 koordinációs eszközei – a <semaphore>,
<latch>, <barrier> és
<syncstream> – a mutex és condition variable fölötti,
gyakori mintákra szabott absztrakciók. Nem helyettesítik a
memóriamodellt, és nem adnak automatikus cancellationt vagy fairness-t:
a helyes használatot a hívónak kell szerződésként betartania. Ez a rész
a RAII lockok, condition variable és wait
predicate mintájára épít, a promise, future,
async és task eredmény felé pedig ott kapcsolódik, ahol a várakozás
eredménye és a task élettartama válik fontossá. A
<barrier>, <latch>,
<semaphore> és <syncstream>
könyvtári támogatása compiler- és standard library verziófüggő; a
konkrét buildprofilban ellenőrizni kell. Az itteni példák C++20-at
feltételeznek, és nem állítják, hogy fordításuk vagy futásuk ebben a
szövegben megtörtént.
Mind a négy eszköz koordinációt végez: egy művelet láthatóvá tesz egy
korábbi állapotot egy másik szálnak. A szemafor release
művelete szinkronizál azzal az acquire-rel, amely az
inkrementált számlálót megfigyeli; a latch utolsó
count_downja szinkronizál a wait
visszatérésével; a barrier fázisának completionje szinkronizál a fázisra
várakozók visszatérésével; az osyncstream::emit pedig a
mögöttes streambuf felé továbbított rekord koherens átvitelét
biztosítja. Ez nem azt jelenti, hogy a primitívek minden mellékhatást
„varázsolnak” – a programozónak kell biztosítania, hogy a megosztott
adatot csak a megfelelő szinkronizációs pont után olvassa.
| Eszköz | Ismételhető | Tipikus használat | Fő élettartam-kockázat |
|---|---|---|---|
counting_semaphore |
igen | kapacitáskorlát, engedély, jelzés | túlrelease; várakozó a destruktorban |
latch |
nem | egyszeri startkapu, inicializálás befejezése | count_down túllépés; várakozó a destruktorban |
barrier |
igen | ismétlődő fázisok szinkronizálása | kimaradó résztvevő; completion szerződésszegés |
osyncstream |
igen | logrekord-koherencia | a mögöttes stream élettartama; megosztott objektum |
Közös szabály, hogy a semaphore, latch és barrier destruktorának előfeltétele: ne legyen blokkolt várakozó szál. Ennek megsértése nem definiált viselkedés; a gyakorlatban holtpont, összeomlás vagy csendes adatkorrupció lehet. A primitív nem tartja életben a várakozót, és a várakozó nem tartja életben a primitívet.
Bizonyításvázlat (vázlat). Ha egy szál a
release/count_down/arrive
műveletet hajtja végre, majd egy másik szál sikeresen visszatér a
megfelelő várakozásból, a szabvány synchronization relationje miatt az
előbbi szál előtti írások láthatók az utóbbi szálban. Ezért a „jelző”
primitívek nem csupán időzítésre, hanem publikálásra is használhatók; a
megosztott adatot azonban továbbra is védeni kell az egyidejű
írásoktól.
A std::counting_semaphore<LeastMaxValue> egy nem
másolható számlálót tart. Az acquire() addig blokkol, amíg
a számláló nullánál nagyobb, majd csökkenti azt; a
try_acquire(), try_acquire_for() és
try_acquire_until() nem blokkoló, illetve időkorlátos
változatok. A release(update = 1) növeli a számlálót, és
felébreszthet várakozókat. A std::binary_semaphore a
counting_semaphore<1> aliasa.
A release előfeltétele, hogy a növelés ne vigye a
számlálót a max() fölé:
.
A max() legalább a template paraméter, de
implementációfüggően nagyobb is lehet. Fontos következmény: a
konstruktor kezdőértéke nem felső korlát. Ha
counting_semaphore<2> s(2)-t hozunk létre, attól még
a max() nagyobb lehet, és a „legfeljebb kettő engedély”
invariantot a programnak kell betartania, nem a szemafor. A túlrelease
nem „csak” logikai hiba: előfeltétel-sértés, tehát nem definiált
viselkedés.
A szemafornak nincs tulajdonosa abban az értelemben, mint a mutexnek:
a release-et más szál is végrehajthatja, mint amelyik az
acquire-t. Ez alkalmas jelzésre és kapacitáskorlátra, de
nem nyújt kölcsönös kizárást „tulajdonosi” garanciával. Kölcsönös
kizáráshoz továbbra is a std::mutex a természetes
választás; a bináris szemafor inkább ping-pong jelzésre vagy esemény
jelzésére való. C-ben a POSIX sem_t hasonló szerepet tölt
be, de a sem_destroy élettartam-feltétele és a névvel
ellátott szemaforok kezelése külön figyelmet igényel.
Leállításnál a counting_semaphore nem kínál
stop_token túlterhelést. A try_acquire_for
időkorlátos ciklusa adhat korlátos reakcióidőt, de ez polling: a stop
kérés és a várakozás vége között legfeljebb a választott időköz telik
el. Megszakítható várakozásra a C++20
condition_variable_any stop_token-es
wait változata a természetes eszköz; a szemaforral épített
„stop-aware” kaput külön kell megtervezni.
A std::latch egy egyszer használható leszámláló. A
konstruktorban megadott számlálót a count_down(update = 1)
csökkenti; a wait() addig blokkol, amíg a számláló nullává
nem válik; a try_wait() nem blokkoló ellenőrzés; az
arrive_and_wait(update = 1) a count_down és a
wait összevonása. A count_down előfeltétele,
hogy a frissítés ne haladja meg az aktuális számlálót. Ha többször
csökkentenénk nullánál, az előfeltétel-sértés.
A latch tipikus használata a startkapu: a fő szál létrehoz egy
std::latch start(1) objektumot, a munkások
start.wait()-tel várakoznak, majd a fő szál
start.count_down() hívásával egyszerre engedi el őket.
Ugyanígy használható „minden rész inicializálva” kapuként:
résztvevő count_down után a várakozó továbblép. A latch nem
resetelhető; ismétlődő fázisokhoz barrier kell. A latch élettartama
alatt a számláló monoton csökken, és nulla után már nem nő.
A std::barrier egy fix résztvevőszámú, ismételhető
fáziskapu. A konstruktor megadja a várt érkezők számát és opcionálisan
egy completion függvényt. A résztvevők arrive()-tal
érkeznek, amely egy arrival_token-t ad vissza, majd
wait(token)-tel várnak a fázis befejezésére; az
arrive_and_wait() a kettőt együtt végzi. Amikor az utolsó
résztvevő is megérkezik, a completion függvény lefut, majd a várakozók
visszatérnek, és kezdődhet a következő fázis.
A completion szerződése szigorú: pontosan a fázis utolsó érkezője futtatja, a várakozók csak utána térnek vissza, nem dobhat kivételt, és nem hívhatja a barrier tagfüggvényeit. A gyakorlatban rövid, nem blokkoló művelet legyen; a „ completionben mindenki helyett mindent megcsinálok” minta könnyen szűk keresztmetszet vagy holtpont forrása. A completion alatt a barrier már a következő fázisra készül, ezért abból a barrierre hivatkozni különösen veszélyes.
Az arrive_and_drop() a hívó szálat véglegesen kiveszi a
következő fázisokból: az aktuális fázisban még érkezik, de a várt
résztvevőszám csökken. Csak olyan szál hívhatja, amely részt vesz az
aktuális fázisban, és utána már nem használhatja a barriert. Ha egy
résztvevő egyszerűen kilép, és nem hív arrive_and_drop-ot,
a többi szál örökre várhat. A barrier nem tudja kívülről „leszállítani”
a várt számot; a drop az egyetlen mechanizmus. Leállításnál ezért vagy
minden résztvevőnek végig kell mennie egy utolsó fázison, vagy a dropot
gondosan, közös protokoll szerint kell használni.
osyncstream:
rekordkoherenciaA std::osyncstream (pontosabban
basic_osyncstream<char>) egy puffert tart, és az
emit() híváskor – illetve a destruktorban – a puffert
egybefüggő rekordként továbbítja a mögöttes streambuf felé. A cél, hogy
a több szálból írt logüzenetek ne csússzanak egymásba. Nem globális
sorrendet ad: két rekord sorrendje attól függ, melyik emit
éri el előbb a mögöttes streamet. A rekordkoherencia csak a syncstreamen
keresztül történő emitokra vonatkozik; a közvetlen
std::cout-írásokkal való keveredést külön kell kezelni.
A legegyszerűbb minta a rekordonkénti ideiglenes objektum:
void log_line(const char* who, int round) {
std::osyncstream(std::cout) << who << " round " << round << '\n';
}Az ideiglenes osyncstream a teljes kifejezés végén
megsemmisül, és ekkor emitál. Egy szálon belül létrehozott, hosszabb
életű osyncstream is használható, de egy objektumot nem
szabad több szál között megosztani: az operator<< és
a belső puffer nem válik thread-safe-é attól, hogy az emit
szinkronizált. A mögöttes streamnek és streambufnak túl kell élnie az
osyncstream-et; ha az osyncstream egy lokális
std::ostringstream-re mutat, amely már megsemmisült, a
destruktor emitja dangling streambufra ír. Globális
osyncstream esetén a statikus megsemmisítési sorrend külön
kockázat, ezért a gyakorlatban lokális vagy szálonkénti példányt érdemes
használni. Ha a mögöttes stream kivételre van kapcsolva, az emit dobhat;
a destruktorban ez terminate-hez vezethet, ezért a hibakezelést ne a
destruktorra bízzuk.
A legfontosabb élettartam-szabály: a semaphore, latch és barrier
destruktora előtt minden várakozónak ki kell lépnie. Ez tipikusan azt
jelenti, hogy a tulajdonos szál joinolja a munkásokat, vagy egy külön
leállítási fázisban mindenkit beenged. A std::jthread
automatikus joinja önmagában nem old meg mindent: a stop kérés
kooperatív, és nem szakít meg egy semaphore.acquire() vagy
latch.wait() hívást. Ha a munkás ezekben blokkol, a
jthread destruktora joinol, és a program holtpontba
kerülhet. A stopot ezért a várakozásba kell beépíteni: condition
variable esetén a stop_token-es wait
túlterheléssel, szemafor esetén időkorlátos try_acquire_for
ciklussal, barrier esetén pedig explicit leállítási fázissal vagy
arrive_and_drop-pal.
A fairness nem garantált. A release,
count_down és arrive által felébresztett
várakozó kiválasztása nem specifikált; a primitívek nem FIFO-k, és nem
biztosítanak prioritást. Éhezés előfordulhat, különösen sok várakozó és
ritka release esetén. Ha a sorrendiség üzleti követelmény, azt külön
adatszerkezettel és mutex/CV párossal kell megvalósítani, nem a primitív
„fairnek” feltételezett viselkedésére bízva.
A következő példa négy munkást indít, egy latch startkapuval engedi
el őket, egy kétengedélyes szemaforral korlátozza az egyidejű „IO”
szakaszt, egy barrierrel pedig három fázisban szinkronizál. A naplózás
rekordonkénti osyncstream-mel történik. A tulajdonos a
main: a latch, a szemafor, a barrier és a
std::vector<std::thread> a main
élettartamában él; a munkások referenciákat kölcsönöznek. Invalidáló
művelet a primitívek destruktora, ha várakozó van; a hiba kimenete
holtpont vagy nem definiált viselkedés. A példa feltételezi, hogy a
szálak létrehozása sikerül; termelési kódban a kivételbiztos joinról
külön kell gondoskodni.
// sync_demo.cpp — C++20
#include <atomic>
#include <barrier>
#include <chrono>
#include <cstddef>
#include <cstring>
#include <exception>
#include <iostream>
#include <latch>
#include <semaphore>
#include <stdexcept>
#include <syncstream>
#include <thread>
#include <vector>
namespace {
void log_line(int who, int round) {
std::osyncstream(std::cout) << "worker " << who << " round " << round << '\n';
}
struct SlotGuard { // a szemafor kivételes úton is felszabadul
std::counting_semaphore<2>& s;
explicit SlotGuard(std::counting_semaphore<2>& sem) : s(sem) { s.acquire(); }
~SlotGuard() { s.release(); }
};
struct StartGuard { // a főszál unwindingja esetén is elengedi
std::latch& start; // a workereket, de futás nélkül
std::atomic<bool>& abort;
bool released = false;
void release() { released = true; start.count_down(); }
~StartGuard() { if (!released) { abort = true; start.count_down(); } }
};
} // namespace
int main(int argc, char** argv) {
constexpr int workers = 4;
constexpr int rounds = 3;
const bool worker_fails = argc > 1 && std::strcmp(argv[1], "worker-fail") == 0;
const bool main_fails = argc > 1 && std::strcmp(argv[1], "main-fail") == 0;
std::latch start(1);
std::atomic<bool> abort_run{false};
std::counting_semaphore<2> io_slots(2);
std::barrier round_done(workers, []() noexcept {
std::osyncstream(std::cout) << "-- phase complete --\n";
});
std::vector<std::exception_ptr> errors(workers); // szálanként saját hely
try {
std::vector<std::jthread> pool; // unwinding esetén is join
pool.reserve(workers);
StartGuard guard{start, abort_run}; // a pool UTÁN: előbb fut le
for (int i = 0; i < workers; ++i) {
pool.emplace_back([&, i] {
start.wait();
if (abort_run) return; // indítás meghiúsult: nincs munka
try { // a worker kivételhatára
for (int r = 1; r <= rounds; ++r) {
{
SlotGuard slot(io_slots);
std::this_thread::sleep_for(std::chrono::milliseconds(5));
if (worker_fails && i == 2 && r == 2)
throw std::runtime_error("disk error");
log_line(i, r);
}
round_done.arrive_and_wait();
}
} catch (...) {
errors[i] = std::current_exception();
round_done.arrive_and_drop(); // a többiek ne várjanak rá
}
});
}
if (main_fails) throw std::runtime_error("setup failed");
guard.release();
} catch (const std::exception& e) { // ide csak a főszál hibája jut
std::cerr << "main: " << e.what() << '\n';
return 1;
}
int failed = 0; // a join után az errors olvasható
for (int i = 0; i < workers; ++i) {
if (!errors[i]) continue;
++failed;
try { std::rethrow_exception(errors[i]); }
catch (const std::exception& e) {
std::cerr << "worker " << i << ": " << e.what() << '\n';
}
}
return failed == 0 ? 0 : 1;
}Fordítás GNU környezetben:
g++ -std=c++20 -O2 -pthread -Wall -Wextra -Wpedantic sync_demo.cpp -o sync_demoA -pthread a szálkezeléshez szükséges. A példa nem
használ stopot: a leállítás a ciklus természetes végével történik. A
szemafor nem fair, ezért a példa nem garantálja, hogy minden munkás
ugyanannyi időt kap.
A hibakezelésnél két külön határt kell megtervezni.
join előtt kivétellel lép ki
(például egy további emplace_back vagy beállítás hibázik),
a joinable std::thread destruktora
std::terminate-et hív. Ez ellen véd a
std::jthread, amely a destruktorában joinol. Önmagában
azonban kevés: a szálak a start.wait()-ben állnak, így a
join örökre várna. Ezért a StartGuard unwinding esetén
megszakítást jelez, és elengedi a latch-et; a workerek munka nélkül
kilépnek, és a join befejeződik.std::terminate-et vált ki, és
ezen sem a jthread, sem egy join-guard nem változtat: azok
a létrehozó oldalt védik, nem a szál belépési pontját. A workernek ezért
magának kell elkapnia a kivételt, és dokumentált csatornán jelentenie:
itt egy szálanként külön std::exception_ptr helyen, amelyet
a főszál a join után olvas. A puszta catch és
korai return viszont új holtpontot okozna, mert a többi
worker a barrierben a kilépett résztvevőre várna. Ezért a hibaúton
arrive_and_drop() következik: a worker az aktuális fázisba
még beérkezik, a további fázisokból pedig kivonja magát. A szemafort a
SlotGuard a kivételes úton is felszabadítja../sync_demo # normál futás
./sync_demo worker-fail # a 2-es worker a 2. körben dob
./sync_demo main-fail # a főszál az indítás előtt dobBuildjegyzőkönyv (lab-gcc profil). A
program figyelmeztetés nélkül fordult. Normál futásban 12 körsor és 3
fáziszárás, kilépési kód 0. A worker-fail esetben a 2-es
worker az 1. kör után kiesik: 10 körsor és 3 fáziszárás után a főszál
worker 2: disk error üzenettel, 1-es kóddal lép ki,
holtpont és terminate nélkül. A main-fail
esetben main: setup failed, 0 körsor, 1-es kód, holtpont
nélkül. Mindhárom eset ThreadSanitizer (setarch -R) alatt
is figyelmeztetés nélkül futott.
release túllépi a max()-ot, vagy a latch
count_down többel csökkent, mint a számláló. Mindkettő
előfeltétel-sértés, nem „csak” logikai hiba.max(); a kettő nem ugyanaz.arrive_and_drop nélkül,
mire a többi örökre vár. Leállításnál a fázistopológia nem változhat
önkényesen.osyncstream objektumot több szál használ, vagy az
osyncstream túléli a mögöttes
ostringstream-et. Utóbbi dangling streambuf, a destruktor
emitja nem definiált.jthread stop kérésétől várjuk, hogy megszakítsa a
semaphore.acquire()-t vagy a latch.wait()-et.
Nem szakítja meg; a join holtpont lehet.Válaszd ki a megfelelő C++20 eszközt, és indokold a döntést. Mindhárom esetben nevezd meg azt is, miért nem a másik kettőt választanád.
Nyolc munkásnak meg kell várnia, hogy a konfiguráció betöltődjön; ez egyszer történik meg a program indulásakor.
Legfeljebb három párhuzamos adatbázis-kapcsolat lehet egyszerre.
Négy szál tíz iterációban dolgozik, és minden iteráció végén mindenkinek meg kell várnia a többieket, majd jöhet a következő iteráció.
Írj egy acquire_stop_aware függvényt, amely egy
std::counting_semaphore<N> engedélyét próbálja
megszerezni, de std::stop_token kérés esetén legfeljebb egy
rövid időkorlátnyi késéssel visszatér. Készíts hozzá olyan RAII
engedélyőrt, amely a már megszerzett engedélyt „adoptálja”, és a
destruktorában release-el. Magyarázd meg, miért nem lehet a
semaphore.acquire()-t egyszerűen megszakítani.
A következő részlet három hibát tartalmaz. Azonosítsd őket, mondd meg, melyik szabályt sértik, és javítsd őket.
// részlet, nem önálló program
std::latch ready(2);
ready.count_down(3); // (A)
std::barrier b(2);
// az egyik résztvevő szálban:
b.arrive_and_drop();
b.arrive_and_wait(); // (B)
std::osyncstream make_log() {
std::ostringstream oss;
return std::osyncstream(oss); // (C)
}std::latch, például
std::latch ready(1): a munkások wait()-tel
várnak, a betöltő count_down()-nal nyit. Azért nem barrier,
mert a kapu egyszeri, és nincs ismétlődő fázis; azért nem szemafor, mert
nem kapacitást osztunk, hanem egy egyszeri eseményre várunk.
std::counting_semaphore<3> slots(3): az
acquire()/release() pár adja a
kapacitáskorlátot. Azért nem latch, mert a művelet ismétlődik; azért nem
barrier, mert a résztvevők nem fázisonként szinkronizálnak, hanem
erőforrásért versenyeznek.
std::barrier rounds(4): minden iteráció egy fázis, a
résztvevők száma fix. Azért nem latch, mert a latch nem resetelhető;
azért nem szemafor, mert nem engedélyt osztunk, hanem mindenkinek meg
kell várnia mindenkit.
#include <chrono>
#include <cstddef>
#include <mutex>
#include <semaphore>
#include <stop_token>
template <std::ptrdiff_t N>
bool acquire_stop_aware(std::counting_semaphore<N>& sem, std::stop_token st) {
while (!st.stop_requested()) {
if (sem.try_acquire_for(std::chrono::milliseconds(10))) {
if (st.stop_requested()) { // stop-prioritás: a közben megszerzett
sem.release(); // engedélyt visszaadjuk
return false;
}
return true;
}
}
return false;
}
template <std::ptrdiff_t N>
class sem_guard {
public:
explicit sem_guard(std::counting_semaphore<N>& s) : s_(&s) { s_->acquire(); }
sem_guard(std::counting_semaphore<N>& s, std::adopt_lock_t) noexcept : s_(&s) {}
~sem_guard() { if (s_) s_->release(); }
sem_guard(const sem_guard&) = delete;
sem_guard& operator=(const sem_guard&) = delete;
sem_guard(sem_guard&& other) noexcept : s_(other.s_) { other.s_ = nullptr; }
private:
std::counting_semaphore<N>* s_;
};A stop-aware ág használata:
if (acquire_stop_aware(io_slots, st)) {
sem_guard<2> g(io_slots, std::adopt_lock);
// ...
}A 10 ms a try_acquire_for névleges várakozási
ideje, vagyis a stop-ellenőrzés polling-intervalluma, nem szigorú felső
korlát: a szabvány megengedi, hogy az időkorlátos várakozás az ütemezés
és az erőforrás-versengés miatt később térjen vissza ([C++20]
[thread.req.timing]), és a visszatérés után a szálnak még ütemeződnie is
kell. Szigorú leállási határidőt csak a platform ütemezési garanciáival
együtt lehet vállalni. A stop és az engedélyszerzés egyidejűségekor két
kimenet megengedett: ha a szerzés győz, a függvény igazat ad, és a hívó
a guarddal visszaadja az engedélyt; ha stop-prioritás kell, a fenti
változat a megszerzés után még egyszer megnézi a stopot, és a már
megszerzett engedélyt visszaadja. Az engedélyszám mindkét kimenetnél
megmarad. A semaphore.acquire() azért nem szakítható meg,
mert a szemafor API-jának nincs stop_token paramétere; a
szál blokkolása a primitív belső várakozásában történik, amelyet
kívülről nem lehet „kiszakítani”. Az adoptáló konstruktor
elengedhetetlen: ha a guard konstruktora újra acquire-t
hívna, a már megszerzett engedély mellé egy másodikat is foglalnánk.
Ellenőrzési feltétel: stop nélkül a függvény a release után igazzal tér
vissza; stop kérés után — a névleges intervallum és az ütemezési késés
után — hamissal, és a sikeres ágban a guard pontosan egyszer release-el.
A teszt ne a 10 ms-os határidőt mérje, hanem az engedélymegmaradást.
Buildjegyzőkönyv (lab-gcc profil). Egy
2000 iterációs versenyteszt (counting_semaphore<1>, a
release és a request_stop mindkét sorrendben)
minden iteráció után pontosan egy engedélyt talált a szemaforban. Normál
buildben mind a 2000 eset a stop ágra futott, TSan alatt
(setarch -R) 750 megszerzés és 1250 stop — mindkét
megengedett kimenet előfordult, engedély nem veszett el.
A ready.count_down(3) előfeltétele, hogy a frissítés
ne haladja meg a számlálót; itt a számláló 2, a frissítés 3. Ez
előfeltétel-sértés, nem definiált viselkedés. Javítás:
ready.count_down(2), vagy ha három külön esemény van,
std::latch ready(3).
Az arrive_and_drop() után a szál véglegesen kimarad
a barrierből, és nem hívhatja újra a barrier tagfüggvényeit. A
b.arrive_and_wait() utána szerződésszegés. Javítás: a drop
után a szál ne használja a barriert; ha még részt kell vennie, ne
dropoljon, hanem normál arrive_and_wait-tel zárja le a
fázist, és a leállást külön protokollal kezelje.
A visszatérő osyncstream a lokális oss
streambufjára mutat, amely a függvény végén megsemmisül. A visszatérés
után az osyncstream destruktorának emitja dangling
streambufra ír. Javítás: az oss-nek túl kell élnie az
osyncstream-et, például a hívóban hozzuk létre mindkettőt,
vagy a függvény előbb emitáljon, majd a puffert
std::string-ként adja vissza. Ellenőrzési feltétel: az
osyncstream élettartama soha ne haladja meg a mögöttes
stream/streambuf élettartamát, és a destructor ne fusson olyan
objektumon, amelynek a streambufja már megszűnt.
A counting_semaphore kapacitást és engedélyt modellez; a
latch egyszeri, a barrier ismétlődő fáziskapu;
az osyncstream a logrekordok koherenciáját adja. Egyik sem
ad automatikus cancellationt vagy fairness-t, és mindegyiknél kritikus a
várakozók élettartama: a destruktor előtt nem maradhat blokkolt
várakozó. A stopot a várakozásba kell beépíteni – condition variable
esetén stop_token-nel, szemafor esetén időkorlátos
próbálkozással, barrier esetén explicit leállítási fázissal. A
primitívek szinkronizációs pontot adnak, de a megosztott adat
tulajdonjogát és a hozzáférési szerződést továbbra is a programnak kell
biztosítania.