惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

AI
AI
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
D
Docker
Last Week in AI
Last Week in AI
Apple Machine Learning Research
Apple Machine Learning Research
WordPress大学
WordPress大学
F
Full Disclosure
博客园 - 【当耐特】
博客园 - 司徒正美
V
Visual Studio Blog
F
Fortinet All Blogs
T
Tor Project blog
T
Threatpost
Blog — PlanetScale
Blog — PlanetScale
月光博客
月光博客
C
Cyber Attacks, Cyber Crime and Cyber Security
阮一峰的网络日志
阮一峰的网络日志
GbyAI
GbyAI
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
T
Tenable Blog
M
MIT News - Artificial intelligence
L
Lohrmann on Cybersecurity
P
Palo Alto Networks Blog
I
Intezer
Stack Overflow Blog
Stack Overflow Blog
The Register - Security
The Register - Security
The Last Watchdog
The Last Watchdog
S
Securelist
T
Tailwind CSS Blog
V
Vulnerabilities – Threatpost
U
Unit 42
博客园 - 叶小钗
P
Proofpoint News Feed
C
Cybersecurity and Infrastructure Security Agency CISA
H
Help Net Security
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
酷 壳 – CoolShell
酷 壳 – CoolShell
Hacker News - Newest:
Hacker News - Newest: "LLM"
NISL@THU
NISL@THU
J
Java Code Geeks
H
Hackread – Cybersecurity News, Data Breaches, AI and More
SecWiki News
SecWiki News
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Project Zero
Project Zero
T
The Exploit Database - CXSecurity.com
TaoSecurity Blog
TaoSecurity Blog
A
Arctic Wolf
Martin Fowler
Martin Fowler
T
Threat Research - Cisco Blogs
N
News | PayPal Newsroom

Hacker News

