2025-06-10

一、背景与挑战

在高并发场景下(如电商秒杀、社交动态流、API 网关),PHP 应用面临以下主要挑战:

  1. 阻塞等待带宽与资源浪费

    • 传统 PHP 是同步阻塞模式:发起一次远程接口调用或数据库查询,需要等待 I/O 完成后才能继续下一个操作。
    • 若同时有上千个请求进入,数百个慢接口轮询会导致大量进程或协程处于“睡眠等待”状态,CPU 资源无法被充分利用。
  2. 并发任务数量失控导致资源耗尽

    • 如果不对并发并行任务数量加以限制,瞬时并发过多会导致内存、文件描述符、数据库连接池耗尽,从而引发请求失败或服务崩溃。
    • 必须在吞吐与资源可承受之间找到平衡,并对“并发度”进行动态或静态约束。
  3. 传统锁与阻塞带来性能瓶颈

    • 在并发写共享资源(如缓存、日志、文件)时,若使用简单的互斥锁(flock()Mutex),会导致大量进程/协程等待锁释放,降低吞吐。
    • 异步非阻塞模型可以通过队列化或原子操作等方式减少锁竞争开销。

为应对上述挑战,本文将从 PHP 异步处理并发控制 两个维度展开,重点借助 Swoole 协程(也兼顾 ReactPHP/Amp 等方案)示例,展示如何在高并发场景下:

  • 非阻塞地执行网络/数据库 I/O
  • 有效控制并发数量,避免资源耗尽
  • 构建任务队列与限流策略
  • 处理并发写冲突与锁优化

二、PHP 异步基础:从阻塞到非阻塞

2.1 同步阻塞模式

在传统 PHP 脚本中,读取远程接口或数据库都会阻塞当前进程或线程,示例代码:

<?php
function fetchData(string $url): string {
    // 这是阻塞 I/O,同一时刻只能执行一条请求
    $response = file_get_contents($url);
    return $response ?: '';
}

// 串行发起多个请求
$urls = [
    'http://api.example.com/user/1',
    'http://api.example.com/user/2',
    'http://api.example.com/user/3',
];

$results = [];
$start = microtime(true);
foreach ($urls as $url) {
    $results[] = fetchData($url);
}
$end = microtime(true);
echo "同步完成,用时: " . round($end - $start, 3) . " 秒\n";
  • 若每个 fetchData() 需要 1 秒,3 个请求依次执行耗时约 3 秒。
  • 并发量一旦增大,阻塞等待会累加,导致吞吐急剧下降。

2.2 非阻塞/异步模型

异步 I/O 可以让单个进程在等待网络或磁盘操作时“挂起”该操作,并切换到下一任务,完成后再回来“续写”回调逻辑,实现“并发”效果。常见 PHP 异步方案包括:

  1. Swoole 协程:借助底层 epoll/kqueue,将 I/O 操作切换为协程挂起,不阻塞进程。
  2. ReactPHP / Amp:基于事件循环(Event Loop),使用回调或 yield 关键字实现异步非阻塞。
  3. Parallel / pthreads:多线程模型,将每个任务交给独立线程执行,本质上是并行而非真正“异步”。

下文将重点以 Swoole 协程 为主,兼顾 ReactPHP 思路,并展示如何借助这些模型让代码从“线性阻塞”变为“并发异步”。


三、方案一:Swoole 协程下的异步处理

3.1 Swoole 协程简介

  • 协程(Coroutine):一种“用户态”轻量线程,具有非常快速的上下文切换。
  • 当协程执行到阻塞 I/O(如 HTTP 请求、MySQL 查询、Redis 操作)时,会自动将该协程挂起,让出 CPU 给其他协程。I/O 完成后再恢复。
  • Swoole 通过底层 hook 系统函数,将传统阻塞函数转换为可挂起的异步调用。

只需在脚本中调用 Swoole\Coroutine\run() 创建协程容器,之后在任意位置使用 go(function(){…}) 即可开启协程。

3.2 示例:并发发起多 HTTP 请求

<?php
use Swoole\Coroutine\Http\Client;
use Swoole\Coroutine;

// 并发请求列表
$urls = [
    'http://httpbin.org/delay/1',
    'http://httpbin.org/delay/2',
    'http://httpbin.org/delay/3',
    'http://httpbin.org/get?param=4',
    'http://httpbin.org/uuid'
];

Co\run(function() use ($urls) {
    $responses = [];
    $wg = new Swoole\Coroutine\WaitGroup();

    foreach ($urls as $idx => $url) {
        $wg->add();
        go(function() use ($idx, $url, &$responses, $wg) {
            $parts = parse_url($url);
            $host = $parts['host'];
            $port = $parts['scheme'] === 'https' ? 443 : 80;
            $path = $parts['path'] . (isset($parts['query']) ? '?' . $parts['query'] : '');

            $cli = new Client($host, $port, $parts['scheme'] === 'https');
            $cli->set(['timeout' => 5]);
            $cli->get($path);
            $responses[$idx] = [
                'status' => $cli->statusCode,
                'body'   => substr($cli->body, 0, 100) . '…'
            ];
            $cli->close();
            echo "[协程 {$idx}] 请求 {$url} 完成,状态码={$responses[$idx]['status']}\n";
            $wg->done();
        });
    }

    $wg->wait();
    echo "[主协程] 所有请求已完成,共 " . count($responses) . " 条。\n";
    print_r($responses);
});

ASCII 流程图

┌─────────────────────────────────────────────────────────────────┐
│                   主协程 (Coroutine)                            │
│           (Co\run 内部作为主调度)                                │
└─────────────────────────────────────────────────────────────────┘
        │             │             │             │             │
        │             │             │             │             │
        ▼             ▼             ▼             ▼             ▼
    ┌───────┐      ┌───────┐      ┌───────┐      ┌───────┐      ┌───────┐
    │协程 0 │      │协程 1 │      │协程 2 │      │协程 3 │      │协程 4 │
    │发起 GET…│     │发起 GET…│     │发起 GET…│     │发起 GET…│     │发起 GET…│
    └───┬───┘      └───┬───┘      └───┬───┘      └───┬───┘      └───┬───┘
        │             │             │             │             │
        │             │             │             │             │
      I/O 阻塞        I/O 阻塞       I/O 阻塞       I/O 阻塞       I/O 阻塞
        │             │             │             │             │
   [挂起协程 0]  [挂起协程 1]  [挂起协程 2]  [挂起协程 3]  [挂起协程 4]
        ↓             ↓             ↓             ↓             ↓
  Swoole 底层 挂起 I/O 等待异步事件完成
        ↓             ↓             ↓             ↓             ↓
   I/O 完成         I/O 完成        I/O 完成        I/O 完成        I/O 完成
        │             │             │             │             │
  恢复协程 0       恢复协程 1    恢复协程 2    恢复协程 3    恢复协程 4
        │             │             │             │             │
     处理响应        处理响应       处理响应       处理响应       处理响应
        │             │             │             │             │
    $wg->done()     $wg->done()   $wg->done()   $wg->done()   $wg->done()
        └─────────────────────────────────────────┘
                           ↓
             主协程 调用 $wg->wait() 解除阻塞,继续执行
                           ↓
             输出所有响应并退出脚本

3.3 并发控制:限制协程数量

在高并发场景中,如果一次性开启上千个协程,可能出现以下风险:

  • 突发大量并发 I/O,造成网络带宽瞬间拥堵
  • PHP 进程内存分配不够,一次性分配大量协程栈空间导致 OOM

限制协程并发数示例

<?php
use Swoole\Coroutine;
use Swoole\Coroutine\Channel;

$urls = []; // 假设有上千个 URL 列表
for ($i = 0; $i < 1000; $i++) {
    $urls[] = "http://httpbin.org/delay/" . rand(1, 3);
}

// 最大并发协程数
$maxConcurrency = 50;

