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

推荐订阅源

云风的 BLOG
云风的 BLOG
Last Week in AI
Last Week in AI
IT之家
IT之家
H
Hackread – Cybersecurity News, Data Breaches, AI and More
博客园 - 三生石上(FineUI控件)
Microsoft Azure Blog
Microsoft Azure Blog
Recent Announcements
Recent Announcements
The Register - Security
The Register - Security
C
Cyber Attacks, Cyber Crime and Cyber Security
S
SegmentFault 最新的问题
Engineering at Meta
Engineering at Meta
Know Your Adversary
Know Your Adversary
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
WordPress大学
WordPress大学
C
CXSECURITY Database RSS Feed - CXSecurity.com
F
Fox-IT International blog
C
Cybersecurity and Infrastructure Security Agency CISA
P
Privacy & Cybersecurity Law Blog
雷峰网
雷峰网
大猫的无限游戏
大猫的无限游戏
F
Future of Privacy Forum
阮一峰的网络日志
阮一峰的网络日志
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Recorded Future
Recorded Future
P
Proofpoint News Feed
O
OpenAI News
C
CERT Recently Published Vulnerability Notes
E
Exploit-DB.com RSS Feed
Spread Privacy
Spread Privacy
酷 壳 – CoolShell
酷 壳 – CoolShell
人人都是产品经理
人人都是产品经理
罗磊的独立博客
V
V2EX - 技术
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
T
The Blog of Author Tim Ferriss
N
Netflix TechBlog - Medium
AWS News Blog
AWS News Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
爱范儿
爱范儿
李成银的技术随笔
C
Cisco Blogs
SecWiki News
SecWiki News
Application and Cybersecurity Blog
Application and Cybersecurity Blog
L
LINUX DO - 热门话题
B
Blog RSS Feed
Google DeepMind News
Google DeepMind News
G
Google Developers Blog
Latest news
Latest news
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
J
Java Code Geeks

DEV Community

My agent could see the dropdown. It just couldn't pick anything. Why `mixed` Is the Worst Type in Your PHP Codebase (and How to Kill It) PHP 8.4 Asymmetric Visibility: 5 Patterns That Replace Constructors and Setters apt-mark hold doesn't pin versions — how it nearly removed OpenSSH across our fleet Getting Started with AWS — A Beginner Friendly Introduction I Built a Free Metal Weight Calculator — Here's the Math Behind It From Half Baked Repos to GitHub Glory: How I Am Finishing My Ambitious Ten App Masterpiece Aasa: The Phone That Finally Notices Why Fast Development Fails Without Strong Engineering Foundations Journey Builder vs Automation Studio: Which Tool for Which Job Dynamic Content Blocks: One Email, Different Content Per Tier Everyone's Talking About Gemini 3.5 Flash. The Real Story at Google I/O 2026 Was a Skill File. Enhancing the AI Blog System: SQLite Support and Streamlined Publishing Features I Fine-Tuned Gemma 4 on an Emotion Dataset Using a Single GPU Omnichannel inventory in D365: DOM + the Inventory Add-in File-Drop Automations: SFMC Pattern for Daily Imports Regression Testing in Agile: How to Test Without Slowing Down Your Sprints I build projects and manage teams without a single call Making a Calculator UI with HTML5 and CSS3 Full Next.js + Node.js + PostgreSQL Interview Task Setup Google’s Gemini Coding Demos Revealed the Slow Death of “Blank Page Programming” Verification Activity: SFMC Guard Against Empty Files Integrating Shopify with external systems: MVP connection choices Beyond RAG: Architecting Local Long-Context Pipelines with Gemma 4's 31B Dense Model KloudAudit vs AWS Cost Explorer: Why I Stopped Using Cost Explorer for Waste Detection Telegram: API bot access token Gemma 4 at the Edge AasPass: A lightweight, local-first password vault for developers Why Local AI Was the Real Winner of Google I/O 2026 (An Insider’s Take) Laravel Google Drive Filesystem: Unlimited Cloud Storage with Familiar Syntax When not to build an AI agent (and what to ship instead) What a real Sanity CMS development services proposal looks like Why hybrid search is the boring default we keep recommending I kept improving my .NET order pipeline after a CTO left feedback. Here is where it ended up. Why Developers go behind Linux ? Does Front End need HTML, CSS? - Part - 2 From Prompts to Action: What Gemini 3.5 Flash and the Agentic Stack Mean for Developers Does Front End need HTML, CSS? - Part - 1 The real attack surface for AI coding agents is the config file Chai aur SQL — A Beginner's Journey into Databases Find Your Route Source Score: Continuing Exploration of LLM Usage in Automated Workflows Tried using the Claude Platform on AWS Your Node.js Server is Using Just One CPU. Here's How to Fix It. 🚀 Google Antigravity 2.0 Quietly Changes What It Means to Be a Software Engineer Environment variables vs connection references in Power Platform Multi-BU D365 environment: single tenant, multiple LEs AI API Integration Testing Checklist for Multi-Model Apps ORA-00203 오류 원인과 해결 방법 완벽 가이드 Designing a Data Extension in SFMC: The Four Decisions First
PHP 纤程在生产环境中的应用:4个真实案例,展示其优于 curl_multi 和队列的场景
Gabriel Anha · 2026-05-24 · via DEV Community