Introducing Claude Opus 4.7 Qwen Studio The Future of Everything is Lies, I Guess: Where Do We Go From Here? GitHub - SeanFDZ/macmind: Single-layer transformer in HyperTalk for the classic Macintosh Show HN: Agent-cache – Multi-tier LLM/tool/session caching for Valkey and Redis Bonsai 1-bit WebGPU - a Hugging Face Space by webml-community Moving a large-scale metrics pipeline from StatsD to OpenTelemetry / Prometheus GitHub - Nightmare-Eclipse/RedSun: The Red Sun vulnerability repository GitHub - SethPyle376/hiraeth: Local AWS emulator focused on fast integration testing, with SQS support, SQLite-backed state, and a debug-friendly web UI. GitHub - macOS26/Agent: Any AI, replaces Claude Code, Cursor, OpenClaw. Over 18 LLM providers (Claude, OpenAI, Gemini, Ollama, Zai, HF, Qwen) wired into a native Mac app that writes code, builds Xcode projects, bumps versions, manages git, automates Safari, use AppleScript, JS or Accessibility, extend Agent! w/ MCP Servers, run tasks from your iPhone via Messages. YouTube now lets you turn off Shorts I Made a Terminal Pager Burgers | マクドナルド公式 Commands — HackerNews CLI documentation ChatGPT for Excel PiCore - Raspberry Pi Port of Tiny Core Linux Live Nation illegally monopolized ticketing market, jury finds Google Broke Its Promise to Me. Now ICE Has My Data. Founding Engineer at Adaptional | Y Combinator CRISPR takes important step toward silencing Down syndrome’s extra chromosome GitHub - saffron-health/libretto: The AI toolkit for building reliable browser automations US v. Heppner (S.D.N.Y. 2026) no attorney-client privilege for AI chats [pdf] Retrofitting JIT Compilers into C Interpreters IPv6 – Google The Accursèd Alphabetical Clock Cybersecurity Looks Like Proof of Work Now Fragments: April 14 Cal.com Goes Closed Source: Why AI Security Is Forcing Our Decision | Cal.com - Scheduling Software for Online Bookings Laravel raised money and now injects ads directly into your agent When moving fast, talking is the first thing to break Too much Discussion of the XOR swap trick – Heather Cafe Introduction to Spherical Harmonics for Graphics Programmers The Grand Line Building a Z-Machine in the worst possible language High-Level Rust: Getting 80% of the Benefits with 20% of the Pain GitHub - duguyue100/midnight-captain: Inspired by Midnight Commander, tailored to my taste. How to build a `git diff` driver · Jamie Tanna | Software Engineer Center for Responsible, Decentralized Intelligence at Berkeley The Local Universe’s Expansion Rate Is Clearer Than Ever, but Still Doesn’t Add Up - A new synthesis of astronomical measurements confirms a persistent mismatch that could point to physics beyond current models The air throughout our homes is infused with microplastics. But there are things you can do to breathe less of them The disturbing white paper Red Hat is trying to erase from the internet – OSnews The Future of Everything is Lies, I Guess: Annoyances ‘Abhorrent’: the inside story of the Polymarket gamblers betting millions on war Productive procrastination — Max van IJsselmuiden maps, territory and LMs 447 Terabytes per Square Centimetre at Zero Retention Energy: Non-Volatile Memory at the Atomic Scale on Fluorographane Show HN: Pardonned.com – A searchable database of US Pardons 20 Years on AWS and Never Not My Job The Seasons are Wrong Artemis II crew splashes down near San Diego after historic moon mission We gave an AI a 3 year retail lease in SF and asked it to make a profit | Andon Labs How a dancer with ALS used brainwaves to perform live On filing the corners off my MacBooks Installing every* Firefox extension OpenClaw’s memory is unreliable, and you don’t know when it will break Steve Blank Nowhere Is Safe Chimpanzees in Uganda locked in vicious 'civil war', say researchers watgo - a WebAssembly Toolkit for Go linux/Documentation/process/coding-assistants.rst at master · torvalds/linux GitHub - callumlocke/json-formatter: Makes JSON easy to read. Founding Product Engineer at Bild AI | Y Combinator A compelling title that is cryptic enough to get you to take action on it GitHub - Keychron/Keychron-Keyboards-Hardware-Design: Industrial design files for Keychron keyboards and mice. 100+ models with CAD assets in STEP, DXF, DWG, and PDF. Source-available, with commercial use allowed for original compatible accessories within the license terms. [ANNOUNCE] WireGuardNT v0.11 and WireGuard for Windows v0.6 Released 1D-Chess Helium Is Hard to Replace Cooperative Vectors Introduction | Evolve Keeping a Postgres queue healthy — PlanetScale Our response to the Axios developer tool compromise Do Americans read print books, e-books or audiobooks more? The Zettelkasten Method in Obsidian: A Practical Setup Guide Artemis II Is Competency Porn and We Are Starving For It WeakC4 Flight Viz — Cockpit View A Mexican surveillance giant you’ve never heard of is now watching the U.S. border Surelock: Deadlock-Free Mutexes for Rust RISC-V 101 – what is it and what does it mean for Canonical? | Ubuntu The Problem That Built an Industry How Much Linear Memory Access Is Enough? | Solidean Investigating Split Locks on x86-64 Simplest hash functions Sybilproof reputation mechanisms (2005) [pdf] What is a property? How Complex is my Code? Static code analysis in Kotlin — tools overview Toffoli gates are all you need PGLite evangelism dcmake: a new CMake debugger UI Clojure on Fennel part one: Persistent Data Structures Fragments: April 2 Python Release Python install manager 26.1 The Life and Death of the Book Review - Liberties Introducing Database Traffic Control — PlanetScale Bitcoin miners are losing $19,000 on every BTC produced as difficulty drops 7.8% God sleeps in the minerals Building slogbox Apple Silicon and Virtual Machines: Beating the 2 VM Limit Who was “Not Even Wrong” first? Pokemon Evolution Vs Darwinian Evolution The APL Programming Language Source Code
What Async Promised and What it Delivered — Causality
2026-04-22 · via Hacker News

OS threads carry overhead: an operating system thread reserves virtual address space for its stack and takes on the order of tens of microseconds to create on modern Linux. (edit Apr 27: The original “roughly a millisecond”, and “megabyte of stack space” conflated virtual reservation with physical memory commitment. Thanks to ibraheemdev, jemfinch, eklitzke on HN) Context switches happen in kernel space and burn CPU cycles, and O(n) readiness polling (select, poll) add up at scale. A server handling thousands of concurrent connections and dedicating one thread per connection means thousands of threads each consuming memory and competing for scheduling. The system spends time managing threads that could be better spent doing useful work.

