Antwoord op de vraag
Wanneer een async toekomst wordt verworpen terwijl deze is onderbroken op een await punt (zoals wanneer een zusterbranch wordt voltooid in tokio::select!), wordt de Drop implementatie synchronisch uitgevoerd om de vastgehouden middelen te vernietigen. Het risico doet zich voor wanneer de toekomst middelen bezit die asynchrone opruiming vereisen—zoals het legen van een TcpStream, het verzenden van een protocolafsluitframe of het bevestigen van een database-transactie—omdat de Drop trait geen async context biedt. Als de toekomst wordt geannuleerd na het gedeeltelijk wijzigen van de status (bijvoorbeeld het schrijven van een halve bestandbuffer) maar vóór de finalisatie, kan de synchronische Drop niet .await de voltooiing van opruimoperaties, wat mogelijk het systeem in een inconsistente staat laat of middelen laat lekken. De architectonische oplossing omvat het drop-guard patroon: het verpakken van de hulpbron in een guard struct waarvan de Drop implementatie ofwel een synchronische fallback-opruiming plant (met acceptatie van blokkeringrisico's) of de hulpbron overbrengt naar een losgekoppelde opruimtaak, waarbij wordt gezorgd dat de kritieke invariant (bijv. tijdelijke bestandsverwijdering) uiteindelijk wordt gehandhaafd zonder afhankelijk te zijn van async code binnen de destructor.
Situatie uit het leven
We ontwikkelden een media-inname service met een hoge doorvoer waar tokio::spawn gelijktijdige bestandsuploads beheerste. Elke uploadtaak schreef blokken naar een tijdelijk bestand op schijf, voerde viruscontrole uit via een extern proces, en verplaatste uiteindelijk atomisch het gevalideerde bestand naar een permanente opslagbucket. De eis was strikt: als de klant werd losgekoppeld (en taakannulering veroorzaakte via select! tussen de viruscontrole en de atomische verplaatsing), moest het tijdelijke bestand onmiddellijk worden verwijderd om uitputting van schijfruimte te voorkomen.
Oplossing 1: Synchronische opruiming in Drop. We implementeerden een TempFileGuard struct die std::fs::File en de padstring omhulde. In zijn Drop implementatie riepen we std::fs::remove_file synchronisch aan om het tijdelijke bestand te verwijderen. Voordelen: De code was eenvoudig en garandeerde uitvoering tijdens het afwikkelen van de stapel of annulering. Nadelen: std::fs::remove_file is een blokkerende syscall. Wanneer uitgevoerd op de worker threads van de Tokio runtime, blokkeerde dit de thread gedurende milliseconden onder hoge schijfbelasting, wat andere taken verhongerde en de async niet-blokkeringcontract schond. Bovendien, als het tijdelijke bestand zich op een netwerkbestandssysteem (NFS) bevond, kon de blokkering zich uitbreiden tot seconden, wat catastrofale latentiebelgen veroorzaakte.
Oplossing 2: Aangemaakte opruimtaak. In de Drop van de guard, vingen we de padstring en creëerden we een losgekoppelde tokio::task om tokio::fs::remove_file asynchroon uit te voeren. Voordelen: Dit gaf de controle onmiddellijk terug aan de runtime, waardoor de latentie behouden bleef. Nadelen: Als de runtime al aan het afsluiten was of onder extreme belasting stond, kon de opruimtaak mogelijk nooit worden uitgevoerd, wat leidde tot middelenlekken. Bovendien vereiste dit patroon dat de guard een Clone handle voor de runtime vasthield, wat de levensduur van de struct bemoeilijkte en potentiële use-after-free introduceerde als de runtime vóór de guard werd verworpen.
Oplossing 3: Expliciete annuleringstoken met synchronische fallback. We gebruikten tokio_util::sync::CancellationToken en structureerden de uploadlogica om voor annulering te controleren vóór de atomische verplaatsing. Als geannuleerd, werd een synchronische verwijdering geprobeerd alleen als het bestand onder een bepaalde groottegrens viel (snelle verwijdering), anders werd het in de wachtrij geplaatst voor een speciale achtergrond opruimthread (aangemaakt via std::thread) met een kanaal. De Drop van de guard behandelde alleen de zeldzame randgeval van een paniek, waarbij synchronische verwijdering als laatste redmiddel werd gebruikt. Gekozen oplossing: We kozen voor Optie 3. Het balansde determinisme (synchronisch pad voor kleine bestanden) met schaalbaarheid (achtergrondthread voor langzame operaties) terwijl het blokkeren van de Tokio workers werd vermeden. Het resultaat was nul gelekte tijdelijke bestanden tijdens belastingstest met 10.000 gelijktijdige annuleringen, en de p99 latentie bleef stabiel omdat de achtergrondthread de NFS-latentiekosten opnam.
Wat kandidaten vaak missen
Waarom is het aanroepen van block_on binnen een Drop implementatie om asynchrone opruiming uit te voeren fundamenteel ongezond in de meeste asynchrone runtimes?
Proberen block_on binnen Drop aan te roepen creëert een herintredingsrisico. Drop wordt synchronisch aangeroepen tijdens het afwikkelen van de stapel of wanneer een toekomst wordt geannuleerd. Als de huidige thread een worker thread van de Tokio (of async-std) runtime is, zal block_on proberen de reactor tot voltooiing te brengen voor de nieuwe toekomst. Echter, de runtime wacht al op de huidige taak (degene die wordt verworpen) om de thread vrij te geven. Dit leidt tot een deadlock: block_on wacht tot de reactor de opruimtoekomst polst, maar de reactor kan geen voortgang maken omdat de thread geblokkeerd is binnen block_on. Bovendien panikeren runtimes zoals Tokio expliciet wanneer ze geneste block_on aanroepen detecteren om dit scenario te voorkomen. De juiste aanpak is om opruiming synchronisch uit te voeren (als het onmiddellijk is) of het over te dragen aan een speciale thread via een kanaal, nooit de asynchrone executor van binnen een destructor te blokkeren.
Hoe beperkt het ontwerp van de Future::poll methode inherent annulering tot alleen bij await-punten, en waarom is dit significant voor het ontwerp van kritieke secties?
De Future::poll methode is synchronisch en moet Poll::Ready of Poll::Pending snel retourneren; het kan niet halverwege de uitvoering onderbreken. Een await punt is syntactische suiker voor de compiler-gegenereerde toestandsmachine die tussen toestanden overgaat wanneer poll Pending retourneert. De executor (of de select! macro) kan de toekomst alleen verworpen wanneer deze niet actief wordt uitgevoerd—specifiek wanneer deze Pending heeft geretourneerd en de controle heeft afgestaan. Gevolgelijk is annulering atomair met betrekking tot poll aanroepen. Dit is significant omdat het garandeert dat elke code tussen twee await punten (een "kritieke sectie") volledig of helemaal niet wordt uitgevoerd vanuit het perspectief van de asynchrone runtime. Echter, als een toekomst een MutexGuard vasthoudt over een await (wat Rust verbiedt voor standaard Mutex maar toestaat voor tokio::sync::Mutex), kan annulering gedeelde gegevens in een inconsistente staat achterlaten. Kandidaten missen vaak dat ze moeten zorgen dat de invariant van de datastructuur wordt hersteld vóór elk await punt, niet alleen aan het einde van de functie, omdat annulering Drop uitvoert op alle levende variabelen precies op dat opschorte punt.
In de context van std::pin::Pin, waarom moeten de toekomsten die in select! worden gebruikt ofwel Unpin zijn of expliciet gepind, en hoe voorkomt dit geheugen-onveiligheid tijdens gedeeltelijk verwerpen?
select! polst willekeurig meerdere toekomsten. Als een toekomst !Unpin is (bijvoorbeeld, het bevat zelf-verwijsende pointers of indringende lijstlinks), zou het verplaatsen ervan na de eerste poll die pointers ongeldig maken. Pin garandeert dat de geheugenlocatie van de toekomst stabiel blijft. select! vereist dat toekomsten Unpin zijn (waardoor verplaatsingen mogelijk zijn) of al Pin-ned zijn naar een specifieke geheugenlocatie (stapel of heap). Wanneer een branch wordt voltooid, verworpen select! de andere toekomsten. Als de toekomst Unpin was, wordt deze verplaatst naar de drop glue. Als deze Pin-ned was, wordt deze ter plaatse verworpen. De geheugenveiligheidsgarantie is afkomstig van Pin die ervoor zorgt dat drop op de toekomst wordt aangeroepen op zijn oorspronkelijke geheugenadres, waardoor het gebruik-na-vrij of dangling pointer-problemen worden voorkomen die zouden ontstaan als een zelf-verwijsende toekomst werd verplaatst (zelfs voor vernietiging) na polsen. Kandidaten vergeten vaak dat Pin niet alleen van invloed is op het polsen, maar ook op de vernietigingssemantiek van geannuleerde toekomsten.