在 2021 年 11 月,PHP 8.1 中发布了 Fibers。RFC 的内容很小,API 只有四个方法,而且大多数 Laravel 代码库仍然没有使用它们。公开vendor/在一个新的Laravel项目中,你会发现Guzzle、Symfony HttpClient和ReactPHP都在底层使用它们。而你的代码?可能一个也没有。

对于一个CRUD应用来说,这很好。但当你扩展到五个HTTP服务、流式传输多个GB的文件或设置一个webhook调度器时,队列或阻塞的二元选择就会变得昂贵。Fibers解决的是队列过度解决的一类特定问题,curl_multi 未能充分解决。以下是四个它们发挥作用的情况。

纤维实际上是什么(60秒思维模型)

纤维是一个协程。不是线程。不是进程。一个可以在同一PHP进程中暂停自身并稍后恢复的执行栈。

整个API:

$fiber = new Fiber(function (): void {
    echo "step 1\n";
    $resumed = Fiber::suspend("paused");  // hands control back
    echo "step 2 with: $resumed\n";
});

$out = $fiber->start();           // "step 1", returns "paused"
$fiber->resume("hello");          // "step 2 with: hello"
$fiber->isTerminated();           // true

进入全屏模式 退出全屏模式

Fiber::suspend()这是魔法。它只能从正在运行的 Fiber 内部调用。从主流程中调用它,你就会得到Fiber\FiberError: Cannot call Fiber::suspend() outside of a Fiber来电者start()或者resume()恢复你暂停的任何内容。

在继续之前需要牢记两件事:

  1. 纤维是协作的。没有什么能抢占它们的执行。如果一个纤维没有调用suspend(),它运行到完成并阻塞主流程。
  2. ,它们共享同一个进程。没有GIL,没有内存拷贝,没有IPC。全局变量是共享的。PDO连接也是共享的,这是大多数人遇到的第一个坑(更多内容见下文)。

案例一:并行HTTP发散

您的结账端点调用支付网关、欺诈服务、忠诚度API、运费计算器和仓库可用性服务。五个调用,每个约400毫秒,顺序执行。在顺利情况下,这是2秒的时间。

curl_multi_exec() 解决了这个问题,但API是一个20年前的轮询循环,带有do {} whileselect风格的文件描述符监视。它有效。读起来并不令人愉快。

这是光纤版本。模式:每个HTTP调用都存在于自己的Fiber中。一个小型调度器泵送curl_multi并继续执行Fibers,直到它们的句柄完成。