This is the C10K problem, named by Dan Kegel in 1999. If you were building a web server, a chat system, or anything with a large number of simultaneous connections, you needed a way to handle concurrency without a thread per connection.

The answer came in waves, each solving the previous wave’s worst problem while introducing new ones. Previously we’ve looked at channels in Go and actors in Erlang. Now we turn to async, which is everywhere these days.

Callbacks

The first wave was straightforward: don’t block the thread. Instead of waiting for an i/o operation to complete, register a function to be called when it finishes and move on to the next piece of work. Event loops (select, poll, epoll, kqueue) multiplexed thousands of connections onto a handful of threads, and callbacks were the programmer’s interface to this machinery.

Node.js built an entire ecosystem on this model, handling thousands of concurrent connections on a single thread. Nginx’s event-driven architecture was a major reason it displaced Apache for high-concurrency workloads.

This nicely solved the performance problem, but at a cost: callbacks invert control flow. Instead of writing “do A, then B, then C” as three sequential statements, you write “do A, and when it’s done call this function, which does B, and when that’s done call this other function, which does C.” The programmer’s intent becomes scattered across nested closures. JavaScript developers named this “callback hell” and built an entire website to commiserate.

Callbacks have deeper problems than aesthetics, such as fracturing error handling. Each callback needs its own error path. Errors can’t propagate naturally up the call stack because there is no call stack (callbacks run in a different context from where they are registered). Handling partial failure in a chain of callbacks means threading error state through every function in the chain.

Plus, callbacks have no notion of cancellation. If you start an asynchronous operation and then decide you don’t need the result, there’s no general way to stop it. The callback will fire eventually, and your code needs to handle the case where it no longer cares about the result.

Callbacks solved the resource problem (too many threads) by creating an ergonomics problem (code that’s hard to write, read, and get right).

Promises and Futures

The next wave started with a good idea: what if, instead of passing a callback for later invocation, an asynchronous operation immediately returned an object representing its eventual result?

This is a promise (JavaScript) or future (Java, Rust, etc). The concept dates to Baker and Hewitt in 1977, but it took the C10K pressure of the 2010s to push it into mainstream programming. JavaScript standardized native Promises in ES2015 following the community-driven Promises/A+ spec, and Java 8 introduced CompletableFuture.

Promises are more ergonomic than callbacks. First, promises are composable: promise.then(f).then(g) reads as a pipeline instead of a nested pyramid. Error handling also consolidates: a .catch() at the end of a chain handles failures from any step. And promises are values that you can store, pass around, and return from functions. A first-class handle to an eventual value moves the conversation away from raw threads and toward data dependencies. The idea that “this value depends on a computation that hasn’t finished yet” is a useful thing to be able to express.

Here’s JavaScript reading a user profile and then fetching their recent orders, first with callbacks, then with promises:

// Callbacks: nested, error handling at every level
getUser(userId, (err, user) => {
    if (err) return handleError(err);
    getOrders(user.id, (err, orders) => {
        if (err) return handleError(err);
        render(user, orders);
    });
});

// Promises: chained, error handling consolidated
getUser(userId)
    .then(user => getOrders(user.id).then(orders => [user, orders]))
    .then(([user, orders]) => render(user, orders))
    .catch(handleError);

The promise-based version is not a huge improvement on this small example, but the difference grows with complexity: five steps deep in callbacks is nearly unreadable, while five .then() calls chained together are at least linear.

But promises introduced their own problems:

Promises are one-shot. A promise resolves exactly once. This makes them unsuitable for modeling streams, events, repeated messages, or any ongoing communication. A WebSocket that receives a stream of messages doesn’t map onto “a value that will exist later.” This forces a split: promises for request-response patterns, and something else (event emitters, observables, callbacks again) for everything else.

Composition is clunky. The example above hints at it: getting both user and orders into the final .then() requires nesting or awkward gymnastics with Promise.all. Two independent async operations are easy (Promise.all([a, b])), but anything more complex (conditional branching, loops over async operations, early exit) requires increasingly elaborate combinator patterns. These patterns work but they’re a functional programming idiom grafted onto an imperative language and they don’t feel natural.

