由于越来越需要在每个 Web 请求中执行更多操作,因此异步编程是扩展 Web 应用程序的基础构建块。一个典型的例子是发送电子邮件作为请求的一部分。
在许多 Web 应用程序中,当服务器上处理某些内容时,我们希望通过电子邮件通知人们,这通常是对第三方服务(如 SendGrid、Mailchimp 等)的单独 HTTP 请求。
当您需要一次发送大量电子邮件时,这将成为一个非常简单的示例。在 PHP 中,如果您要发送电子邮件并且 HTTP 过程需要 100 毫秒才能完成,那么您将通过发送数十或数百封电子邮件来快速增加请求的总时间。
当然,任何优秀的第三方电子邮件服务都会提供一个批量端点来否定这一点,但为了举例 - 假设您要发送 100 封电子邮件,并且每封电子邮件都必须单独处理。
因此,我们需要做出决定:我们如何才能将电子邮件的处理转移到一个单独的进程中,以便它不会阻止原始 Web 请求?
这就是我们将在本文中探索的内容,特别是在有或没有新基础架构的情况下可以在 PHP 中解决的所有不同方法。
使用 exec()
exec()是 PHP 中的原生函数,可用于执行外部程序并返回结果。在我们的例子中,它可能是一个发送电子邮件的脚本。这个函数使用操作系统来生成一个全新的(空白的,没有复制或共享的)进程,你可以将任何你需要的状态传递给它。
让我们看一个例子。
<?php
// handle a web request
// record the start time of the web request
$start = microtime(true);
$path = __DIR__ . '/send_email.php';
// output to /dev/null & so we don't block to wait for the result
$command = 'php ' . $path . ' --email=%s > /dev/null &';
$emails = ['joe@blogs.com', 'jack@test.com'];
// for each of the emails, call exec to start a new script
foreach ($emails as $email) {
// Execute the command
exec(sprintf($command, $email));
}
// record the finish time of the web request
$finish = microtime(true);
$duration = round($finish - $start, 4);
// output duration of web request
echo "finished web request in $duration\n";
发送电子邮件.php
<?php
$email = explode('--email=', $argv[1])[1];
// this blocking sleep won't affect the web request duration
// (illustrative purposes only)
sleep(5);
// here we can send the email
echo "sending email to $email\n";
输出
$ php src/exec.php
finished web request in 0.0184
上面的脚本显示 Web 请求仍然在几毫秒内完成,即使sleep在 send_email.php 脚本中有一个阻塞函数调用。
它不阻塞的原因是因为我们已经在命令中包含exec我们> /dev/null &不想等待exec命令完成所以我们可以获得结果,这意味着它可以在后台和网络请求中发生可以继续。
这样,Web 请求脚本只负责运行脚本,而不是监视其执行和/或失败。
这是该解决方案固有的缺点,因为流程的监控落在流程本身上并且无法重新启动。但是,这是一种无需太多努力即可将异步行为引入 PHP 应用程序的简单方法。
exec在服务器上运行命令,因此您必须注意脚本的执行方式,尤其是当它涉及用户输入时。它可能很难管理,exec尤其是当您管理应用程序的扩展时,因为脚本可能运行在处理外部 Web 请求的同一个机器上,所以如果有数百或数千个新进程,您最终可能会耗尽 CPU 和内存产生于exec.
pcntl_fork
pcntl_fork是一个低级函数,需要启用 PCNTL 扩展,它是一种功能强大但可能容易出错的方法,用于在 PHP 中编写异步代码。
pcntl_fork将分叉或克隆当前进程并将其拆分为一个父进程和多个子进程(取决于它被调用的次数)。通过检测进程 ID 或 PID,我们可以在父进程或子进程的上下文中运行不同的代码。
父进程将负责生成子进程并等待生成的进程完成后才能完成。
在这种情况下,我们可以更好地控制进程如何退出,并且可以轻松地编写一些逻辑来处理子进程失败时的重试。
现在,转到我们用例的示例代码,以非阻塞方式发送电子邮件。
<?php
function sendEmail($to, $subject, $message)
{
// Code to send email (replace with your email sending logic)
// This is just a mock implementation for demonstration purposes
sleep(3); // Simulating sending email by sleeping for 3 seconds
echo "Email sent to: $to\n";
}
$emails = [
[
'to' => 'john@example.com',
'subject' => 'Hello John',
'message' => 'This is a test email for John.',
],
[
'to' => 'jane@example.com',
'subject' => 'Hello Jane',
'message' => 'This is a test email for Jane.',
],
// Add more email entries as needed
];
$children = []