Async work and resource handling
Follow capacity acquisition, child settlement and cleanup after failure. Compare those obligations, not only async/await spelling. See readability under real business rules for policy branches, initialization and coordination.
A document dashboard displays the byte size of each uploaded file. An unreadable file is represented by -1; successful empty files have size 0. The dashboard reads at most two files at once and keeps results in input order. This is an I/O workload, so overlapping waits can help without making each file read faster.
The file paths are already validated strings, and the source is a finite list. Files must fit the runtime's byte-array and Integer length limits. The comparison uses no cancellation request. Each child converts expected filesystem access failures into a result, so the example does not depend on how a concurrent scope aggregates multiple escaping exceptions.
DirectSharp: structured enumeration
These actions belong in the document service. ReadSizeAsync uses ordinary .NET file APIs. LoadSizesAsync publishes the collected results after the concurrent loop has joined its iterations.
// Dashboard policy: preserve input order, bounded I/O and the stated error result.
async action ReadSizeAsync(path as String)
returns Integer
try
let bytes = await Global.System.IO.File.ReadAllBytesAsync(path)
return bytes.Length
catch error as Global.System.IO.IOException
return -1
catch error as Global.System.UnauthorizedAccessException
return -1
end try
end action
async action LoadSizesAsync(paths as Global.System.Collections.Generic.IEnumerable <of String>)
returns Global.System.Collections.Generic.IEnumerable <of Integer>
require paths is not nothing otherwise new Global.System.ArgumentNullException("paths")
concurrent for_each path in paths collects sizes as Integer with limit: 2
collect sizes = await ReadSizeAsync(path)
end for_each
return sizes
end action
Inside an async action, the logical value of the imported Task-returning file operation is consumed in sequence. The concurrent loop creates overlap between different paths; statements within one iteration remain sequential. Its collected values preserve source order, even if the third file finishes before the first. Empty input completes with no iterations and an empty result.
The limit is an integer here. A shared Gate can instead coordinate several scopes against the same capacity. A limit of zero or a negative integer is invalid. An ordinary for_each would process the same files sequentially.
C#: tasks and an explicit semaphore
// Dashboard policy: preserve input order, bounded I/O and the stated error result.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
public static async Task<int> ReadSizeAsync(string path)
{
try
{
byte[] bytes = await File.ReadAllBytesAsync(path);
return bytes.Length;
}
catch (IOException) { return -1; }
catch (UnauthorizedAccessException) { return -1; }
}
public static async Task<int[]> LoadSizesAsync(IEnumerable<string> paths)
{
if (paths is null)
throw new ArgumentNullException(nameof(paths));
using var gate = new SemaphoreSlim(2);
async Task<int> ReadBoundedAsync(string path)
{
await gate.WaitAsync();
try { return await ReadSizeAsync(path); }
finally { gate.Release(); }
}
Task<int>[] tasks = paths.Select(ReadBoundedAsync).ToArray();
return await Task.WhenAll(tasks);
}
Task.WhenAll returns the values in task-array order. The finally returns the permit when an iteration finishes or throws. The semaphore is disposed only after normal completion of the joined tasks. This implementation eagerly creates one task per path; it is suitable for a modest finite batch. A streaming implementation can use a worker pool or Parallel.ForEachAsync and explicitly preserve result ordering.
VB.NET: the same .NET primitives
' Dashboard policy: preserve input order, bounded I/O and the stated error result.
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.Collections.Generic
Imports System.IO
Imports System.Threading
Imports System.Threading.Tasks
Public Async Function ReadSizeAsync(path As String) As Task(Of Integer)
Try
Dim bytes As Byte() = Await File.ReadAllBytesAsync(path)
Return bytes.Length
Catch ex As IOException
Return -1
Catch ex As UnauthorizedAccessException
Return -1
End Try
End Function
Private Async Function ReadBoundedAsync(path As String,
gate As SemaphoreSlim) As Task(Of Integer)
Await gate.WaitAsync()
Try
Return Await ReadSizeAsync(path)
Finally
gate.Release()
End Try
End Function
Public Async Function LoadSizesAsync(paths As IEnumerable(Of String)) As Task(Of Integer())
If paths Is Nothing Then Throw New ArgumentNullException(NameOf(paths))
Using gate As New SemaphoreSlim(2)
Dim tasks As New List(Of Task(Of Integer))()
For Each path As String In paths
tasks.Add(ReadBoundedAsync(path, gate))
Next
Return Await Task.WhenAll(tasks)
End Using
End Function
This separates the bounded helper instead of using C#'s local function. It preserves the same semaphore lifetime and task order. The difference in length partly reflects where each language places that helper, rather than an additional business requirement.
Python: asyncio with thread-based file reads
# Dashboard policy: preserve input order, bounded I/O and the stated error result.
import asyncio
from pathlib import Path
from collections.abc import Iterable
async def read_size_async(path: str) -> int:
try:
contents = await asyncio.to_thread(Path(path).read_bytes)
return len(contents)
except OSError:
return -1
async def load_sizes_async(paths: Iterable[str]) -> list[int]:
if paths is None:
raise ValueError("paths is required")
gate = asyncio.Semaphore(2)
async def read_bounded(path: str) -> int:
async with gate:
return await read_size_async(path)
return list(await asyncio.gather(*(read_bounded(path) for path in paths)))
Python's standard file API is synchronous, so this version moves reads to worker threads. gather preserves argument order. async with releases the semaphore on coroutine exit. The .NET versions use their asynchronous file APIs. These are equivalent successful results and expected filesystem-error results under the stated inputs; they are not identical I/O engines or performance models.
Failure, cancellation and batch size
For files of 3 and 0 bytes plus a missing path, the result is [3, 0, -1]. An absent source fails. An empty source returns an empty sequence. Malformed paths, a failing source enumerator, memory exhaustion and caller cancellation are outside this comparison's input contract and are not silently converted to -1.
C#'s Task.WhenAll, Python's gather and DirectSharp's structured scopes should not be treated as interchangeable exception aggregators. Their failure and cancellation policies need their own explicit contract when child exceptions can escape. Python cancellation of to_thread does not stop the underlying operating-system read; cancellation of the waiting coroutine is distinct from stopping a worker thread.
For large batches, task-per-input creation can consume substantial memory in C#, VB.NET and Python. A concurrency permit limits active reads, not the number of pending task objects in these implementations. DirectSharp's source limit specifies active iteration capacity; it does not promise a particular allocation strategy. Measure the implementation used for production workloads.
Explicit DirectSharp async actions provide the Cancellation context, including Cancellation.Token and Cancellation.ThrowIfCanceled(). Imported .NET methods can receive that standard CancellationToken. C# and VB.NET pass a token parameter explicitly. A Python coroutine normally receives cancellation through its task; libraries may additionally expose their own cancellation API. Cooperation is required in every case, and cancellation does not imply transactional rollback.
Several independent results
A dashboard may instead need one risk score, one credit score and one address score. Given existing async service actions that return Integer scores, DirectSharp can name the outputs directly:
concurrent all
returns risk as Integer, credit as Integer, address as Integer with limit: 2
risk = run LoadRiskAsync(customerId)
credit = run LoadCreditAsync(customerId)
address = run LoadAddressAsync(customerId)
end concurrent
RenderDashboard(risk, credit, address)
The outputs become available after the scope. A sibling producer cannot read risk as a completed value inside the scope. For C# and VB.NET, keep task references and await their joint completion before reading results. Python can gather the corresponding awaitables. DirectSharp applies its structured failure and settlement rules: owned work is joined and failures are propagated according to the completion mode. An equivalent C#, VB.NET or Python implementation must match those rules as well as the successful outputs. The file-size comparison above uses a narrower workload by returning expected I/O failures as data.
concurrent first chooses the first successful producer; concurrent any chooses the first completed producer, including a failed completion. C#'s Task.WhenAny alone does not implement first-success semantics. Neither does Python's first-completion wait. A first-success race needs code that rejects failed completions and keeps waiting for a success. DirectSharp first then requests cooperative cancellation of unfinished losers and joins them; any does not cancel merely because a winner was selected. Task.WhenAny alone supplies neither policy.
Related values from one winner
If a primary service and a backup each load a customer together with its accounts, return one ordinary CustomerBundle object from each competing operation. DirectSharp's v1 first and any publish one result binding. Reading its Customer and Accounts members after the scope keeps the values associated with the selected operation. Racing the customer and accounts separately could combine unrelated snapshots from different backends.
The same design applies in C#, VB.NET and Python: select one operation's result object, then read its members. A C# or VB.NET class, or a Python dataclass, can express that application result. DirectSharp does not require tuple destructuring or a special multi-output branch syntax. all can still publish separate outputs from separate operations, as the independent-score example does.
One result object preserves association. A backend must obtain a consistent snapshot if the application also requires transactional consistency. Ordinary object semantics provide no automatic deep copying, rollback of a losing request or shared-state protection. Cooperative cancellation can leave a completed external effect in place. Joining also means a slow, noncooperative loser can delay scope completion after a winner is selected; the scope's completion includes settling its owned work.
Deterministic resource cleanup
An import audit counts nonempty lines in an ASCII text file. A line containing spaces counts as nonempty; blank lines do not. LF and CRLF line endings are allowed. Opening or reading the file can fail, and that failure propagates. Every version closes the opened reader on normal exit or a read failure.
DirectSharp:
// Dashboard policy: preserve input order, bounded I/O and the stated error result.
action CountNonemptyLines(path as String)
returns Integer
let reader = Global.System.IO.File.OpenText(path)
try
let count as Integer = 0
let line = reader.ReadLine()
while line is not nothing
if line.Length > 0 then
count = count + 1
end if
line = reader.ReadLine()
end while
return count
finally
reader.Dispose()
end try
end action
C#:
// Dashboard policy: preserve input order, bounded I/O and the stated error result.
using System.IO;
public static int CountNonemptyLines(string path)
{
using var reader = File.OpenText(path);
int count = 0;
string? line;
while ((line = reader.ReadLine()) is not null)
{
if (line.Length > 0)
count++;
}
return count;
}
VB.NET:
' Dashboard policy: preserve input order, bounded I/O and the stated error result.
Imports System.IO
Public Function CountNonemptyLines(path As String) As Integer
Using reader As StreamReader = File.OpenText(path)
Dim count As Integer = 0
Dim line As String = reader.ReadLine()
While line IsNot Nothing
If line.Length > 0 Then count += 1
line = reader.ReadLine()
End While
Return count
End Using
End Function
Python:
def count_nonempty_lines(path: str) -> int:
with open(path, "r", encoding="utf-8-sig", newline=None) as reader:
count = 0
for line in reader:
if len(line.rstrip("\r\n")) > 0:
count += 1
return count
DirectSharp shows the cleanup directly: finally calls the reader's ordinary .NET Dispose method after a normal return or a read failure. Acquisition happens before the try, so a failed open does not attempt to dispose an uncreated reader. VB.NET and Python return inside their resource scopes; C#'s using declaration covers the rest of the method. Each closes the reader before the caller receives the result. All four examples use synchronous reader cleanup.
C# and VB.NET use the same .NET libraries directly. DirectSharp follows that model. It does not need a file-reading adapter, validation keyword or special database grammar to call existing APIs.