Errors vanish silently. JavaScript promises that reject without a .catch() handler originally just swallowed the error. The value was lost causing failures to be invisible. This was bad enough that Node.js eventually changed unhandled rejections from a warning to a process crash, and browsers added unhandledrejection events. A feature designed to improve error handling managed to create an entirely new class of silent failures that didn’t exist with callbacks.

The type split. Every function now returns either a value or a promise of a value. So callers need to know which one they’re getting and libraries need to decide which one to provide. A function that was synchronous becomes asynchronous when you add a database call to it, and now every caller needs to handle a promise instead of a value. This is a mild form of the coloring problem that the next wave would make even worse.

Async/Await

Promise chains still looked nothing like the sequential code developers wrote for everything else. Async/await, pioneered by C# in 2012 and adopted by JavaScript (ES2017), Python (3.5), Rust (1.39), Kotlin, Swift, and Dart, delivered exactly that:

// Promise chains
function loadDashboard(userId) {
    return getUser(userId)
        .then(user => getOrders(user.id)
            .then(orders => [user, orders]))
        .then(([user, orders]) => render(user, orders));
}

// Async/await
async function loadDashboard(userId) {
    const user = await getUser(userId);
    const orders = await getOrders(user.id);
    return render(user, orders);
}

The async/await version reads like sequential code. Variables bind naturally. You can use try/catch instead of .catch(). Loops work with await inside them. It’s an ergonomic win for linear sequences of asynchronous operations.

The industry adopted it fast, with JavaScript frameworks going all-in, Python’s asyncio becoming the standard approach for concurrent i/o, and Rust stabilizing async/await as the path to high-performance networking. Within a few years, async/await was the default way to write concurrent i/o code in most mainstream languages.

Paying the Function Coloring Tax

In 2015, right as async/await was gaining steam, Bob Nystrom published “What Color is Your Function?”, a thought experiment about a language where every function is either “red” or “blue.” Red functions can call blue functions, but blue functions can’t call red functions without special ceremony. Every function must choose a color, and if you call a red function from a blue one, the blue one must become red, spreading virally throughout the codebase.

This was an analogy to async/await: async functions are red, sync functions are blue. An async function can call a sync function without issue, but calling an async function from a sync function requires blocking the thread or restructuring the code. Every function in your program must choose a color, and that choice propagates through every caller.

Nystrom’s post stuck because it put a name to something developers had been experiencing without a vocabulary for it. Function coloring reshapes entire codebases and ecosystems.

The Rust async ecosystem fragmented around competing runtimes (Tokio, async-std, smol) that provide incompatible implementations of fundamental types like TCP streams and timers. A library written for Tokio can’t easily be used with async-std. The popular HTTP client reqwest simply requires Tokio, and if your project uses a different runtime, that’s your problem. Now library authors either pick Tokio (locking out alternatives) or attempt runtime-agnostic abstractions (adding complexity and sometimes performance overhead).

Tokio’s dominance is function coloring at ecosystem scale. The tax shows up at other scales too:

At the function level, adding a single i/o call to a previously synchronous function changes its signature, its return type, and its calling convention. Every caller must be updated, and their callers must be updated. The change ripples through the call graph until it hits a framework entry point or a main function. A one-line database lookup can require modifying dozens of files.

At the library level, authors face a choice of writing a sync library and exclude async users, or writing an async library and force sync users to add runtime dependencies (or maintain both). Many choose “both,” doubling the API surface, the test matrix, and the maintenance burden. In Python, the requests library (sync) and aiohttp (async) are separate projects by separate authors doing the same thing. httpx eventually appeared to offer both interfaces from one package, which is an improvement only needed because of the split.

At the ecosystem level, the Rust example above is the norm, not the exception. Every library that touches i/o must choose a color, and that choice limits which other libraries it can work with. The Rust async book itself notes that “sync and async code also tend to promote different design patterns, which can make it difficult to compose code intended for the different environments.”

And the costs aren’t just logistical: async/await introduced entirely new categories of bugs that threads don’t have. O’Connor documents a class of async Rust deadlocks he calls “futurelocks”: a future acquires a lock, then stops being polled while another future tries to acquire the same lock. With threads, a thread holding a lock always makes progress toward releasing it (unless you do something everyone knows is dangerous, like SuspendThread). With async Rust, the standard tools like select!, buffered streams, and FuturesUnordered routinely stop polling futures that hold resources. The original futurelock at Oxide required core dumps and a disassembler to diagnose.