final class ParallelHttp
{
    /** @param array<string, string> $urls keyed by label */
    public static function get(array $urls): array
    {
        $mh = curl_multi_init();
        $handles = [];
        $fibers = [];

        foreach ($urls as $key => $url) {
            $fibers[$key] = new Fiber(function () use ($url, $mh, &$handles, $key) {
                $ch = curl_init($url);
                curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
                curl_setopt($ch, CURLOPT_TIMEOUT, 5);
                curl_multi_add_handle($mh, $ch);
                $handles[$key] = $ch;

                // hand control back to the scheduler until curl says done
                Fiber::suspend();

                $body = curl_multi_getcontent($ch);
                curl_multi_remove_handle($mh, $ch);
                curl_close($ch);
                return $body;
            });
            $fibers[$key]->start();
        }

        // pump until every handle is finished
        do {
            curl_multi_exec($mh, $active);
            curl_multi_select($mh, 0.05);
        } while ($active > 0);

        curl_multi_close($mh);

        $results = [];
        foreach ($fibers as $key => $fiber) {
            $results[$key] = $fiber->resume();
        }
        return $results;
    }
}

$out = ParallelHttp::get([
    'payment'  => 'https://api.example.com/payment/check',
    'fraud'    => 'https://api.example.com/fraud/score',
    'loyalty'  => 'https://api.example.com/loyalty/balance',
    'shipping' => 'https://api.example.com/shipping/quote',
    'stock'    => 'https://api.example.com/warehouse/stock',
]);

进入全屏模式 退出全屏模式

来自针对五个端点的合成基准测试,服务器端人为延迟400ms的数字:顺序cURL耗时约2,050毫秒;Fiber +curl_multi 版本在 ~440 毫秒内完成。这是可以预料的。实际运行时间大约是最慢的调用加上一点调度开销。Guzzle 异步文档 报告了类似的趋势,而 ReactPHP 的 HTTP 客户端基准测试 则处于同一范围内。

Fiber 版本并不比原始版本更快。curl_multi。它底层的网络原语是相同的。变化的是每个Fiber得到一个看起来正常的函数体。没有回调,没有then(),没有Promise链。当错误处理变得复杂时,这一点很重要。try/catch的工作方式完全符合你的预期.

案例二:带背压的流处理

读取一个10GB的CSV文件,使用fopen +fgetcsv 在向无法及时响应的数据库批量写入数据时会正常工作。该脚本会以 PHP 解析的速度读取,而下游消费者要么导致队列内存溢出,要么丢弃数据行。

纤程允许生产者在消费者落后时暂停。这里是一个带有限制性缓冲区的模式:

final class BoundedChannel
{
    /** @var list<array<string, string>> */
    private array $buffer = [];
    private ?Fiber $waitingProducer = null;
    private ?Fiber $waitingConsumer = null;
    private bool $closed = false;

    public function __construct(private readonly int $capacity = 100) {}

    public function send(array $row): void
    {
        while (count($this->buffer) >= $this->capacity) {
            $this->waitingProducer = Fiber::getCurrent();
            Fiber::suspend(); // wait for the consumer to drain
        }
        $this->buffer[] = $row;
        if ($this->waitingConsumer !== null) {
            $c = $this->waitingConsumer;
            $this->waitingConsumer = null;
            $c->resume();
        }
    }

    public function receive(): ?array
    {
        while ($this->buffer === [] && !$this->closed) {
            $this->waitingConsumer = Fiber::getCurrent();
            Fiber::suspend();
        }
        if ($this->buffer === []) return null;
        $row = array_shift($this->buffer);
        if ($this->waitingProducer !== null) {
            $p = $this->waitingProducer;
            $this->waitingProducer = null;
            $p->resume();
        }
        return $row;
    }

    public function close(): void { $this->closed = true; }
}

切换全屏模式 退出全屏模式

一个真实的摄入管道将一个生产 Fiber 和一个消费 Fiber 连接到通道:

$channel = new BoundedChannel(capacity: 500);

$producer = new Fiber(function () use ($channel): void {
    $fp = fopen('orders-2026.csv', 'r');
    $header = fgetcsv($fp);
    while (($row = fgetcsv($fp)) !== false) {
        $channel->send(array_combine($header, $row));
    }
    fclose($fp);
    $channel->close();
});