// 使用 Channel 作为“令牌桶”或“协程池”
Co\run(function() use ($urls, $maxConcurrency) {
    $sem = new Channel($maxConcurrency);

    // 初始化令牌桶:放入 $maxConcurrency 个令牌
    for ($i = 0; $i < $maxConcurrency; $i++) {
        $sem->push(1);
    }

    $wg = new Swoole\Coroutine\WaitGroup();

    foreach ($urls as $idx => $url) {
        // 从令牌桶取出一个令牌;若为空则挂起等待
        $sem->pop();

        $wg->add();
        go(function() use ($idx, $url, $sem, $wg) {
            echo "[协程 {$idx}] 开始请求 {$url}\n";
            $parts = parse_url($url);
            $cli = new \Swoole\Coroutine\Http\Client($parts['host'], 80);
            $cli->get($parts['path']);
            $cli->close();
            echo "[协程 {$idx}] 完成请求\n";

            // 任务完成后归还令牌,让下一个协程能够启动
            $sem->push(1);

            $wg->done();
        });
    }

    $wg->wait();
    echo "[主协程] 所有请求已完成。\n";
});

原理与说明

  1. Channel 令牌桶

    • 创建一个容量为 $maxConcurrency 的 Channel,并预先 push() 同样数量的“令牌”(任意占位符)。
    • 每次要启动新协程前,先 pop() 一个令牌;如果 Channel 为空,则意味着当前已有 $maxConcurrency 个协程在运行,新的协程会被挂起等待令牌。
    • 协程执行完毕后 push() 回一个令牌,让后续被挂起的协程继续运行。
  2. 并发控制

    • 该方案等效于“协程池(Coroutine Pool)”,始终只维持最多 $maxConcurrency 个协程并发执行。
    • 避免瞬时并发过大导致 PHP 内存或系统资源耗尽。
  3. ASCII 图解:并发限制流程
┌─────────────────────────────────────────────────────────┐
│                     主协程 (Coroutine)                  │
└─────────────────────────────────────────────────────────┘
         │            │            │            │
    pop  │            │            │            │
─────────┼────────────┼────────────┼────────────┤
         ▼ (取令牌)    ▼ (取令牌)   ▼ (取令牌)   ▼  (取令牌)
     ┌───────────┐  ┌───────────┐  ┌───────────┐  ┌───────────┐
     │ 协程 1    │  │ 协程 2    │  │ 协程 3    │  │ 协程 4    │
     │ 执行请求  │  │ 执行请求  │  │ 执行请求  │  │ 执行请求  │
     └────┬──────┘  └────┬──────┘  └────┬──────┘  └────┬──────┘
          │              │              │              │
        完成           完成           完成           完成
          │              │              │              │
        push           push           push           push
    (归还令牌)      (归还令牌)      (归还令牌)      (归还令牌)
          └──────────────┴──────────────┴──────────────┘
                       ↓
             下一个协程获取到令牌,继续启动

四、方案二:ReactPHP / Amp 异步事件循环

除了 Swoole,常见的 PHP 异步框架还有 ReactPHPAmp。它们并不依赖扩展,而是基于事件循环(Event Loop) + 回调/Promise模式实现异步:

  • ReactPHP:Node.js 式的事件循环,提供 react/httpreact/mysqlreact/redis 等组件。
  • Amp:基于 yield / await 的协程式语法糖,更接近同步写法,底层也是事件循环。

下面以 ReactPHP 为例,展示如何发起并发 HTTP 请求并控制并发量。

4.1 安装 ReactPHP

composer require react/event-loop react/http react/http-client react/promise react/promise-stream

4.2 并发请求示例(ReactPHP)

<?php
require 'vendor/autoload.php';

use React\EventLoop\Loop;
use React\Http\Browser;
use React\Promise\PromiseInterface;

// 要并发请求的 URL 列表
$urls = [
    'http://httpbin.org/delay/1',
    'http://httpbin.org/delay/2',
    'http://httpbin.org/get?foo=bar',
    'http://httpbin.org/status/200',
    'http://httpbin.org/uuid'
];

// 并发限制
$maxConcurrency = 3;
$inFlight = 0;
$queue = new SplQueue();

// 存放结果
$results = [];

// 创建 HTTP 客户端
$client = new Browser(Loop::get());

// 将 URL 推入队列
foreach ($urls as $idx => $url) {
    $queue->enqueue([$idx, $url]);
}

function processNext() {
    global $queue, $inFlight, $maxConcurrency, $client, $results;

    while ($inFlight < $maxConcurrency && !$queue->isEmpty()) {
        list($idx, $url) = $queue->dequeue();
        $inFlight++;
        /** @var PromiseInterface $promise */
        $promise = $client->get($url);
        $promise->then(
            function (\Psr\Http\Message\ResponseInterface $response) use ($idx, $url) {
                global $inFlight, $results;
                $results[$idx] = [
                    'url'    => $url,
                    'status' => $response->getStatusCode(),
                    'body'   => substr((string)$response->getBody(), 0, 100) . '…'
                ];
                echo "[主循环] 请求 {$url} 完成,状态码=". $response->getStatusCode() . "\n";
                $inFlight--;
                processNext(); // 继续处理下一批任务
            },
            function (Exception $e) use ($idx, $url) {
                global $inFlight, $results;
                $results[$idx] = [
                    'url'   => $url,
                    'error' => $e->getMessage()
                ];
                echo "[主循环] 请求 {$url} 失败: " . $e->getMessage() . "\n";
                $inFlight--;
                processNext();
            }
        );
    }

    // 当队列空且 inFlight=0 时可以结束循环
    if ($queue->isEmpty() && $inFlight === 0) {
        // 打印所有结果
        echo "[主循环] 所有请求完成,共 " . count($results) . " 条\n";
        print_r($results);
        Loop::stop();
    }
}

// 启动处理
processNext();
Loop::run();

ASCII 流程图

┌───────────────────────────────────────────────────────────┐
│                   ReactPHP 事件循环                        │
└───────────────────────────────────────────────────────────┘
      │            │            │            │            │
      │            │            │            │            │
      ▼            ▼            ▼            ▼            ▼
[HTTP get]    [HTTP get]    [HTTP get]    [队列等待]    [队列等待]
  (url1)        (url2)        (url3)        (url4)        (url5)
      │            │            │
      │ inFlight=3  │ (并发达到 max=3) 等待   等待
      ▼            ▼            ▼
 I/O await      I/O await      I/O await 
   (挂起)         (挂起)         (挂起)
      │            │            │
HTTP 响应1     HTTP 响应2     HTTP 响应3
      │            │            │
 inFlight--     inFlight--     inFlight--
      └┬──────┐     └┬──────┐     └┬──────┐
       │      │      │      │      │      │
       ▼      ▼      ▼      ▼      ▼      ▼
  processNext  processNext  processNext   ...
  检查队列 &   检查队列 &   检查队列 &
  并发数<3      并发数<3      并发数<3
       ↓      ↓      ↓ 
 发起 next HTTP 请求  … 

五、并发控制与资源管理

无论异步模型如何,在高并发场景下,必须对并发度进行有效管理,否则可能出现:

  • 内存耗尽:过多协程/进程同时运行,导致内存飙升。
  • 连接池耗尽:如 MySQL/Redis 连接池不足,导致请求被拒绝。
  • 下游接口限制:第三方 API 有 QPS 限制,过高并发会被封禁。

常见并发控制手段包括:

  1. 令牌桶/信号量:通过 Channel、Semaphore 等机制限制并发量。
  2. 任务队列/进程池/协程池:预先创建固定数量的“工作单元”,并从队列中取任务执行。
  3. 速率限制(Rate Limiting):使用 Leaky Bucket、Token Bucket 或滑动窗口算法限速。
  4. 超时与重试策略:对超时的异步任务及时取消或重试,避免僵死协程/进程。

下面以 Swoole 协程为例,介绍信号量限速两种并发控制方式。


5.1 信号量(Semaphore)并发控制

Swoole 协程提供了 Swoole\Coroutine\Semaphore 类,可用于限制并发访问某段代码。

示例:并发查询多个数据库并限制并发数