A Sequential Trap

A subtler cost that gets less attention is that async/await’s greatest strength, making asynchronous code look sequential, is also a cognitive trap.

async function loadDashboard(userId) {
    const user = await getUser(userId);
    const orders = await getOrders(user.id);
    const recommendations = await getRecommendations(user.id);
    return render(user, orders, recommendations);
}

This fetches orders and recommendations sequentially: getRecommendations doesn’t start until getOrders finishes. But these two operations are independent, because recommendations don’t depend on orders. So they could run in parallel, but don’t. The code looks clean and correct while leaving performance on the table.

The parallel version requires the programmer to explicitly break out of sequential style:

async function loadDashboard(userId) {
    const user = await getUser(userId);
    const [orders, recommendations] = await Promise.all([
        getOrders(user.id),
        getRecommendations(user.id)
    ]);
    return render(user, orders, recommendations);
}

The pattern scales poorly beyond small examples. In a real application with dozens of async calls, determining which operations are independent and can be parallelized requires the programmer to manually analyze dependencies and restructure the code accordingly. The sequential syntax actively obscures the dependency structure, i.e. the one piece of information that would tell you what can run in parallel.

Async/await was introduced to make asynchronous code easier to write. It made “what can run concurrently” something the programmer must determine manually and express through combinator patterns that break the sequential flow that was the whole point.

What Async Got Right

To be fair, async abstractions did improve things.

Async/await’s ergonomics for linear sequences are better than callbacks or promise chains. For code that’s inherently sequential but happens to include i/o, async/await removes real syntactic noise. It’s easier to read and debug than callback-based code.

Some language designers chose different paths. For example, Go deliberately chose goroutines, accepting a heavier runtime in exchange for no function coloring at all. (Edit note Apr 24: Go actually introduced a form of coloring through context.Context, which propagates through calls for cancellation. Edit Apr 27: previous language implied Go made the decision in reaction to async/await) Java’s Project Loom (virtual threads in Java 21) also made a different bet: lightweight threads that look and behave like regular threads, so no code needs to change color. The Loom team explicitly cited function coloring as a problem they wanted to avoid.

Zig went further: it removed its compiler-level async/await entirely and rebuilt around an Io interface parameter that i/o operations accept. The runtime (threaded, event-loop, whatever the user supplies) fulfills the interface. Function signatures don’t change based on how they’re scheduled, and async/await become library functions rather than language keywords. Though some argue that the Io parameter itself is a form of coloring.

Language designers who studied the async/await experience in other ecosystems concluded that the costs of function coloring outweigh the benefits and chose different paths.

Accumulating Costs

Each solution solved a problem but introduced new costs. And those costs are structural, affecting the shape of every program, library, and API in the codebase.

WaveSolvedIntroduced
CallbacksThread-per-connection resource exhaustionInverted control flow, fragmented error handling, callback hell
PromisesNesting, error consolidation, values over callbacksOne-shot limitation, silent error swallowing, mild type split
Async/AwaitErgonomics for linear async sequencesFunction coloring, ecosystem fragmentation, new deadlock classes, sequential trap

Each wave made the local experience of writing async code more pleasant while making the global experience more complex. The developer writing a single async function has never had it better, while the team maintaining a large codebase with mixed sync/async code, managing dependency compatibility across runtimes, and trying to find parallelism opportunities hidden behind sequential-looking await chains are carrying a burden that didn’t exist before these abstractions were introduced.

This isn’t a case of bad engineering. The people who designed callbacks, promises, and async/await were solving real problems, and each step was a reasonable response to the previous step’s failures. But fifteen years and several iterations in, the accumulated tax is sizable, and a pattern is visible: each fix treats symptoms while leaving the structure intact.

The callbacks-to-promises-to-async/await arc may be the clearest illustration yet of a theme running through this series: approaches that start by asking “how do we manage concurrent execution?” keep generating new problems at every level of abstraction. You can watch this one play out in real time, across a single ecosystem, within a single decade.

References