$consumer = new Fiber(function () use ($channel): void {
    $batch = [];
    while (($row = $channel->receive()) !== null) {
        $batch[] = $row;
        if (count($batch) === 50) {
            OrderImporter::insertBatch($batch); // blocking DB write
            $batch = [];
        }
    }
    if ($batch !== []) OrderImporter::insertBatch($batch);
});

$producer->start();
$consumer->start();

进入全屏模式 退出全屏模式

缓冲区达到500行时,生产者会自动暂停。消费者清空缓冲区后,生产者继续运行。内存使用保持稳定。使用这种模式处理9GB的CSV文件,整个运行过程中驻留内存始终低于100MB,因为缓冲区限制在500行。有趣的对比不是速度(数据库是瓶颈),而是无论fgetcsv读取多快,脚本都不会因内存耗尽而崩溃。

生成器会让人懒惰,但是生成器不能在辅助方法内部产生,否则会污染整个调用链。纤维可以。这才是真正的易用性优势.

案例三:替换生成器协程

在纤维出现之前,人们在PHP中这样处理异步工作:

function fetchUser(int $id): Generator {
    $raw = yield $http->get("/users/$id");        // a Promise
    $orders = yield $http->get("/orders?u=$id");
    return ['user' => $raw, 'orders' => $orders];
}

进入全屏模式 退出全屏模式

你需要一个"协程运行器",它会遍历生成器,等待每个yield的承诺,并将结果推回。这种模式是有效的(Recoil、Amp v2大量使用它,甚至Laravel Octane也有它的变种),但签名: Generator总是不对。函数返回array。类型系统从未与现实相符。

Fibers解决了这个问题。同样的逻辑,真实的返回类型:

public function fetchUser(int $id): UserBundle
{
    $user = $this->client->get("/users/$id");      // suspends internally
    $orders = $this->client->get("/orders?u=$id");
    return new UserBundle($user, $orders);
}

进入全屏模式 退出全屏模式

客户端在等待套接字时暂停当前的纤程。该函数看起来像直线同步代码。PHPStan、Psalm、IDE自动补全:它们都获得了正确的类型。Amp团队在v3版本中围绕纤程重写了所有内容(发布说明),正好就是这个原因。ReactPHP发布了react/async 4.x 做同样的:await() 是一个基于光纤的原始对象,它允许一个返回 Promise 的库看起来是同步的。

如果你有一个基于 Generator 的 2019 年的 "async" 辅助函数的代码库,通常移除它们以使用光纤会减少 30-40% 的文件内容,纯粹是仪式性的。行为没有变化。

案例 4:在 ReactPHP / AMPHP 里面,而不转换整个代码库

你不必完全投入事件循环才能使用 Fibers。这是大多数“我应该使用 Amp 吗?”的帖子所遗漏的点。

一个需要分发给三个 Webhooks 的 Symfony Messenger 处理器可以本地拉入一个 Fiber 感知的 HTTP 客户端,在处理器内部运行一个小await,并保持其他方面不变:

use Amp\Http\Client\HttpClientBuilder;
use Amp\Http\Client\Request;
use function Amp\async;
use function Amp\Future\awaitAll;

final class DispatchWebhooksHandler
{
    public function __invoke(OrderPlaced $event): void
    {
        $client = HttpClientBuilder::buildDefault();
        $urls = WebhookSubscriptions::for($event->orderId);

        $futures = array_map(
            fn(string $url) => async(fn() =>
                $client->request(new Request($url, 'POST', json_encode($event)))
            ),
            $urls,
        );

        [$errors, $responses] = awaitAll($futures);
        foreach ($errors as $url => $err) {
            FailedWebhook::record($url, $err->getMessage());
        }
    }
}

进入全屏模式 退出全屏模式