<?php
use Swoole\Coroutine;
use Swoole\Coroutine\MySQL;
use Swoole\Coroutine\Semaphore;

// 假设有若干用户 ID,需要并发查询用户详细信息
$userIds = range(1, 100);

// 最大并发协程数
$maxConcurrency = 10;

// 创建信号量
$sem = new Semaphore($maxConcurrency);

Co\run(function() use ($userIds, $sem) {
    $results = [];
    $wg = new Swoole\Coroutine\WaitGroup();

    foreach ($userIds as $id) {
        // 从信号量中获取一个票,若已达上限,挂起等待
        $sem->wait();

        $wg->add();
        go(function() use ($id, $sem, &$results, $wg) {
            // 连接数据库
            $db = new MySQL();
            $db->connect([
                'host'     => '127.0.0.1',
                'port'     => 3306,
                'user'     => 'root',
                'password' => '',
                'database' => 'test',
            ]);
            $res = $db->query("SELECT * FROM users WHERE id = {$id}");
            $results[$id] = $res;
            $db->close();
            echo "[协程] 查询用户 {$id} 完成\n";

            // 释放一个信号量票
            $sem->release();
            $wg->done();
        });
    }

    $wg->wait();
    echo "[主协程] 所有用户查询完成,共 " . count($results) . " 条数据\n";
    // 处理 $results
});

原理与说明

  1. new Semaphore($maxConcurrency):创建一个最大并发数为 $maxConcurrency 的信号量。
  2. $sem->wait():用于“申请”一个资源票(P 操作);若当前已有 $maxConcurrency 条协程已持有票,则其他协程会被挂起等待。
  3. $sem->release():释放一个资源票(V 操作),如果有协程在等待,会唤醒其中一个。
  4. 结合 WaitGroup,保证所有查询完成后再继续后续逻辑。

5.2 速率限制(限速)示例

在高并发场景,有时需要对同一个下游接口或资源进行限速,避免瞬时并发过多触发封禁。常用算法有 令牌桶(Token Bucket)漏桶(Leaky Bucket)滑动窗口。本文以“令牌桶”算法为例,在协程中简单实现 API QPS 限制。

示例:令牌桶限速

<?php
use Swoole\Coroutine;
use Swoole\Coroutine\Channel;

// 目标 QPS(每秒最多 5 次请求)
$qps = 5;

// 创建一个容量为 $qps 令牌桶 Channel
$bucket = new Channel($qps);

// 持续向桶中投放令牌
go(function() use ($qps, $bucket) {
    while (true) {
        // 如果桶未满,则放入一个令牌
        if (!$bucket->isFull()) {
            $bucket->push(1);
        }
        // 每隔 1/$qps 秒产生一个令牌
        Coroutine::sleep(1 / $qps);
    }
});

// 需要并发发起的总任务数
$totalTasks = 20;

// 等待组
$wg = new Swoole\Coroutine\WaitGroup();

for ($i = 1; $i <= $totalTasks; $i++) {
    $wg->add();
    go(function() use ($i, $bucket, $wg) {
        // 从桶中取出一个令牌,若桶空则等待
        $bucket->pop();
        // 令牌取到后即可发起请求
        echo "[协程 {$i}] 获取令牌,开始请求 API 时间:" . date('H:i:s') . "\n";
        Coroutine::sleep(0.1); // 模拟 API 请求耗时 100ms
        echo "[协程 {$i}] 请求完成 时间:" . date('H:i:s') . "\n";
        $wg->done();
    });
}

$wg->wait();
echo "[主协程] 所有任务完成。\n";

ASCII 图解:令牌桶限速

  (桶满 5 个令牌后,多余的生产操作会 skip)
┌─────────────────────────────────────────────────────────────┐
│                     令牌桶(Channel)                        │
│               capacity = 5 (Max Token)                      │
│   ┌───┬───┬───┬───┬───┐                                      │
│   │ 1 │ 1 │ 1 │ 1 │ 1 │  <- 初始填满 5 个                      │
│   └───┴───┴───┴───┴───┘                                      │
└─────────────────────────────────────────────────────────────┘
      ↑                 ↑                 ↑
      │                 │                 │
[协程 1 pop]        [协程 2 pop]       [协程 3 pop]
      │                 │                 │
 发起请求            发起请求         发起请求
 (now bucket has 2 tokens)    (1 token)      (0 token)
      │                 │                 │
 多余 Pop 时协程会被挂起          │
      └───────────────┬─────────────┘
                      │
             令牌生产协程每 0.2 秒推 1 令牌
                      │
     ┌────────────────┼────────────────┐
     │                │                │
   T+0.2s          T+0.4s           T+0.6s
  bucket:1         bucket:2         bucket:3
     │                │                │
 [协程 4 pop]     [协程 5 pop]     [协程 6 pop]
  发起请求           发起请求           发起请求
  • 桶初始放满 5 个令牌,因此前 5 个协程几乎可瞬时拿到令牌并发起请求。
  • 之后只有当令牌按 1/$qps 秒速率补充时,新的协程才能从桶中拿到令牌并发起请求,从而平滑控制请求 QPS。

六、并发写冲突与锁优化

在高并发写共享资源(如缓存、日志、队列)时,必须避免过度的锁竞争,否则会变成串行模式,扼杀并发增益。

6.1 缓存原子更新示例

假设要对 Redis 或 APCu 中的计数器执行自增操作,传统方式可能是:

<?php
// 非原子操作示例:读-改-写
$count = apcu_fetch('page_view') ?: 0;
$count++;
apcu_store('page_view', $count);
  • 当并发高时,两个进程可能都 fetch=100,然后同时写入 101,导致计数丢失。

原子操作示例

<?php
// 使用 APCu 内置原子自增函数
$newCount = apcu_inc('page_view', 1, $success);
if (!$success) {
    // 如果键不存在,则先写入 1
    apcu_store('page_view', 1);
    $newCount = 1;
}
  • apcu_inc 是原子操作,内部会做加锁,确保并发自增结果准确无误。

6.2 文件锁与异步队列

如果需要对同一个文件或日志进行并发写入,可以将日志写入“异步队列”(如 Channel 或消息队列),然后在单独的写日志协程/进程中顺序消费,避免并发锁:

示例:协程队列写日志

<?php
use Swoole\Coroutine;
use Swoole\Coroutine\Channel;

Co\run(function() {
    // 日志队列 Channel(容量1000)
    $logQueue = new Channel(1000);

    // 日志写入协程(单独一个),顺序消费
    go(function() use ($logQueue) {
        $fp = fopen(__DIR__ . '/app.log', 'a');
        while (true) {
            $entry = $logQueue->pop(); // 阻塞等待日志
            if ($entry === false) {
                // Channel 关闭
                break;
            }
            fwrite($fp, $entry . "\n");
        }
        fclose($fp);
    });

    // 模拟多个业务协程并发写日志
    for ($i = 1; $i <= 50; $i++) {
        go(function() use ($i, $logQueue) {
            $msg = "[协程 {$i}] 这是一条日志,时间:" . date('H:i:s');
            $logQueue->push($msg);
        });
    }

    // 等待一定时间后关闭日志队列
    Coroutine::sleep(1);
    $logQueue->close(); // 关闭 Channel,让日志写入协程退出
});
  • 原理:所有协程都将日志数据 push 到共享队列,单独的日志写协程依次 pop 并写入文件,避免多协程同时 fopen/fwrite 竞争。
  • 该模式也可用于“任务队列消费”、“图片处理队列”等高并发写场景。

七、总结与最佳实践

