This is a viewer only at the moment see the article on how this works.
To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk
This is a preview from the server running through my markdig pipeline
Friday, 12 December 2025
Τα περισσότερα συστήματα async είτε θυμούνται πάρα πολλά (logs, ουρά, επιμένουν σκουπίδια που ποτέ δεν ήθελες)... ή δεν θυμούνται τίποτα σε όλα (φωτιά-και-ξέχασε τις μαύρες τρύπες που εξαφανίζονται τη στιγμή που κάτι πάει στραβά).
Αυτό το άρθρο εισάγει κάτι διαφορετικό:
Ένα μοτίβο όπου κάθε λειτουργία async γίνεται ένα μικροσκοπικό ανιχνεύσιμο, Επιθεωρήσιμη, re-retrievable, Μονάδα εργασίας αυτοκαθαρισμού -και όλο το σύστημα παραμένει ιδιωτικό, περιορισμένο, γρήγορο και καθοριστικό.
Είναι η συντροφιά του προηγούμενου άρθρου μου, **Μαθαίνοντας LRUs - Όταν η υπέρβαση της ικανότητας κάνει το σύστημά σας καλύτερο**Αυτό το ένα εξερευνούσε πώς η περιορισμένη μνήμη + η συρόμενη λήξη γίνεται ένας μηχανισμός επιβίωσης.
Αυτό εξερευνεί το άλλο μισό: Περιορίζεται, εφήμερη εκτέλεση -πώς ένα μικροσκοπικό τροχαίο ρυθμιστικό των ενεργών εργασιών γίνεται αποσφαλματωτής, ένα αρχείο καταγραφής γεγονότων, και ένας κινητήρας αναπαραγωγής, χωρίς να κρατήσει τα δεδομένα των χρηστών.
Το παράδειγμα προέρχεται από το μεταφραστικό widget του blog μου -ένα μικρό UI για τη μετάφραση του περιεχομένου markdown on-the-fly.
Αυτό είναι Μέρος 1 μιας σειράς δύο μερών:
Οι προγραμματιστές ASP.NET τείνουν να επιλέγουν από τρεις κακές επιλογές:
Εσύ Task.Run() Κάτι, ελπίζω να τελειώσει, και όταν ανατιναχτεί δεν παίρνεις τίποτα.
Κόβεις τις κλωστές που δεν πρέπει να μπλοκάρεις, πεθαίνεις, εκρήγνυται η ευπρέπεια, οι χρήστες σου σε μισούν.
Τώρα έχεις:
Χρήσιμο... αλλά εξωτερικό. Επίσης: δεν μπορεί να ξαναπαίξει τίποτα και συχνά διαρρέει δεδομένα που ποτέ δεν σκόπευες να αποθηκεύσεις.
Αυτό που πραγματικά θέλουμε είναι:
Μια "αρκετά μεγάλη" ανάμνηση του τι κάνει το σύστημα - και τίποτα περισσότερο.
Ιδού η ιδέα σε μια πρόταση:
Kick off μια λειτουργία async αμέσως, παρακολουθείτε το ρητά μέσω μιας TaskClearingΠηγή, καθαρίστε το καθοριστικά, και κρατήστε ένα μικροσκοπικό rolling buffer των τελευταίων εργασιών, ώστε να μπορείτε να επιθεωρήσετε ή να τα ανακτήσει -χωρίς να διατηρήσετε οποιοδήποτε περιεχόμενο χρήστη.
Κάθεται στα μισά του δρόμου μεταξύ:
...χωρίς να γίνει κάποιος από αυτούς.
flowchart TB
subgraph Client["Client Request"]
R[Start Translation]
end
subgraph API["API Layer"]
A[Create TaskId] --> B[Create TaskCompletionSource]
B --> C[Queue to Channel]
C --> D[Return TaskId Immediately]
end
subgraph Cache["Ephemeral Cache (Bounded)"]
E[Store TranslateTask]
F[Max 5 per user]
G[6hr absolute / 1hr sliding expiry]
end
subgraph Worker["Background Worker"]
H[Read from Channel]
H --> I[Execute Translation]
I --> J[Complete TCS]
end
R --> A
B --> E
E --> F --> G
C --> H
style Cache fill:none,stroke:#10b981,stroke-width:2px
style Worker fill:none,stroke:#6366f1,stroke-width:2px
Η δομή των βασικών δεδομένων είναι TranslateTask -μια μικροσκοπική ατομική μονάδα εκτέλεσης που υποστηρίζεται από ένα Task<TaskCompletion>:
// From TranslateTask.cs
public class TranslateTask(
string taskId,
DateTime startTime,
string language,
Task<TaskCompletion>? task)
: TranslateResultTask(taskId, startTime, language)
{
public Task<TaskCompletion>? Task { get; init; } = task;
}
public record TaskCompletion(
string? TranslatedMarkdown,
string OriginalMarkdown,
string Language,
bool Complete,
DateTime? EndTime);
Περιέχει:
Task<TaskCompletion>Τίποτα δεν γράφτηκε στον δίσκο, τίποτα αποθηκευμένο πέρα από το παράθυρο.
AWS έχει Βήμα Λειτουργίες. Azure έχει Ανθεκτικές Λειτουργίες. Έχετε... 30 γραμμές κώδικα που δεν απαιτούν ένα νομοσχέδιο σύννεφο.
Η μαγεία συμβαίνει στο BackgroundTranslateServiceΑντί για φωτιά-και-ξέχασε, χρησιμοποιούμε ένα Ολοκλήρωση έργουΠηγή -ένα είδος υπόσχεσης που μας επιτρέπει να επιστρέψουμε αμέσως ενώ η δουλειά γίνεται στο παρασκήνιο.
// From BackgroundTranslateService.cs
private readonly Channel<(PageTranslationModel, TaskCompletionSource<TaskCompletion>)>
_translations = Channel.CreateUnbounded<(PageTranslationModel, TaskCompletionSource<TaskCompletion>)>();
private async Task<Task<TaskCompletion>> Translate(PageTranslationModel message)
{
// Create a TaskCompletionSource that will eventually hold the result
var tcs = new TaskCompletionSource<TaskCompletion>();
// Send the translation request along with the TCS to be processed
await _translations.Writer.WriteAsync((message, tcs));
// Return the Task immediately -caller can await it or check status later
return tcs.Task;
}
Αν δεν έχεις χρησιμοποιήσει TaskCompletionSource<T> Πριν, σκεφτείτε το ως ένα Υποσχέσου ότι θα ελέγχεις με το χέρι.. Σε αντίθεση με ένα κανονικό Task που ολοκληρώνει όταν τελειώνει η εργασία του, ένα TCS ολοκληρώνει όταν εσύ κλήση SetResult(), SetException(), ή SetCanceled().
Αυτό σας επιτρέπει:
Task στον καλούντα αμέσως.Είναι η γέφυρα μεταξύ "φωτιά-και-ξέχασε" και "φωτιά-και-track."
Όταν ένας χρήστης κάνει κλικ στο "Μετάφρασε," το frontend καλεί το API:
// From translations.js
fetch('/api/translate/start-translation', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
Language: shortCode,
OriginalMarkdown: markdown
})
})
.then(response => response.json())
.then(taskId => {
// Got a taskId immediately -translation is running in background
console.log("Task ID:", taskId);
// Poll for updates via HTMX
htmx.ajax('get', "/editor/get-translations", {
target: '#translations',
swap: 'innerHTML'
});
});
Το API επιστρέφει αμέσως με μια απλή ταυτότητα εργασίας:
// From TranslateAPI.cs
[HttpPost("start-translation")]
public async Task<Results<Ok<string>, BadRequest<string>>> StartTranslation(
[FromBody] MarkdownTranslationModel model)
{
if (!backgroundTranslateService.TranslationServiceUp)
return TypedResults.BadRequest("Translation service is down");
// Create a unique identifier for this translation task
var taskId = Guid.NewGuid().ToString("N");
var userId = Request.GetUserId(Response);
// Trigger translation -returns Task<TaskCompletion> immediately
var translationTask = await backgroundTranslateService.Translate(model);
// Wrap it in our trackable TranslateTask
var translateTask = new TranslateTask(taskId, DateTime.Now, model.Language, translationTask);
// Store in the ephemeral cache (bounded, self-cleaning)
translateCacheService.AddTask(userId, translateTask);
// Return the task ID to the client -they can poll for status
return TypedResults.Ok(taskId);
}
Η απάντηση είναι άμεση. Η πραγματική μετάφραση τρέχει στο παρασκήνιο. Ο χρήστης μπορεί να δημοσκοπήσει ή απλά να παρακολουθήσουν την ενημέρωση UI.
Η βασική εικόνα από την Άρθρο LRU ισχύει και εδώ:
Κράτα αρκετή ιστορία για να αποσφαλματώσεις και να λογικευτείς για το σύστημα... και άσε τη φυσική φθορά να σβήσει οτιδήποτε άλλο.
// From TranslateCacheService.cs
public class TranslateCacheService(IMemoryCache memoryCache)
{
public void AddTask(string userId, TranslateTask task)
{
if (memoryCache.TryGetValue(userId, out CachedTasks? tasks))
{
var currentTasks = tasks?.Tasks ?? new List<TranslateTask>();
currentTasks = currentTasks.OrderByDescending(x => x.StartTime).ToList();
// Keep only the 5 most recent tasks -bounded window
if (currentTasks.Count >= 5)
{
var lastTask = currentTasks.Last();
currentTasks.Remove(lastTask);
}
currentTasks.Add(task);
currentTasks = currentTasks.OrderByDescending(x => x.StartTime).ToList();
tasks!.Tasks = currentTasks;
memoryCache.Set(userId, tasks, new MemoryCacheEntryOptions
{
AbsoluteExpiration = tasks.AbsoluteExpiration,
SlidingExpiration = TimeSpan.FromHours(1)
});
}
else
{
// First task for this user
var cachedTasks = new CachedTasks
{
Tasks = new List<TranslateTask> { task },
AbsoluteExpiration = DateTime.Now.AddHours(6)
};
memoryCache.Set(userId, cachedTasks, new MemoryCacheEntryOptions
{
AbsoluteExpiration = cachedTasks.AbsoluteExpiration,
SlidingExpiration = TimeSpan.FromHours(1)
});
}
}
public List<TranslateTask> GetTasks(string userId)
{
if (memoryCache.TryGetValue(userId, out CachedTasks? tasks))
return tasks?.Tasks ?? new List<TranslateTask>();
return new List<TranslateTask>();
}
private class CachedTasks
{
public List<TranslateTask> Tasks { get; set; } = new();
public DateTime AbsoluteExpiration { get; set; }
}
}
Κάθε νέα εργασία:
flowchart LR
subgraph Window["Per-User Task Window (Max 5)"]
T1[Oldest Task] --- T2[Older] --- T3[Recent] --- T4[Newer] --- T5[Newest]
end
New[New Task] -->|Enqueue| Window
T1 -->|Evicted| Gone[(Expired)]
style Window fill:none,stroke:#10b981,stroke-width:2px
style Gone fill:none,stroke:#ef4444,stroke-width:2px
Δεν υπάρχει κίνδυνος κατακράτησης. Δεν PII στην κρύπτη (μόνο ταυτότητες εργασιών, χρονοσφραγίσεις, και γλωσσικούς κώδικες).
Η υπηρεσία υποβάθρου εφαρμόζει a σταθερό νόμισμα βρόχος χρησιμοποιώντας έναν αναγνώστη καναλιών και Task.WhenAny:
// From BackgroundTranslateService.cs
private async Task TranslateFilesAsync(CancellationToken cancellationToken)
{
var processingTasks = new List<Task>();
while (!cancellationToken.IsCancellationRequested)
{
// Fill up to IPCount concurrent tasks (e.g., 4 parallel translations)
while (processingTasks.Count < markdownTranslatorService.IPCount &&
!cancellationToken.IsCancellationRequested)
{
var item = await _translations.Reader.ReadAsync(cancellationToken);
var translateModel = item.Item1;
var tcs = item.Item2;
// Start the task and add it to the list
var task = TranslateTask(cancellationToken, translateModel, item, tcs);
processingTasks.Add(task);
}
// Wait for ANY of the tasks to complete
var completedTask = await Task.WhenAny(processingTasks);
processingTasks.Remove(completedTask);
// Handle exceptions if needed
try
{
await completedTask;
}
catch (Exception ex)
{
logger.LogError(ex, "Error translating markdown");
}
}
}
Αυτό είναι σαν Κυβερνήτης σε κινητήρα ατμού: περισσότερο φορτίο → πίσω πίεση → φυσικό throttling → σταθερότητα.
Το μοτίβο σας δίνει:
Όταν τελειώσει το έργο μετάφρασης, ολοκληρώνουμε το TCS:
// From BackgroundTranslateService.cs
private async Task TranslateTask(
CancellationToken cancellationToken,
PageTranslationModel translateModel,
(PageTranslationModel, TaskCompletionSource<TaskCompletion>) item,
TaskCompletionSource<TaskCompletion> tcs)
{
try
{
await retryPolicy.ExecuteAsync(async () =>
{
// Do the actual translation work
var translatedMarkdown = await markdownTranslatorService.TranslateMarkdown(
translateModel.OriginalMarkdown,
translateModel.Language,
cancellationToken);
// SUCCESS: Complete the TCS with the result
tcs.SetResult(new TaskCompletion(
translatedMarkdown,
translateModel.OriginalMarkdown,
translateModel.Language,
true,
DateTime.Now));
});
}
catch (TranslateException e)
{
// FAILURE: Complete the TCS with an exception
tcs.SetException(new Exception($"Translation failed after 3 retries: {e.Message}"));
}
catch (Exception e)
{
// UNEXPECTED: Complete the TCS with the exception
tcs.SetException(e);
}
}
Του καλούντος. Task<TaskCompletion> - που πήραν αμέσως όταν τηλεφώνησαν. Translate() - Τώρα ολοκληρώνει.
await Αν θέλουν να μπλοκάρουνIsCompleted προς δημοσκόπησηIsFaulted Για να δούμε αν απέτυχε.Result για να πάρει το μεταφρασμένο περιεχόμενοΌταν επιδεικνύουμε εργασίες στον χρήστη, προβάλλουμε την κατάσταση ζωντανής εργασίας σε ένα μοντέλο προβολής:
// From TranslateTask.cs
public TranslateResultTask(TranslateTask task, bool includeMarkdown = false)
{
TaskId = task.TaskId;
StartTime = task.StartTime;
Language = task.Language;
// Check for faulted state first -a faulted task is also "completed" in .NET terms
if (task.Task?.IsFaulted == true)
{
Failed = true;
Completed = false;
TotalMilliseconds = (int)(DateTime.Now - task.StartTime).TotalMilliseconds;
}
else if (task.Task?.IsCompletedSuccessfully == true)
{
Completed = true;
Failed = false;
var endTime = task.Task.Result.EndTime;
TotalMilliseconds = (int)((endTime - task.StartTime)!).Value.TotalMilliseconds;
EndTime = endTime;
}
else
{
// Still in progress
Completed = false;
Failed = false;
TotalMilliseconds = (int)(DateTime.Now - task.StartTime).TotalMilliseconds;
}
if (Completed && includeMarkdown)
{
var result = task.Task?.Result;
if (result == null) return;
OriginalMarkdown = result.OriginalMarkdown;
TranslatedMarkdown = result.TranslatedMarkdown;
}
}
NET's Task έχει κάποιους ιδιόμορφους συνδυασμούς κατάστασης:
IsCompleted είναι αλήθεια για και τα δύο επιτυχής ολοκλήρωση ΚΑΙ ελαττωματικές εργασίεςIsCompletedSuccessfully είναι μόνο αλήθεια για την επιτυχίαIsFaulted Σημαίνει ότι έριξε μια εξαίρεσηIsCanceled Σημαίνει ότι ακυρώθηκε.Οπότε πρέπει να το ελέγξεις. IsFaulted πριν από τον έλεγχο IsCompletedΑλλιώς θα αντιμετωπίσεις τις αποτυχίες ως επιτυχίες.
Οι δημοσκοπήσεις προβολής για ενημερώσεις που χρησιμοποιούν HTMX hx-trigger:
@* From _GetTranslations.cshtml *@
@{
var allCompleted = Model.All(x => x.Completed);
var trigger = allCompleted ? "none" : "every 5s";
}
<div class="translationpoller"
hx-get="/editor/get-translations"
hx-swap="outerHTML"
hx-trigger="@trigger">
<table class="table">
@foreach (var item in Model)
{
<tr>
<td>
@if (item.Completed)
{
<a href="#" x-on:click.prevent="viewTranslation('@item.TaskId')">View</a>
}
else if (item.Failed)
{
<text>Failed</text>
}
else
{
<text>Processing</text>
}
</td>
<td>
@if (item.Completed)
{
<i class='bx bx-check text-green'></i>
}
else if (item.Failed)
{
<i class='bx bx-x text-red'></i>
}
else
{
<img src="~/img/3-dots-bounce.svg" />
}
</td>
<td>@item.Language</td>
<td>@TimeSpan.FromMilliseconds(item.TotalMilliseconds).Humanize()</td>
</tr>
}
</table>
</div>
Το έξυπνο κομμάτι: hx-trigger="@trigger" αλλαγές με βάση την κατάσταση:
"none")Αυτό είναι αυτορυθμιζόμενες δημοσκοπήσεις - Σταματάει αυτόματα όταν δεν υπάρχει τίποτα να δεις.
Όταν ο χρήστης κάνει κλικ στο "Προβολή," φέρνουμε την ολοκληρωμένη μετάφραση:
// From TranslateAPI.cs
[HttpGet("get-translation/{taskId}")]
public async Task<Results<JsonHttpResult<TranslateResultTask>, BadRequest<string>>> GetTranslation(
string taskId)
{
var userId = Request.GetUserId(Response);
var tasks = translateCacheService.GetTasks(userId);
var translationTask = tasks.FirstOrDefault(t => t.TaskId == taskId);
if (translationTask == null)
return TypedResults.BadRequest("Task not found");
// Include the markdown content in the response
var result = new TranslateResultTask(translationTask, includeMarkdown: true);
return TypedResults.Json(result);
}
Η JavaScript στη συνέχεια κοσμεί τον επεξεργαστή:
// From translations.js
export function viewTranslation(taskId) {
fetch(`/api/translate/get-translation/${taskId}`)
.then(response => response.json())
.then(data => {
// Show the translated content area
document.getElementById("translatedcontent").classList.remove("hidden");
// Populate the editors
var originalMde = window.mostlylucid.simplemde.getinstance('translatedcontentarea');
originalMde.value(data.originalMarkdown);
var mde = window.mostlylucid.simplemde.getinstance('markdowneditor');
mde.value(data.translatedMarkdown);
});
}
Επειδή:
Αυτή είναι η αρχιτεκτονική. Εύχομαι τα προγράμματα περιήγησης και τα πλαίσια frontend να είχαν χρησιμοποιηθεί για εργασίες συνεδρίας.
Τι παίρνεις χωρίς να σκέφτεσαι πολύ σκληρά:
Πως να το κάνετε αυτό; |---------|-----| | Σταθερότητα Βραχυκύκλωμα μέσω καναλιού + μοτίβο semaphore | Αρνητικός βρόχος ανατροφοδότησης Περισσότερο φορτίο → πίσω πίεση → οριοθετημένη μνήμη → συνεπής συμπεριφορά | Προβολή αποσφαλμάτωσης Οι ακριβείς λειτουργίες είναι παρούσες μόλις αρκετό καιρό για να επιθεωρήσουν | Αναθεώρησις Τα ολοκληρωμένα αποτελέσματα είναι προσβάσιμα μέχρι τη λήξη τους. | Μηδενική κατακράτηση δεδομένων □ Απόρρητο και απλότητα ευθυγραμμισμένη για μία φορά | Αυτοπεποίθηση εκτέλεσης Οι παλιές δουλειές παραλείπουν, οι σχετικές παραμένουν
Ακριβώς όπως η κρύπτη LRU ακονίζει τη μνήμη συμπεριφοράς, το παράθυρο λειτουργίας κυλώντας ακονίζει την προβολή εκτέλεσης.
Χρησιμοποιήστε το όταν:
Μην το χρησιμοποιείτε όταν:
Εάν η Άρθρο LRU ήταν περίπου μάθηση ξεχνώντας, αυτό είναι περίπου εκτέλεση με εξάτμιση.
Το σύστημα θυμάται ακριβώς αρκετά για να βοηθήσει - και τίποτα περισσότερο.
Είναι:
Το καλύτερο πράγμα; Ξέρετε ήδη πώς να το εφαρμόσετε: TaskCompletionSource, α Channel, a IMemoryCache με συρόμενη λήξη, και έναν εργάτη υποβάθρου.
Φωτιά... και μην ξεχνάς.
Το Μέρος 2ο: Κατασκευή μιας Αναξιοποίητης Βιβλιοθήκης Εκτέλεσης της Εφήμερης, θα μετατρέψουμε αυτό το μοτίβο σε ένα drop-in βοηθό:
EphemeralForEachAsync<T> - όπως Parallel.ForEachAsync αλλά με την παρακολούθηση λειτουργίαςEphemeralWorkCoordinator<T> - μια μακροχρόνια ορατή ουρά εργασίαςAddEphemeralWorkCoordinator<TCoordinator> (όπως AddHttpClient)© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.