async()会生成一个 Fiber。awaitAll() 会阻塞处理器,直到所有 Fiber 完成执行,但仅限于这个处理器。你的 Symfony 应用的其余部分会继续执行它正在做的事情。没有全局运行的事件循环。没有“将整个框架转换为异步的”。

关键在于 Amp 的 EventLoop 在第一次使用时才懒加载初始化,并且在没有待处理任务时能够干净地关闭。同样的情况也适用于react/async。你可以在一个方法中获得协程的易用性,而不会感染整个代码库。

Fibers无法提供帮助的地方

PDO正在阻塞。将PDO::query()放在Fiber内部并不能使其异步。Fiber会阻塞整个进程,直到libpq返回。默认模式下的mysqli也是一样。本地文件上的file_get_contents()也是一样。sleep()也是一样。

CPU密集型工作(图像缩放、对200MB的JSON结构进行编码、bcrypt)从纤程中得不到任何好处。它们是为I/O等待设计的。如果瓶颈是一个紧密的for循环进行哈希操作,你需要使用pthread、并行处理或pcntl_fork,而不是协程.

心理检查:在Node.js中,这个操作会返回一个Promise吗libuv的调用?如果是,一个基于Fiber的包装器会很有帮助。如果不是(你的瓶颈是PHP本身),Fibers将无效。

主要注意事项:存在异步PDO替代品(amphp/postgresamphp/mysql)。)。值得知道它们的存在。值得知道替换它们很少是无成本的。Eloquent 和 Doctrine 都假定阻塞驱动器。

错误处理的陷阱

Fibers 中的异常表现几乎和你预期的差不多。差不多。

$fiber = new Fiber(function (): void {
    Fiber::suspend();
    throw new RuntimeException("boom");
});

$fiber->start();
try {
    $fiber->resume(); // exception propagates HERE, not where it was thrown
} catch (RuntimeException $e) {
    // caught
}

进入全屏模式 退出全屏模式

异常会传递到调用者resume() 那通常没问题。陷阱在于 Fiber::throw():

$fiber = new Fiber(function (): void {
    try {
        Fiber::suspend();
    } catch (DomainException $e) {
        // the throw() call lands inside the suspend
    }
});
$fiber->start();
$fiber->throw(new DomainException("cancelled"));

Enter fullscreen mode Exit fullscreen mode

throw()suspend() 点注入异常。如果 Fiber 没有捕获它,异常会冒泡到调用者。throw()。人们用它来取消操作,但很容易设置一个跨越三个 Fiber 的例外,从而失去跟踪实际捕获它的人。

经验法则:保持 Fiber 身体简短。如果一个 Fiber 在其入口点周围的错误处理比 try/catch 更复杂,你可能想要跨 suspend() 返回一个 Result 风格的对象,而不是通过它抛出。

这对你的代码库意味着什么

Fibers是一种基础组件,而不是你可以随意附加的功能。应该使用它们的地方:并行I/O分支、具有背压感知的流、替换基于生成器的异步辅助工具,以及在通常阻塞的框架内部嵌入使用。不应该使用它们的地方:CPU工作、PDO,以及你没有明确的挂起点描述的任何地方。

如果你已经部署了一个Laravel或Symfony应用,最低风险的第一步是案例4:一个单独的处理程序,通过Amp或react/async生成几个Fibers,在本地运行,然后退出。没有全局事件循环。没有基础设施变更。衡量收益,然后扩展。

你当前应用中最慢的分支是什么,案例4能否在不重写框架的情况下适应它?


如果这有帮助

纤维是运行时原语。更大的问题是架构上的:这个发散应该存在于你的领域层还是适配器中?这就是大多数代码库变得纠缠的地方。解耦PHP 是当框架默认功能不再足够时,你的代码库会去调用的架构层;它涵盖了如何在边缘保持异步 I/O,同时让你的领域代码保持愉悦的同步状态.

Decoupled PHP — Clean and Hexagonal Architecture for Applications That Outlive the Framework

可在 Kindle、Paperback 和 Hardcover 版本上购买。英语、德语和日语版本现已上市——葡萄牙语和西班牙语版本即将推出。