在高并发场景下,PHP 应用要想获得优异性能,需要结合业务场景与技术选型,合理利用异步与并发控制。本文从以下几个方面给出了详尽示例与说明:

  1. 阻塞 vs 非阻塞

    • 传统同步阻塞模型容易导致请求累加等待,吞吐下降。
    • 通过 Swoole 协程、ReactPHP、Amp 等框架可实现异步非阻塞,提升 I/O 并发度。
  2. Swoole 协程示例

    • 并发发 HTTP 请求:利用 go() + WaitGroup 实现简单并发调用。
    • 并发控制:借助 ChannelSemaphore 实现令牌桶或协程池,限制同时运行的协程数量,保护系统资源。
  3. ReactPHP 事件循环示例

    • 使用事件循环与 Promise 模式对大批量请求进行异步并发,并通过手动队列管理控制并发度。
  4. 并发写冲突与异步队列

    • 对共享资源(如文件、日志、缓存)并发写时,应尽量使用原子操作或将写操作集中到单独的协程/进程中顺序执行,避免锁竞争。
  5. 速率限制(Rate Limiting)

    • 通过令牌桶算法简单实现 QPS 控制,确保下游接口调用不会被超载或封禁。
  6. 常见 Pitfall 与注意事项

    • PCNTLParallelSwoole 各有使用场景与系统依赖,不同场合下需要灵活选型。
    • 异步代码中要避免使用阻塞 I/O,否则整个协程/事件循环会被挂起。
    • 必须对“并发度”进行限制,避免系统瞬间创建过多协程/进程导致资源耗尽。
    • 在协程环境下,原生函数会被 hook,确保使用 Swoole 协程安全的客户端(如 Swoole\Coroutine\MySQLSwoole\Coroutine\Http\Client 等)。

最佳实践总结

  1. 如果项目仅需并发简单任务(比如几百个独立操作),可优先选择 Swoole 协程,开发成本低、性能佳;
  2. 如果需要兼容更底层的 PHP 版本,或只需在 CLI 环境下快速多进程,可选择 PCNTL
  3. 若需要在纯 PHP 生态(无扩展)下实现异步,且对回调/Promise 接受度高,可使用 ReactPHPAmp
2025-06-10

PHP 并发处理的三种常见且高效的并发处理手段:多进程(PCNTL)多线程/多任务(Parallel 扩展)协程/异步(Swoole)。每一部分都有完整的代码示例、ASCII 流程图和深入的原理说明,帮助你快速掌握 PHP 在不同场景下的并发实现方法。


一、并发处理的必要性与常见场景

在 Web 应用或脚本中,我们常会遇到如下需要并发处理的场景:

  1. 并行发起多个网络请求(如抓取多个第三方接口数据,批量爬虫)
  2. 执行大量 I/O 密集型任务(如大批量文件读写、图像处理、数据库导入导出)
  3. 后台任务队列消费(如将若干任务交给多进程或多线程并行处理,提高吞吐)
  4. 长连接或异步任务(如 WebSocket、消息订阅、实时推送)

如果依赖传统的“串行”方式,一个一个地依次执行,就会导致等待时间累加响应速度下降CPU/IO 资源无法充分利用。通过并发(并行或异步)处理,可以显著提升脚本整体吞吐,并降低单次操作的总耗时。

在 PHP 领域,常见的三种并发思路是:

  1. 多进程(Process):通过 pcntl_fork() 创建子进程,各自独立执行任务,适合计算与 I/O 混合型场景。
  2. 多线程/多任务(Thread/Task):使用 parallelpthreads 扩展,在同一进程内启动多个执行环境,适合轻量计算与共享内存场景。
  3. 协程/异步(Coroutine/Async):以 Swoole 为代表,通过协程或事件循环驱动单进程并发,极大降低上下文切换开销,适合大并发 I/O 场景。

下面我们依次详细介绍这三种并发手段,给出代码示例、ASCII 图解与性能要点。


二、方案一:多进程 —— 使用 PCNTL 扩展

2.1 基本原理

  • 概念:在 Unix-like 系统(Linux、macOS)中,进程(Process)是操作系统分配资源的基本单位。通过调用 pcntl_fork(),父进程会复制出一个子进程,两者从 fork 点开始各自独立运行。
  • 优势

    1. 资源隔离:父子进程各自拥有独立的内存空间,互不干扰,适合运行耗时耗内存的任务。
    2. 稳定可靠:某个子进程 crash 不会直接影响父进程或其他子进程。
    3. 利用多核:在多核 CPU 上,多个进程可并行调度,提高计算与 I/O 并行度。
  • 劣势

    1. 内存开销大:每个子进程都会复制父进程的内存页,fork 时会产生写时复制(Copy-on-Write)。
    2. 上下文切换成本:系统调度多进程会带来一定开销,频繁 fork/exit 会影响效率。
    3. 开发复杂度高:需要手动回收子进程、避免僵尸进程,并处理进程间通信(若有需求)。

2.2 环境准备

  1. 确保 PHP 编译时开启了 --enable-pcntl(多数 Linux 包管理器自带支持)。
  2. CLI 模式下运行。Web 环境(Apache/Nginx+PHP-FPM)通常不允许 pcntl_fork(),需要从命令行执行脚本。
  3. PHP 7+ 建议版本,语法与功能更完善。

2.3 简单示例:并行执行多个任务

下面示例演示如何利用 pcntl_fork() 启动多个子进程并行执行任务(如访问 URL、处理数据),并在父进程中等待所有子进程结束。

<?php
// 文件:multi_process.php

// 要并行执行的“任务”:简单模拟网络请求或耗时计算
function doTask(int $taskId) {
    echo "[子进程 {$taskId}] 开始任务,PID=" . getmypid() . "\n";
    // 模拟耗时:随机 sleep 1~3 秒
    $sleep = rand(1, 3);
    sleep($sleep);
    echo "[子进程 {$taskId}] 任务完成,用时 {$sleep} 秒\n";
}

// 任务数量(子进程数)
// 建议不要超过 CPU 核心数的 2 倍,否则上下文切换开销可能增大
$taskCount = 5;
$childPids = [];

// 父进程循环 fork
for ($i = 1; $i <= $taskCount; $i++) {
    $pid = pcntl_fork();
    if ($pid === -1) {
        // fork 失败
        die("无法 fork 子进程 #{$i}\n");
    } elseif ($pid === 0) {
        // 子进程分支
        doTask($i);
        // 子进程必须 exit,否则会继续执行父进程后续代码
        exit(0);
    } else {
        // 父进程分支:记录子进程 PID,继续循环创建下一子进程
        $childPids[] = $pid;
    }
}

// 父进程:等待所有子进程完成
echo "[父进程] 等待子进程完成...\n";
foreach ($childPids as $pid) {
    // pcntl_waitpid() 阻塞等待指定子进程结束
    pcntl_waitpid($pid, $status);
    echo "[父进程] 子进程 PID={$pid} 已结束,状态={$status}\n";
}
echo "[父进程] 所有子进程已完成,退出。\n";

运行方式

php multi_process.php

ASCII 流程图

┌─────────────────────────────────────────────────────────┐
│                      父进程 (PID = 1000)                │
└─────────────────────────────────────────────────────────┘
                        │
                        │ pcntl_fork() 创建子进程 1 (PID=1001)
                        ↓
┌─────────────────┐    ┌─────────────────┐
│ 父进程 继续循环 │    │ 子进程 1 执行 doTask(1) │
│ (记录 PID=1001) │    │                  │
└─────────────────┘    └─────────────────┘
   │                            │
   │ pcntl_fork() 创建子进程 2   │
   ↓                            ↓
┌─────────────────┐      ┌────────────────────┐
│ 父进程 继续循环 │      │ 子进程 2 执行 doTask(2) │
│ (记录 PID=1002) │      │                    │
└─────────────────┘      └────────────────────┘
   │                            │
   ⋮                            ⋮
   │                            │
   │ pcntl_fork() 创建子进程 5   │
   ↓                            ↓
┌─────────────────┐      ┌────────────────────┐
│ 父进程 循环结束 │      │ 子进程 5 执行 doTask(5) │
│ (记录 PID=1005) │      │                    │
└─────────────────┘      └────────────────────┘
   │                            │
   │ 父进程调用 pcntl_waitpid() 等待各子进程结束
   └─────────────────────────────────────────────────>
                            │
            ┌───────────────────────────────────────┐
            │ 子进程各自执行完 doTask() 后 exit(0)  │
            └───────────────────────────────────────┘
                            ↓
                父进程输出“子进程 PID 已结束”消息
                            ↓
                 父进程等待完毕后退出脚本

解析与要点

  1. pcntl_fork():返回值

    • 在父进程中,返回子进程的 PID(>0)
    • 在子进程中,返回 0
    • 失败时,返回 -1
  2. 子进程执行完毕后必须 exit(0),否则子进程会继续执行父进程后续代码,导致进程混淆。
  3. 父进程通过 pcntl_waitpid($pid, $status) 阻塞等待指定子进程结束,并获取退出状态。
  4. 最好将任务量与 CPU 核心数做简要衡量,避免创建过多子进程带来过大上下文切换成本。

三、方案二:多线程/多任务 —— 使用 Parallel 扩展

注意:PHP 官方不再维护 pthreads 扩展,且仅支持 CLI ZTS(线程安全)版。更推荐使用 PHP 7.4+ 的 Parallel 扩展,能在 CLI 下创建“并行运行环境(Runtime)”,每个 Runtime 都是独立的线程环境,可以运行 \parallel\Future 任务。

3.1 基本原理

  • Parallel 扩展:为 PHP 提供了一套纯 PHP 层的并行处理 API,通过 parallel\Runtime 在后台启动一个线程环境,每个环境会有自己独立的上下文,可以运行指定的函数或脚本。
  • 优势

    1. 内存隔离:与 pcntl 类似,Runtime 内的代码有自己独立内存,不会与主线程直接共享变量,避免竞争。
    2. API 友好:更类似“线程池+任务队列”模型,提交任务后可异步获取结果。
    3. 无需 ZTS:Parallel 扩展无需编译成 ZTS 版本的 PHP,即可使用。
  • 劣势

    1. 环境要求:仅支持 PHP 7.2+,且需先通过 pecl install parallel 安装扩展。
    2. 内存开销:每个 Runtime 会在后台生成一个线程及其上下文,资源消耗不可忽视。
    3. 不支持 Web 环境:仅能在 CLI 下运行。

3.2 安装与检查

# 安装 Parallel 扩展
pecl install parallel

# 确保 php.ini 中已加载 parallel.so
echo "extension=parallel.so" >> /etc/php/7.4/cli/php.ini

# 验证
php -m | grep parallel
# 如果输出 parallel 则说明安装成功

3.3 简单示例:并行执行多个函数

以下示例演示如何使用 parallel\RuntimeFuture 并行执行多个耗时函数,并在主线程中等待所有结果。

<?php
// 文件:parallel_example.php

use parallel\Runtime;
use parallel\Future;

// 自动加载如果使用 Composer,可根据实际情况调整
// require 'vendor/autoload.php';

// 模拟耗时函数:睡眠 1~3 秒
function taskFunction(int $taskId): string {
    echo "[Thread {$taskId}] 开始任务,TID=" . getmypid() . "\n";
    $sleep = rand(1, 3);
    sleep($sleep);
    return "[Thread {$taskId}] 完成任务,用时 {$sleep} 秒";
}

$taskCount = 5;
$runtimes = [];
$futures = [];

// 1. 创建多个 Runtime(相当于线程环境)
for ($i = 1; $i <= $taskCount; $i++) {
    $runtimes[$i] = new Runtime(); // 新建线程环境
}

// 2. 向各 Runtime 提交任务
for ($i = 1; $i <= $taskCount; $i++) {
    // run() 返回 Future 对象,可通过 Future->value() 获取执行结果(阻塞)
    $futures[$i] = $runtimes[$i]->run(function(int $tid) {
        return taskFunction($tid);
    }, [$i]);
}

// 3. 主线程等待并获取所有结果
foreach ($futures as $i => $future) {
    $result = $future->value(); // 阻塞到对应任务完成
    echo $result . "\n";
}

// 4. 关闭线程环境(可选,PHP 会在脚本结束时自动回收)
foreach ($runtimes as $rt) {
    $rt->close();
}

echo "[主线程] 所有并行任务已完成,退出。\n";

运行方式

php parallel_example.php

ASCII 流程图

┌──────────────────────────────────────────────────────┐
│                    主线程 (PID=2000)                │
└──────────────────────────────────────────────────────┘
     │           │           │           │           │
     │           │           │           │           │
     ▼           ▼           ▼           ▼           ▼
┌────────┐  ┌────────┐  ┌────────┐  ┌────────┐  ┌────────┐
│Runtime1│  │Runtime2│  │Runtime3│  │Runtime4│  │Runtime5│   <- 每个都是一个独立线程环境
│ (TID)  │  │ (TID)  │  │ (TID)  │  │ (TID)  │  │ (TID)  │
└───┬────┘  └───┬────┘  └───┬────┘  └───┬────┘  └───┬────┘
    │             │            │             │           │
    │ 提交任务     │ 提交任务     │ 提交任务      │ 提交任务    │ 提交任务
    ▼             ▼            ▼             ▼           ▼
[Thread1]     [Thread2]     [Thread3]     [Thread4]   [Thread5]
 doTask(1)    doTask(2)    doTask(3)    doTask(4)  doTask(5)
    │             │            │             │          │
    └─────┬───────┴──┬─────────┴──┬───────────┴───┬──────┘
          ▼          ▼           ▼               ▼
   主线程等待 future->value()   ...         Collect Results

解析与要点

  1. new Runtime():为每个并行任务创建一个新的“线程环境”,内部会复制(序列化再反序列化)全局依赖。
  2. 闭包函数传参run(function, [args]) 中,闭包与传入参数会被序列化并发送到对应 Runtime 环境。
  3. Future->value():阻塞等待目标线程返回执行结果。若当前 Future 已完成则立即返回。
  4. 资源隔离:在闭包内部定义的函数 taskFunction 是通过序列化传给线程,并在新环境内执行,主线程无法直接访问线程内部变量。
  5. 关闭线程:可通过 $runtime->close() 将线程环境释放,但脚本结束时会自动回收,无需手动关闭也可。

四、方案三:协程/异步 —— 使用 Swoole 扩展

4.1 基本原理

  • Swoole:一个为 PHP 提供高性能网络通信、异步 I/O、协程等功能的扩展。通过协程(Coroutine)机制,让 PHP 在单进程内实现类似“多线程”的并发效果。
  • 协程:相比传统“线程”更轻量,切换时无需系统调度,几乎没有上下文切换成本。
  • 优势

    1. 高并发 I/O 性能:适合高并发网络请求、长连接、WebSocket 等场景。
    2. 简单语法:使用 go(function() { … }) 即可创建协程,在协程内部可以像写同步代码一样写异步逻辑。
    3. 丰富生态:Swoole 内置 HTTP Server、WebSocket Server、定时器、Channel 等并发构建块。
  • 劣势

    1. 需要安装扩展:需先 pecl install swoole 或自行编译安装。
    2. 不适合全栈同步框架:若项目大量依赖同步阻塞式代码,需要做协程安全改造。
    3. 需使用 CLI 方式运行:不能像普通 PHP-FPM 一样被 Nginx 调用。

4.2 安装与检查

# 安装 Swoole 最新稳定版本
pecl install swoole

# 确保 php.ini 中已加载 swoole.so
echo "extension=swoole.so" >> /etc/php/7.4/cli/php.ini

# 验证
php --ri swoole
# 会显示 Swoole 版本与配置信息

4.3 示例一:协程并行发起多 HTTP 请求

下面示例展示如何通过 Swoole 协程并发地发起多个 HTTP GET 请求,并在所有请求完成后收集响应。

<?php
// 文件:swoole_coro_http.php

use Swoole\Coroutine\Http\Client;
use Swoole\Coroutine;

// 要并行请求的 URL 列表
$urls = [
    'http://httpbin.org/delay/2', // 延迟 2 秒返回
    'http://httpbin.org/delay/1',
    'http://httpbin.org/status/200',
    'http://httpbin.org/uuid',
    'http://httpbin.org/get'
];

// 协程入口
Co\run(function() use ($urls) {
    $responses = [];
    $wg = new Swoole\Coroutine\WaitGroup();

    foreach ($urls as $index => $url) {
        $wg->add(); // 增加等待组计数
        go(function() use ($index, $url, &$responses, $wg) {
            $parsed = parse_url($url);
            $host = $parsed['host'];
            $port = ($parsed['scheme'] === 'https') ? 443 : 80;
            $path = $parsed['path'] . (isset($parsed['query']) ? "?{$parsed['query']}" : '');

            $cli = new Client($host, $port, $parsed['scheme'] === 'https');
            $cli->set(['timeout' => 5]);
            $cli->get($path);
            $body = $cli->body;
            $status = $cli->statusCode;
            $cli->close();

            $responses[$index] = [
                'url'    => $url,
                'status' => $status,
                'body'   => substr($body, 0, 100) . '…' // 为示例只截取前100字符
            ];
            echo "[协程 {$index}] 请求 {$url} 完成,状态码={$status}\n";
            $wg->done(); // 通知 WaitGroup 当前协程已完成
        });
    }

    // 等待所有协程执行完毕
    $wg->wait();
    echo "[主协程] 所有请求已完成,共 " . count($responses) . " 条响应。\n";
    print_r($responses);
});

运行方式

php swoole_coro_http.php

ASCII 流程图

┌───────────────────────────────────────────────────────────────────┐
│                      主协程 (Main Coroutine)                     │
└───────────────────────────────────────────────────────────────────┘
          │             │              │              │              │
      go()【】       go()【】        go()【】        go()【】        go()【】
          │             │              │              │              │
          ▼             ▼              ▼              ▼              ▼
 [协程 0]         [协程 1]        [协程 2]         [协程 3]       [协程 4]
  send GET       send GET         send GET         send GET       send GET
  await I/O      await I/O        await I/O        await I/O      await I/O
    ↑               ↑               ↑               ↑             ↑
   I/O 完成       I/O 完成        I/O 完成        I/O 完成       I/O 完成
    │               │               │               │             │
  异步返回        异步返回         异步返回        异步返回      异步返回
    │               │               │              │             │
  协程 0                               …                            协程 4
  写入 $responses                              …                      写入 $responses
  $wg->done()                                    …                      $wg->done()
    │               │               │              │             │
┌───────────────────────────────────────────────────────────────────┐
│ 主协程调用 $wg->wait() 阻塞,直到所有 $wg->done() 都执行完成        │
└───────────────────────────────────────────────────────────────────┘
                           ↓
┌───────────────────────────────────────────────────────────────────┐
│       打印所有并行请求结果并退出脚本                             │
└───────────────────────────────────────────────────────────────────┘

解析与要点

  1. \Swoole\Coroutine\run():启动一个全新的协程容器环境,主协程会在回调内部启动。
  2. go(function() { … }):创建并切换到一个新协程执行闭包函数。
  3. Swoole\Coroutine\Http\Client:已被协程化的 HTTP 客户端,可在协程中非阻塞地进行网络请求。
  4. WaitGroup:相当于 Go 语言的 WaitGroup,用于等待多个协程都调用 $wg->done(),再从 $wg->wait() 的阻塞中继续执行。
  5. 单进程多协程:所有协程都跑在同一个系统进程中,不会像多进程/多线程那样切换内核调度,协程的上下文切换几乎没有开销。

4.4 示例二:使用 Swoole Process 实现多进程任务处理

如果项目无法全部迁移到协程模式,也可以使用 Swoole 提供的 Process 类来创建多进程,并结合管道/消息队列等在进程间通信。

以下示例演示如何用 Swoole Process 创建 3 个子进程并行执行任务,并在父进程中通过管道收集结果。

<?php
// 文件:swoole_process.php

use Swoole\Process;

// 要并行执行的耗时函数
function doJob(int $jobId) {
    echo "[子进程 {$jobId}] (PID=" . getmypid() . ") 开始任务\n";
    $sleep = rand(1, 3);
    sleep($sleep);
    $result = "[子进程 {$jobId}] 任务完成,用时 {$sleep} 秒";
    return $result;
}

$processCount = 3;
$childProcesses = [];

// 父进程创建多个 Swoole\Process
for ($i = 1; $i <= $processCount; $i++) {
    // 1. 定义子进程回调,使用匿名函数捕获 $i 作为任务编号
    $process = new Process(function(Process $worker) use ($i) {
        // 子进程内部执行
        $result = doJob($i);
        // 将结果写入管道,父进程可读取
        $worker->write($result);
        // 退出子进程
        $worker->exit(0);
    }, true, SOCK_DGRAM); // 启用管道
    $pid = $process->start();
    $childProcesses[$i] = ['pid' => $pid, 'pipe' => $process];
}

// 父进程:等待并读取子进程通过管道写入的数据
foreach ($childProcesses as $i => $info) {
    $pipe = $info['pipe'];
    // 阻塞读取子进程写入管道的数据
    $data = $pipe->read();
    echo "[父进程] 收到子进程 {$i} 结果:{$data}\n";
    // 等待子进程退出,避免僵尸进程
    Process::wait(true);
}

echo "[父进程] 所有子进程处理完成,退出。\n";

运行方式

php swoole_process.php

ASCII 流程图

┌──────────────────────────────────────────────────────────┐
│                      父进程 (PID=3000)                   │
└──────────────────────────────────────────────────────────┘
       │            │            │
       │            │            │
       ▼            ▼            ▼
┌────────┐    ┌────────┐    ┌────────┐
│Proc #1 │    │Proc #2 │    │Proc #3 │
│(PID)   │    │(PID)   │    │(PID)   │
└───┬────┘    └───┬────┘    └───┬────┘
    │             │             │
    │ doJob(1)    │ doJob(2)    │ doJob(3)
    │             │             │
    │ write “结果” │ write “结果” │ write “结果”
    ▼             ▼             ▼
 父进程从管道中    父进程从管道中   父进程从管道中
  读到结果 1       读到结果 2      读到结果 3
    │             │             │
    └─────────────┴─────────────┘
                   │
       父进程调用 Process::wait() 回收子进程
                   ↓
        父进程输出“所有子进程完成”后退出

解析与要点

  1. new Process(callable, true, SOCK_DGRAM):第二个参数 true 表示启用管道通信;第三个参数指定管道类型(SOCK_DGRAMSOCK_STREAM)。
  2. 子进程写入管道:调用 $worker->write($data),父进程通过 $process->read() 获取数据。
  3. 父进程回收子进程:使用 Process::wait()(或 Process::wait(true))等待任意子进程退出,并避免产生僵尸进程。
  4. Swoole Process 与 PCNTL 的区别:前者封装更完善,有更方便的进程管理 API,但本质依然是多进程模型。

五、三种方案对比与选型建议

特性 / 方案多进程(PCNTL)多线程/多任务(Parallel)协程/异步(Swoole)
并发模型操作系统原生进程PHP 用户态线程环境协程(用户态调度,单进程)
安装与启用PHP CLI + pcntl 扩展PHP 7.2+ + parallel 扩展PHP 7.x + swoole 扩展
内存开销每个子进程复制父进程内存(COW)每个 Runtime 启动独立线程,需复制上下文单进程内协程切换,无额外线程上下文
上下文切换开销较高(内核调度)较高(线程调度)非常低(协程切换由 Swoole 管理)
平台兼容性仅 CLI(Unix-like)仅 CLI(PHP 7.2+)仅 CLI(Unix-like/Windows,都支持)
编程复杂度中等(手动 fork/wait、IPC)低(类似线程池、Future 模式)低(异步写法接近同步,可用 channel、WaitGroup)
适用场景计算密集型、多核利用;进程隔离中小规模并行计算;任务隔离高并发 I/O;网络爬虫;实时通信
数据共享进程间需通过管道/消息队列等 IPC线程间需序列化数据到 Runtime协程可共享全局变量(需注意同步)
稳定性高:一个子进程崩溃不影响父进程较高:线程隔离度不如进程,但 Runtime 崩溃会影响父高:协程内抛异常可捕获,单进程风险较低

5.1 选型建议

  1. 纯 CPU 密集型任务(如数据批量计算、图像处理):

    • 建议使用 多进程(PCNTL),能够充分利用多核 CPU,且进程间隔离性好。
  2. 分布式任务调度、轻量并行计算(如同时处理多个独立小任务):

    • 可以考虑 Parallel 扩展,API 更简单,适合 PHP 内部任务并行。
  3. 大量并发网络请求、I/O 密集型场景(如批量爬虫、聊天室、长连接服务):

    • 强烈推荐 Swoole 协程,其异步 I/O 性能远超多进程/多线程,并发量可达数万级别。
  4. 小型脚本并发需求(如定时脚本并行处理少量任务,不想引入复杂扩展):

    • 使用 PCNTL 即可,开发成本低,无需额外安装第三方扩展。

六、常见问题与注意事项

  1. PCNTL 进程数过多导致内存耗尽

    • 在多进程模式下,若一次性 fork 过多子进程(如上百个),会瞬间占用大量内存,可能触发 OOM。
    • 建议按 CPU 核心数设定进程数,或按业务量使用固定大小的进程池,并用队列控制任务分发。
  2. Parallel 运行时环境上下文传递限制

    • Parallel 会序列化全局变量与闭包,若闭包中捕获了不可序列化资源(如数据库连接、Socket),会导致失败。
    • 最好将要执行的代码与其依赖的类、函数文件放在同一脚本中,或先在 Runtime 内重新加载依赖。
  3. Swoole 协程中不可使用阻塞 I/O

    • 在协程中必须使用 Swoole 提供的协程化 I/O(如 Co\MySQL\Co\Http\Client)或 PHP 原生的非阻塞流程(如 file_get_contents 会阻塞整个进程)。
    • 若使用阻塞 I/O,整个进程会被挂起,丧失协程并发优势。
  4. 进程/协程内错误处理

    • 子进程/子协程内发生致命错误不会直接中断父进程,但需要在父进程中捕获(如 PCNTL 的 pcntl_signal(SIGCHLD, ...) 或 Swoole 协程模式下的 try/catch)。
    • 建议在子进程或协程内部加上异常捕获,并在写入管道或 Future 返回错误信息,以便父进程统一处理。
  5. 跨平台兼容性

    • PCNTL 仅在 Linux/macOS 环境可用,Windows 不支持。
    • Parallel 在 Windows、Linux 都可用,但需要 PECL 安装。
    • Swoole 支持多平台,Windows 下也可正常编译与运行,但需使用对应的 DLL 文件。

七、总结

本文系统地介绍了 PHP 并发处理的三种高效解决方案

  1. 多进程(PCNTL)

    • 通过 pcntl_fork() 启动子进程并行运行任务,适合计算密集型或需要进程隔离的场景。
    • 示例中演示了如何 fork 五个子进程并 parallel 执行固定任务,并通过 pcntl_waitpid() 等待子进程结束。
  2. 多线程/多任务(Parallel 扩展)

    • 利用 parallel\Runtime 创建线程环境并提交闭包任务,以 Future->value() 等待结果,适合中小规模并行任务。
    • 相比 PCNTL 更易管理,API 友好,但仍需在 CLI 环境下运行,且需先安装 parallel 扩展。
  3. 协程/异步(Swoole 扩展)

    • 以协程为基础,在单进程内实现高并发 I/O 操作。示例演示了协程并行发起多 HTTP 请求,使用 WaitGroup 整合结果,适合高并发网络场景。
    • Swoole 还提供 Process 类,可用于多进程管理。

最后,结合不同场景与业务需求,进行合理选型:

  • CPU 密集型:优先 PCNTL 多进程。
  • 轻量并行:优先 Parallel 多任务。
  • 高并发 I/O:优先 Swoole 协程异步。
2024-08-24

在PHP中使用Swoole扩展可以轻松地创建TCP服务器。以下是一个简单的TCP服务器示例代码:




<?php
// 创建TCP服务器对象,监听127.0.0.1的9501端口
$tcp_server = new Swoole\Server('127.0.0.1', 9501);
 
// 注册连接回调函数
$tcp_server->on('Connect', function ($server, $fd) {
    echo "客户端连接,ID:{$fd}\n";
});
 
// 注册接收数据回调函数
$tcp_server->on('Receive', function ($server, $fd, $reactor_id, $data) {
    $server->send($fd, "服务器收到数据:{$data}");
});
 
// 注册关闭连接回调函数
$tcp_server->on('Close', function ($server, $fd) {
    echo "客户端关闭,ID:{$fd}\n";
});
 
// 启动服务器
$tcp_server->start();

在这个例子中,我们创建了一个监听在本机IP地址127.0.0.1和端口9501上的TCP服务器。当客户端连接、发送数据或关闭连接时,会分别触发ConnectReceiveClose事件,并执行相应的回调函数。

确保你的PHP环境已经安装了Swoole扩展。可以通过运行php --ri swoole来检查Swoole扩展是否已经安装和加载。如果没有安装,你可以通过PECL安装Swoole扩展,使用命令pecl install swoole

2024-08-23

Swoole和Go在协程性能上的比较并不公平,因为Swoole是一个使用C语言编写的PHP扩展,而Go是一个全新的编程语言。虽然Swoole可以用于编写协程,但它并不是Go语言的直接对手。

在比较PHP Swoole与Go在协程性能时,应当考虑到Go具有内置的协程支持,并且是为高并发而设计的。因此,在相同的工作负载下,Go的性能可能会更高。

然而,这样的比较并不公平,因为它没有考虑到PHP或Go与Swoole之间的其他差异,例如运行时内存消耗、开发生态系统、学习曲线等。

如果你想要进行一个更为公平的比较,你可以使用两种语言各自的协程库来编写简单的HTTP服务器,并在相同的负载下进行测试。

以下是使用Go语言的Go协程和使用Swoole的协程的简单示例:

Go语言协程示例:




package main
 
import (
    "context"
    "fmt"
    "net/http"
    "time"
)
 
func helloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, World!")
}
 
func main() {
    http.HandleFunc("/", helloHandler)
 
    server := &http.Server{Addr: ":8080"}
    go func() {
        if err := server.ListenAndServe(); err != http.ErrServerClosed {
            // Handle error from ListenAndServe
        }
    }()
 
    // Wait for interrupt signal to gracefully shutdown the server with
    // a timeout of 5 seconds.
    quit := make(chan os.Signal)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    server.Shutdown(ctx)
}

Swoole协程示例 (这里只是一个简单的HTTP服务器,并没有实现完整的协程):




<?php
class Server
{
    public $serv;
 
    public function __construct()
    {
        $this->serv = new swoole_http_server("0.0.0.0", 9501);
        $this->serv->set([
            'worker_num' => 2,
            'task_worker_num' => 2,
            'enable_coroutine' => true,
        ]);
        $this->serv->on('Request', [$this, 'onRequest']);
        $this->serv->start();
    }
 
    public function onRequest($request, $response)
    {
        $response->header("Content-Type", "text/plain");
        $response->end("Hello World\n");
    }
}
 
new Server();

请注意,这些示例都非常简单,并不能提供一个完整的协程使用场景。在实际的生产环境中,你需要考虑更多的因素,例如内存管理、错误处理、安全性等。

综上所述,由于Go语言的设计初衷就是为了高并发而生,因此在协程性能上通常优于Swoole,但这也取决于具体的使用场景和工作负载。为了公平的比较,应该使用两种语言各自的最佳实践来编写应用程序。

2024-08-23

以下是一个基于Docker的示例,演示如何使用Supervisor来管理PHP-FPM和Swoole的执行。

首先,创建一个Dockerfile来构建你的PHP环境:




FROM php:7.4-fpm
 
# 安装Supervisor
RUN apt-get update && apt-get install -y supervisor
 
# 安装Swoole扩展
RUN docker-php-ext-install swoole
 
# 复制Supervisor配置文件
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
 
# 设置容器启动时启动Supervisor
CMD ["/usr/bin/supervisord"]

然后,创建一个supervisord.conf文件来配置Supervisor:




[program:php-fpm]
command=/usr/sbin/php-fpm
autostart=true
autorestart=true
 
[program:swoole]
command=/usr/bin/php /path/to/your/swoole/server.php
autostart=true
autorestart=true

确保替换/path/to/your/swoole/server.php为你的Swoole服务脚本路径。

最后,构建并运行你的Docker镜像:




docker build -t your-php-swoole-image .
docker run -d your-php-swoole-image

这个示例演示了如何使用Supervisor来管理PHP-FPM和Swoole进程,确保你的Laravel应用程序能够在Swoole协程环境中运行。

2024-08-19

在PHP中安装Swoole扩展通常可以通过以下步骤进行:

  1. 确保你的PHP版本至少是7.0以上。
  2. 下载Swoole扩展的源代码。可以从Swoole的GitHub仓库(https://github.com/swoole/swoole-src)下载。
  3. 解压缩下载的源代码并进入到目录中。
  4. 使用PHP的pecl命令行工具来安装Swoole扩展,如果没有pecl,可以通过你系统的包管理器安装。

以下是在Linux系统中使用pecl安装Swoole的命令:




pecl install swoole

安装完成后,会提示你在php.ini文件中添加一行来启用这个扩展。按照提示添加:




extension=swoole.so
  1. 重启你的Web服务器或PHP-FPM服务来使扩展生效。
  2. 验证Swoole扩展是否正确安装:



php -m | grep swoole

如果这个命令返回了swoole,则表示扩展已经成功安装。

请注意,具体的安装步骤可能会根据你的操作系统和PHP环境有所不同。如果你使用的是Windows系统,安装过程可能会有所区别。

2024-08-19

Swoole 是一个使用 C 语言编写的 PHP 异步事件驱动的网络通信引擎,可以使 PHP 代码具备异步非阻塞的 I/O 操作能力。

以下是一个简单的 Swoole 服务器示例,它创建了一个 TCP 服务器,并在客户端连接和接收数据时进行处理:




<?php
// 创建 Swoole 服务器对象,参数为 IP 地址和端口
$server = new Swoole\Server('127.0.0.1', 9501);
 
// 注册连接回调函数
$server->on('Connect', function ($server, $fd) {
    echo "客户端连接,ID:{$fd}\n";
});
 
// 注册接收数据回调函数
$server->on('Receive', function ($server, $fd, $reactor_id, $data) {
    $server->send($fd, "服务器收到数据:{$data}");
});
 
// 注册关闭连接回调函数
$server->on('Close', function ($server, $fd) {
    echo "客户端关闭,ID:{$fd}\n";
});
 
// 启动服务器
$server->start();

在这个例子中,服务器监听在 IP 地址 127.0.0.1 和端口 9501 上。当客户端连接、发送数据或关闭连接时,会触发相应的回调函数,并执行相应的处理逻辑。

要运行这段代码,你需要确保你的环境中已经安装了 Swoole 扩展。可以使用 php --ri swoole 命令来检查 Swoole 扩展是否安装以及其版本信息。如果未安装,你可以通过 PECL 或者从源代码编译安装 Swoole。

2024-08-17

在这个例子中,我们将展示如何使用Swoole和Go语言来实现一个简单的HTTP服务器,并进行比较。




// PHP + Swoole 实现
$http = new Swoole\Http\Server("0.0.0.0", 9501);
 
$http->on("request", function ($request, $response) {
    $response->header("Content-Type", "text/plain");
    $response->end("Hello Swoole\n");
});
 
$http->start();



// Go 语言实现
package main
 
import (
    "net/http"
    "os"
)
 
func helloHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/plain")
    w.Write([]byte("Hello Go\n"))
}
 
func main() {
    http.HandleFunc("/", helloHandler)
    http.ListenAndServe(":9501", nil)
}

在这两个例子中,我们都创建了一个监听在9501端口的HTTP服务器,并在接收到请求时返回一个简单的字符串。虽然两种语言和框架在语法和API上有所不同,但它们都展示了如何创建一个基本的HTTP服务器并响应请求。

在实际的技术选型比较中,你可能还需要考虑以下因素:

  1. 开发团队对两种语言的熟悉程度。
  2. 项目的时间限制和预期的功能需求。
  3. 运维团队对两种语言和工具的熟悉程度。
  4. 项目的可扩展性和性能要求。
  5. 项目的长期稳定性和安全性要求。
  6. 与现有系统的集成和兼容性要求。

综合以上因素,最终选择PHP + Swoole或Go作为技术栈可能会因项目而异。

2024-08-17

Swoole 是一个使用 PHP 语言编写的高性能网络通信引擎,提供了异步事件驱动、协程、高级队列、Socket 编程等功能。

在 PHP 中使用 Swoole,首先需要确保你的环境中已经安装了 Swoole 扩展。可以通过以下命令安装:




pecl install swoole

安装完成后,在 php.ini 文件中添加扩展:




extension=swoole.so

下面是一个使用 Swoole 创建一个简单的 TCP 服务器的例子:




<?php
// 创建 Server 对象,监听 1234 端口
$server = new Swoole\Server("127.0.0.1", 1234);
 
// 监听连接进入事件
$server->on('Connect', function ($server, $fd) {
    echo "Client: Connect.\n";
});
 
// 监听数据接收事件
$server->on('Receive', function ($server, $fd, $reactor_id, $data) {
    $server->send($fd, "Server: ".$data);
});
 
// 监听连接关闭事件
$server->on('Close', function ($server, $fd) {
    echo "Client: Close.\n";
});
 
// 启动服务器
$server->start();

运行这个脚本后,服务器将在本机的 1234 端口监听连接。当客户端连接到服务器时,它会收到一个连接进入的通知,并可以接收发送到服务器的数据。服务器收到数据后,会回复一条消息,并在终端打印出相关的信息。

Swoole 提供了丰富的功能,例如创建 HTTP、WebSocket 服务器,使用协程,处理异步任务等。具体使用时,需要根据实际需求选择合适的 Swoole 功能和组件。

2024-08-17

Swoole 是一个使用 PHP 编写的异步并行网络通信引擎,提供了 PHP 语言的异步事件驱动、并行计算、socket 编程等特性。

Swoole 自发布以来,已经被许多开发者用于构建高性能的网络应用,比如 Web 服务器、游戏服务器、物联网设备接入、实时通讯系统等。它可以提高 PHP 应用的性能,并提供了更多的编程可能性。

关于“PHP再伟大”的问题,这是一个相对的概念,取决于你的具体需求和环境。Swoole 可以让 PHP 更加强大和灵活,但也要求开发者需要理解异步编程和网络编程的概念。

以下是一个简单的 Swoole 服务器示例,它创建了一个 TCP 服务器,并在客户端连接时打印一个消息:




<?php
// 创建 Swoole 服务器对象,参数为 IP 和端口
$server = new Swoole\Server('127.0.0.1', 9501);
 
// 注册连接回调函数
$server->on('Connect', function ($server, $fd) {
    echo "Client: Connect.\n";
});
 
// 注册接收数据回调函数
$server->on('Receive', function ($server, $fd, $reactor_id, $data) {
    $server->send($fd, "Server: ".$data);
});
 
// 注册关闭连接回调函数
$server->on('Close', function ($server, $fd) {
    echo "Client: Close.\n";
});
 
// 启动服务器
$server->start();

在这个例子中,我们创建了一个监听在本机 9501 端口的 TCP 服务器,并对客户端的连接、接收数据和关闭连接进行了简单的处理。

因此,虽然 Swoole 可以让 PHP 变得更加强大,但它也增加了学习和使用的复杂度。开发者需要了解 PHP 的异步编程模式,以及 Swoole 提供的各种功能和API。如果你有这样的需求和意愿,Swoole 将是一个值得探索和学习的工具。