PHP Conference Japan 2024

pcntl_fork

(PHP 4 >= 4.1.0, PHP 5, PHP 7, PHP 8)

pcntl_fork複製目前執行的程序

說明

pcntl_fork(): int

pcntl_fork() 函式會建立一個子程序,該子程序僅在其 PID 和 PPID 上與父程序不同。請參閱您系統的 fork(2) 手冊頁面,以了解有關 fork 如何在您的系統上運作的具體詳細資訊。

參數

此函式沒有參數。

回傳值

成功時,子程序的 PID 會在父程序的執行緒中回傳,而 0 會在子程序的執行緒中回傳。失敗時,-1 會在父程序的上下文中回傳,不會建立子程序,並引發 PHP 錯誤。

範例

範例 #1 pcntl_fork() 範例

<?php

$pid
= pcntl_fork();
if (
$pid == -1) {
die(
'無法複製');
} else if (
$pid) {
// 我們是父程序
pcntl_wait($status); // 防止殭屍子程序
} else {
// 我們是子程序
}

?>

參見

新增註解

使用者貢獻的註解 25 則註解

sean dot kelly at mediatile dot com
14 年前
「致命錯誤」一直是我的世界中的禍根,因為沒有任何方法可以在 PHP 中捕獲和處理這種情況。我的團隊幾乎所有東西都用 PHP 建構,以便利用我們核心的程式碼庫,因此找到一個解決方案來解決腳本無法恢復地崩潰,而我們永遠不知道這一點的問題至關重要。

我們其中一個背景自動化系統會建立一個「任務佇列」,對於佇列中的每個任務,都會 include() 一個 PHP 模組來處理該任務。然而,有時行為不佳的模組會因致命錯誤而崩潰,並連帶使父腳本崩潰。

我決定嘗試使用 pcntl_fork() 將任務模組與父程式碼隔離,而且似乎奏效:在模組內產生的致命錯誤會使子任務崩潰,而等待的父程序可以簡單地捕獲子程序的回傳碼,並根據需要追蹤/警告我們問題。

當然,如果我只想 exec() 模組並檢查輸出,也可以完成類似的事情,但是這樣我就無法獲得父腳本精心準備的有狀態環境的好處。這使我能夠讓子程序在父程序正在執行的環境中執行,而不必承擔致命錯誤阻止任務佇列繼續處理的後果。

這是 fork_n_wait.php 給您娛樂

<?php

if (! function_exists('pcntl_fork')) die('此 PHP 安裝中無法使用 PCNTL 函式');

for (
$x = 1; $x < 5; $x++) {
switch (
$pid = pcntl_fork()) {
case -
1:
// @fail
die('複製失敗');
break;

case
0:
// @child: 在此 include() 行為不當的程式碼
print "FORK:子程序 #{$x} 準備爆炸...\n";
generate_fatal_error(); // 未定義函式
break;

default:
// @parent
print "FORK:父程序,讓子程序橫行...\n";
pcntl_waitpid($pid, $status);
break;
}
}

print
"完成! :^)\n\n";
?>

輸出結果為
php -q fork_n_wait.php
FORK:子程序 #1 準備爆炸...
PHP 致命錯誤:在 ~fork_n_wait.php 的第 16 行呼叫未定義的函式 generate_fatal_error()
FORK:父程序,讓子程序橫行...
FORK:子程序 #2 準備爆炸...
PHP 致命錯誤:在 ~/fork_n_wait.php 的第 16 行呼叫未定義的函式 generate_fatal_error()
FORK:父程序,讓子程序橫行...
FORK:子程序 #3 準備爆炸...
PHP 致命錯誤:在 ~/fork_n_wait.php 的第 16 行呼叫未定義的函式 generate_fatal_error()
FORK:父程序,讓子程序橫行...
FORK:子程序 #4 準備爆炸...
PHP 致命錯誤:在 ~/fork_n_wait.php 的第 16 行呼叫未定義的函式 generate_fatal_error()
FORK:父程序,讓子程序橫行...
完成! :^)
amatsak at chestnutsoftware dot com
18 年前
複製時 MySQL 「在查詢期間遺失連線」問題的原因是子程序繼承了父程序的資料庫連線。當子程序結束時,連線就會關閉。如果父程序此時正在執行查詢,則會在已關閉的連線上執行,因此會發生錯誤。

避免此問題的簡單方法是在複製後立即在父程序中建立新的資料庫連線。別忘了在 mysql_connect() 的第四個引數中傳遞 true 來強制建立新的連線

<?php
// 建立 MySQL 連線
$db = mysql_connect($server, $username, $password);

$pid = pcntl_fork();

if (
$pid == -1 ) {
// Fork 失敗
exit(1);
} else if (
$pid ) {
// 我們是父進程
// 因為子進程會關閉連線,所以不能再使用 $db
// 而是為自己建立新的 MySQL 連線
$db = mysql_connect($server, $username, $password, true);
} else {
// 我們是子進程
// 在這裡使用繼承的連線做一些事
// 它會在結束時被關閉
exit(0);
?>

這樣一來,子進程將會繼承舊的連線,使用它並在結束時關閉。父進程不會在意,因為它會在 fork 之後立即為自己開啟新的連線。

希望這對您有幫助。
kenneth at fellowrock dot com
10 年前
我只是想為這個很棒的社群貢獻一點心力,並希望這對某些人有用。雖然 PHP 提供了執行緒選項和並行執行的 multi curl 處理,但我還是設法找出一個解決方案,可以在非執行緒版本的 PHP 中將每個函數作為自己的進程執行。

用法:#!/usr/bin/php
用法:php -f /path/to/file

#!/usr/bin/php
<?php
function fork_process($options)
{
$shared_memory_monitor = shmop_open(ftok(__FILE__, chr(0)), "c", 0644, count($options['process']));
$shared_memory_ids = (object) array();
for (
$i = 1; $i <= count($options['process']); $i++)
{
$shared_memory_ids->$i = shmop_open(ftok(__FILE__, chr($i)), "c", 0644, $options['size']);
}
for (
$i = 1; $i <= count($options['process']); $i++)
{
$pid = pcntl_fork();
if (!
$pid)
{
if(
$i==1)
usleep(100000);
$shared_memory_data = $options['process'][$i - 1]();
shmop_write($shared_memory_ids->$i, $shared_memory_data, 0);
shmop_write($shared_memory_monitor, "1", $i-1);
exit(
$i);
}
}
while (
pcntl_waitpid(0, $status) != -1)
{
if(
shmop_read($shared_memory_monitor, 0, count($options['process'])) == str_repeat("1", count($options['process'])))
{
$result = array();
foreach(
$shared_memory_ids as $key=>$value)
{
$result[$key-1] = shmop_read($shared_memory_ids->$key, 0, $options['size']);
shmop_delete($shared_memory_ids->$key);
}
shmop_delete($shared_memory_monitor);
$options['callback']($result);
}
}
}

// 為每個函數建立大小為 1M 的共享記憶體區塊。
$options['size'] = pow(1024,2);

// 定義 2 個函數作為各自的進程執行。
$options['process'][0] = function()
{
// 任何你需要做的事情都放在這裡...
// 如果你需要結果,回傳其值。
// 例如:長時間執行的進程 1
sleep(1);
return
'Hello ';
};
$options['process'][1] = function()
{
// 任何你需要做的事情都放在這裡...
// 如果你需要結果,回傳其值。
// 例如:
// 例如:長時間執行的進程 2
sleep(1);
return
'World!';
};
$options['callback'] = function($result)
{
// $results 是一個回傳值的陣列...
// $result[0] 對應 $options['process'][0] &
// $result[1] 對應 $options['process'][1] &
// 例如:
echo $result[0].$result[1]."\n";
};
fork_process($options);

?>

如果你想從網頁取得結果,請使用 exec()。例如:echo exec('php -f /path/to/file');

繼續駭客吧!:)
manishpatel2280 at gmail dot com
11 年前
使用 pcntl_fork 來 fork 進程很容易,但是一旦所有子進程完成後,我們要如何控制或進一步處理呢?這裡提供一種方法:

<?php
for ($i = 1; $i <= 5; ++$i) {
$pid = pcntl_fork();

if (!
$pid) {
sleep(1);
print
"In child $i\n";
exit(
$i);
}
}

while (
pcntl_waitpid(0, $status) != -1) {
$status = pcntl_wexitstatus($status);
echo
"Child $status completed\n";
}
?>
kexianbin at diyism dot com
12 年前
在 foreach 中 Fork

<?php
foreach ($tasks as $v)
{if ((
$pid=pcntl_fork())===-1)
{
//...
continue;
}
else if (
$pid)
{
pcntl_wait($status, WNOHANG); //防止殭屍子程序,一個等待對應一個子程序
}
else if (
$pid===0)
{
ob_start();//防止輸出到主程序
register_shutdown_function(create_function('$pars', 'ob_end_clean();posix_kill(getmypid(), SIGKILL);'), array());//在exit()之前殺死自己,否則會關閉與父程序共享的資源
//...
exit();//避免在子程序中執行 foreach 迴圈
}
}
?>
laurent dot salomon at ymail dot com
9 年前
以下範例可在 php5.6 和 apcu 上運作。此範例已在 debian 7 和 Ubuntu 14.04.2 LTS 上測試過。

這是一個簡單的 Job 類別的建議,它可以使用共享記憶體功能在項目佇列上執行任務列表。

class Job
{
const
CACHE_PREFIX = 'SHM/',
WORKER_NUMBER = 10;

private
$_queues = [],
$_tasks = [],
$_pids = [],
$_workerNumber,
$_cacheHandler,
$_prefix,
$_id;

public function __construct(array $queue, $workerNumber = self::WORKER_NUMBER)
{
$count = count($queue);

$length = ceil($count / $workerNumber);

$this->_queues = array_chunk($queue, $length);

$this->setWorkerNumber($workerNumber);

$this->_prefix = self::CACHE_PREFIX . microtime(true) . '/';
}

public function setWorkerNumber($workerNumber)
{
$this->_workerNumber = $workerNumber;
}

public function __get($key)
{
return apc_fetch($this->_prefix . $key);
}

public function __set($key, $value)
{
apc_store($this->_prefix . $key, $value);
}

public function add(Closure $task)
{
$this->_tasks[] = $task->bindTo($this, $this);

return $this;
}

public function run(Closure $task = null)
{
if (isset($task))
{
$this->add($task);
}

$i = 0;

do
{
$queue = $this->_queues[$i++];

$pid = pcntl_fork();

$this->_id = $i;

if ($pid === -1)
{
die("無法 fork!");
}
elseif ($pid !== 0) // 主程序
{
$this->_pids[$pid] = $pid;
}
else // 子程序
{
foreach($this->_tasks as $task)
{
$task($queue);
}

exit(0);
}
}
while($i < $this->_workerNumber);

do // 主程序
{
$pid = pcntl_wait($status);

unset($this->_pids[$pid]);
}
while(count($this->_pids));
}
}

$driver = new mysqli(':host', ':user', ':pwd', ':db');

$query = 'SELECT * FROM :table LIMIT :n';

if (false !== ($res = $driver->query($query)))
{
$resultSet = [];

while($row = mysqli_fetch_assoc($res))
{
$resultSet[] = $row;
}

$job = new Job($resultSet);

$job->test = [];

$job->run(function($queue = [])
{
// 任務

foreach($queue as $value)
{
$test = $this->test;

$value['workedId'] = $this->_id;

// ...

$test[] = $value;

$this->test = $test;
}
});

print_r($job->test);
}
Tony
15 年前
如果您想在將您的 php 頁面返回給使用者後執行某些程式碼。請嘗試以下方法 -

<?php
function index()
{
function
shutdown() {
posix_kill(posix_getpid(), SIGHUP);
}

// 執行一些初始處理

echo("Hello World");

// 切換到守護程序模式。

if ($pid = pcntl_fork())
return;
// 父程序

ob_end_clean(); // 捨棄輸出緩衝區並關閉

fclose(STDIN); // 關閉所有的標準
fclose(STDOUT); // 檔案描述符,因為我們
fclose(STDERR); // 以守護程序執行。

register_shutdown_function('shutdown');

if (
posix_setsid() < 0)
return;

if (
$pid = pcntl_fork())
return;
// 父程序

// 現在以守護程序執行。此程序甚至會在 apachectl 停止後繼續存在。

sleep(10);

$fp = fopen("/tmp/sdf123", "w");
fprintf($fp, "PID = %s\n", posix_getpid());
fclose($fp);

return;
}
?>
KrazyBox
14 年前
關於 fork 程序時如何處理檔案描述符,有很多問題。

請記住,fork() 會複製程序,這表示會複製所有描述符。不幸的是,對於 PHP 程序來說,這是一個相當糟糕的情況,因為大多數描述符都是由 PHP 或 PHP 擴充功能在內部處理的。

解決此問題的簡單且可能「正確」的方法是預先 fork,實際上應該沒有必要在程序中的許多不同點進行 fork,您只需 fork,然後委派工作。使用主程序/工作程序階層。

例如,如果您需要讓許多程序使用 MySQL 連接,只需在建立連接之前 fork,這樣每個子程序都有自己的 mysql 連接,並且由它單獨管理。

透過小心且正確的使用,fork() 可能是一個非常強大的工具。

--請記得好好照顧您的子程序。
qu4qu426com123 at gmail dot com
2 年前
如果您想將子程序的結果從基於 socket 的子程序讀取到父執行緒,這是一個簡單的解決方案。
希望這對某些人有所幫助。

<?php
function fork_process(...$callbacks)
{
$callbackSocket = [];
$callbacks = array_filter($callbacks, function ($callback) {
return
$callback instanceof \Closure;
});
/* 在 Windows 上需要使用 AF_INET */
$domain = (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN' ? AF_INET : AF_UNIX);
$pids = [];
foreach (
$callbacks as $index => $callback) {
$setUpCallbackSocket = socket_create_pair($domain, SOCK_STREAM, 0, $callbackSocket[$index]);
if (!
$setUpCallbackSocket) {
throw new
\Exception(
"socket_create_pair 失敗。原因: " . socket_strerror(socket_last_error()),
1
);
}
$pid = pcntl_fork();
if (
$pid == 0) {
try {
$returnValue = $callback();
// 宣告 $sendingData 的結尾為換行符號,以防止 socket_read 阻塞
$sendingData = serialize($returnValue) . "\0\r\n";
$bufferLength = mb_strlen($sendingData, '8bit');
// 如果不宣告 $bufferLength,它會被靜默截斷為 SO_SNDBUF 的長度
// @see https://php.dev.org.tw/manual/en/function.socket-write.php
// @see https://php.dev.org.tw/manual/en/function.socket-get-option.php
socket_write($callbackSocket[$index][0], $sendingData, $bufferLength);
socket_close($callbackSocket[$index][0]);
} finally {
// 明確地終止程序,否則會關閉與父程序共享的資源
posix_kill(getmypid(), SIGTERM);
exit();
}
} else {
$pids[$index] = $pid;
}
}
$results = [];
foreach (
$pids as $index => $pid) {
pcntl_waitpid($pid, $status);
$msg = "";
while (
$resp = socket_read($callbackSocket[$index][1], 102400)) {
$msg .= $resp;
// 防止 socket_read 阻塞
if ($msg[-1] === "\n" && $msg[-2] === "\r" && $msg[-3] === "\0") {
break;
}
}
socket_close($callbackSocket[$index][1]);
$results[] = unserialize(trim($msg));
}
return
$results;
}

$time = microtime(true);
$resp = fork_process(
function () {
sleep(1);
return
'first';
},
function () {
sleep(2);
return
'second';
},
);
echo (
microtime(true) - $time);
// 2.0448851585388
echo implode(' ', $resp);
// first second
?>
啟發自 https://stackoverflow.com/questions/8707339/sharing-variables-between-child-processes-in-php
duerra at yahoo dot com
14 年前
在某些情況下,使用 pcntl_fork() 可能會有點棘手。對於快速完成的任務,子程序可能會在父程序執行與啟動程序相關的某些程式碼之前就完成處理。父程序可能會在準備好處理子程序的狀態之前就收到訊號。為了處理這種情況,我會在訊號處理器中將 ID 新增到需要清理的程序「佇列」中,以防父程序尚未準備好處理它們。

我包含了簡化的任務守護程序版本,應該可以讓使用者步入正軌。

<?php
declare(ticks=1);
//一個非常基礎的工作排程精靈,您可以根據您的需求進行擴展。
class JobDaemon{

public
$maxProcesses = 25;
protected
$jobsStarted = 0;
protected
$currentJobs = array();
protected
$signalQueue=array();
protected
$parentPID;

public function
__construct(){
echo
"建構中 \n";
$this->parentPID = getmypid();
pcntl_signal(SIGCHLD, array($this, "childSignalHandler"));
}

/**
* 執行排程精靈
*/
public function run(){
echo
"執行中 \n";
for(
$i=0; $i<10000; $i++){
$jobID = rand(0,10000000000000);

while(
count($this->currentJobs) >= $this->maxProcesses){
echo
"已達到允許的最大子程序數量,等待中...\n";
sleep(1);
}

$launched = $this->launchJob($jobID);
}

//等待子程序完成後再結束
while(count($this->currentJobs)){
echo
"等待目前工作完成... \n";
sleep(1);
}
}

/**
* 從工作佇列啟動工作
*/
protected function launchJob($jobID){
$pid = pcntl_fork();
if(
$pid == -1){
//啟動工作時發生問題
error_log('無法啟動新工作,正在退出');
return
false;
}
else if (
$pid){
// 父程序
// 如果子程序執行速度夠快,您有時可能會在執行此程式碼之前收到 childSignalHandler 函式的訊號!
//
$this->currentJobs[$pid] = $jobID;

// 如果在我們到達這裡之前,已捕獲此 pid 的訊號,它將在我們的 signalQueue 陣列中
// 因此,讓我們現在就處理它,就像我們剛收到訊號一樣
if(isset($this->signalQueue[$pid])){
echo
"在訊號佇列中找到 $pid,正在處理它 \n";
$this->childSignalHandler(SIGCHLD, $pid, $this->signalQueue[$pid]);
unset(
$this->signalQueue[$pid]);
}
}
else{
//已分叉的子程序,執行您的工作....
$exitStatus = 0; //如果需要,則為錯誤代碼或其他
echo "在 pid ".getmypid()." 中執行一些有趣的事情\n";
exit(
$exitStatus);
}
return
true;
}

public function
childSignalHandler($signo, $pid=null, $status=null){

//如果沒有提供 pid,則表示我們正在從系統接收訊號。讓我們找出哪個子程序結束
if(!$pid){
$pid = pcntl_waitpid(-1, $status, WNOHANG);
}

//確保我們取得所有已結束的子程序
while($pid > 0){
if(
$pid && isset($this->currentJobs[$pid])){
$exitCode = pcntl_wexitstatus($status);
if(
$exitCode != 0){
echo
"$pid 以狀態 ".$exitCode." 結束\n";
}
unset(
$this->currentJobs[$pid]);
}
else if(
$pid){
//糟糕,我們的工作在父程序甚至還沒注意到它已啟動之前就已完成!
//讓我們記錄下來,並在父程序準備好時處理它
echo "..... 將 $pid 新增至訊號佇列 ..... \n";
$this->signalQueue[$pid] = $status;
}
$pid = pcntl_waitpid(-1, $status, WNOHANG);
}
return
true;
}
}
somebody
14 年前
在學術範例以外的腳本中使用 fork 時,您應該 _非常_ 小心,
或者除非您非常了解它的限制,否則最好完全避免使用它。

問題在於它只是分叉了整個 php 程序,不僅包括
腳本的狀態,還包括任何已載入擴充功能的內部狀態。
這表示所有記憶體都會被複製,但所有檔案描述符都會在
父程序和子程序之間共用。
如果某些擴充功能在內部維護
檔案描述符,這可能會造成嚴重的破壞。
最主要的例子當然是 mysql,但這可能是任何維護
開啟的檔案或網路 Socket 的擴充功能。
此外,僅在父程序或子程序中重新開啟您的連線並不是安全的方法,
因為當舊的連線資源被銷毀時,擴充功能
可能不僅僅是關閉它,例如,還會向伺服器發送登出的請求,
導致連線無法使用。
例如,當 php 結束時,mysql 會發生這種情況 - 在以下腳本中,查詢總是會失敗,並顯示「MySQL server has gone away」(MySQL 伺服器已消失)

<?php
mysql_connect
(/* 在此輸入一個可運作的伺服器? */);
if(
pcntl_fork()) die(); // 分叉一個子程序並讓父程序終止
//if(pcntl_fork()) posix_kill(getmypid(),9); // 可以運作,但非常醜陋
$r=mysql_query("select 1;");
if(!
$r)die(mysql_error()."\n");
?>
(有人建議程序使用 SIGKILL 終止自身,以避免關機時進行任何清理)

(唯一安全的方法是在分叉後關閉所有連線並重新開啟它們,如果擴充功能在內部保持開啟狀態,即使這樣也可能無法實現)

如需示範 fork 可能造成的嚴重破壞,請嘗試以下腳本。
它會開啟一個 mysql 連線,然後分叉,並從父程序和子程序執行查詢,
驗證它是否收到正確的結果。
執行它 (最好在 cli 上) 幾次,您會發現各種可能
結果
- 非常常見的情況是它只是掛起,不再輸出任何內容
- 伺服器也很常會關閉連線,可能是因為它
收到它無法處理的交錯請求。
- 有時一個程序會得到另一個程序的結果
查詢!(因為兩者都透過同一個 Socket 發送查詢,
而誰收到回覆純粹是運氣)

<?php
mysql_connect
(/* 在此處輸入可用的伺服器,好嗎? */);
$f=pcntl_fork();
while(
true){
sleep(rand(0,10)/100);
$r=mysql_query("select $f;");
if(!
$r)die($f.": ".mysql_error()."\n");
list(
$x)=mysql_fetch_array($r);
echo (
$f)?".":"-";
if(
$x!=$f) echo ($f.": 失敗: $x!=$f\n ");
}
?>
arnold at helderhosting dot nl
19 年前
當 PHP 作為 Apache 模組使用時,無法使用 'pcntl_fork' 函數。您只能在 CGI 模式或從命令列使用 pcntl_fork。

使用此函數將導致:'Fatal error: Call to undefined function: pcntl_fork()'(致命錯誤:呼叫未定義的函數:pcntl_fork())
simon dot riget at gmail dot com
6 年前
請注意,如果函數在 php.ini 中被禁用,pcntl_fork 可能會返回 NULL(而不是 -1)。
<?php
$pid
= pcntl_fork();
if (
$pid < 0 || $pid === null )
die (
"無法 fork 處理程序");
?>
jrm456 at speed dot 1s dot fr
9 年前
當 PHP 作為 Apache 模組執行時,無法使用 pcntl_fork() 的解決方案

function background_job($program, $args)
{
# 當 PHP 作為 apache 模組執行時,以下方法無效
/*
$pid = pcntl_fork();
pcntl_signal(SIGCHLD, SIG_IGN);

if ($pid == 0)
{
posix_setsid();
pcntl_exec($program, $args, $_ENV);
exit(0);
}
*/

# 解決方法
$args = join(' ', array_map('escapeshellarg', $args));
exec("$program $args 2>/dev/null >&- /dev/null &");
}
cyrus at hirvi dot com
1 年前
這是 `kenneth at fellowrock dot com` 的 fork_process 函數的 APCu 替代方案(取代共享記憶體)https://php.dev.org.tw/manual/en/function.pcntl-fork.php#115855

<?php
// 用法
$namespace = 'some_namespace';
$processes = [
function () {
sleep(1);
return
'Hello';
},
function () {
sleep(1);
return
' world!';
},
];
$callback = function ($result) {
echo
$result[0] . $result[1];
};
threadize($namespace, $processes, $callback);

function
threadize(string $namespace, $processes, callable $callback)
{
apcu_store($namespace . '_monitor', 0, 86400 * 3);
for (
$i = 0; $i < count($processes); $i++) {
$pid = pcntl_fork();
if (!
$pid) {
if (
$i == 1) {
usleep(100000);
}
apcu_store($namespace . '_process_result_' . $i, $processes[$i](), 86400 * 3);
apcu_inc($namespace . '_monitor');
exit(
0);
}
}
while (
pcntl_waitpid(0, $status) != -1) {
if (
apcu_fetch($namespace . '_monitor') === count($processes)) {
$result = [];
for (
$i = 0; $i < count($processes); $i++) {
$result[$i] = apcu_fetch($namespace . '_process_result_' . $i);
apcu_delete($namespace . '_process_result_' . $i);
}
$callback($result);
apcu_delete($namespace . '_monitor');
}
}
}
?>
John Nicholls
10 年前
https://php.dev.org.tw/manual/en/function.posix-setsid.php 上的說明描述了當子程序啟動時,如何透過簡單呼叫 posix_setsid() 來避免累積無數的殭屍程序。
williamdes at wdes dot fr
2 年前
<?php

declare(strict_types = 1);

/**
* 輔助方法,用於執行任務
*/
function execute_task(int $task_id): void
{
echo
'開始任務:' . $task_id . PHP_EOL;

// 模擬實際工作,使用 sleep()
$execution_time = rand(5, 10);
sleep($execution_time);

echo
"完成任務:${task_id}。耗時 ${execution_time} 秒。\n";
}

/**
* 建立任務列表
*/
function generator(): Generator
{
$item_count = 50;
for (
$i = 1; $i <= $item_count; $i++) {
yield
$i;
}
}

/**
* 開始工作
*/
function launch(): void
{
$processCount = 0;
$status = null;
echo
'以 PID 執行:' . getmypid() . PHP_EOL;

$taskList = generator();
do {
echo
'仍有任務待執行' . PHP_EOL;

if (
$processCount >= 5) {
echo
'等待中,目前執行數:' . $processCount . PHP_EOL;
pcntl_wait($status);
$processCount--;
continue;
}

echo
'執行中的任務數:' . $processCount . PHP_EOL;
$task = $taskList->current();
$taskList->next();
$processCount++;

$pid = pcntl_fork();

if (
$pid === -1) {
exit(
'Fork 時發生錯誤...' . PHP_EOL);
}

if (
$pid === 0) {
$processList[] = getmypid();
echo
'以 PID 執行任務:' . getmypid() . PHP_EOL;
execute_task($task);
exit();
}
} while (
$taskList->valid());

$waitForChildrenToFinish = true;
while (
$waitForChildrenToFinish) {
// 這個 while 迴圈會讓父進程等待,直到所有子執行緒完成,
// 屆時腳本才會繼續執行。
if (pcntl_waitpid(0, $status) === -1) {
echo
'平行執行已完成' . PHP_EOL;
$waitForChildrenToFinish = false;
}
}

// 在所有任務處理完成後執行的程式碼
}

launch();
nmmm at nmmm dot nu
13 年前
讓我抓狂的是,即使我使用以下方式啟動腳本,它在登出後幾個小時就被終止了:

php server.php >& logfile.txt

看起來 PHP 會與標準輸入互動,即使我沒有使用它。

解決方案是使用 nohup 啟動:

nohup php server.php >& logfile.txt

或者將它守護進程化/以守護進程身份執行(例如 fork() 並關閉檔案描述符)。
iulian
14 年前
當使用 fork 在使用 MySQL 的單個作業佇列上執行多個子進程時,我使用 mysql_affected_rows() 來防止工作者之間的衝突。

首先,我找到一個「空閒」的作業:
SELECT job_id FROM queue WHERE status="free"

然後我更新佇列:
UPDATE queue SET worker_id={$worker_id} WHERE job_id={$job_id}

然後我查看該行是否已變更:

<?php
if(mysql_affected_rows() == 0)
{
// 該行未變更,因此一定表示另一個工作者已聲明該作業,因此我回到「尋找空閒作業」查詢
}
else
{
// 執行作業
}
?>
drrota at us dot ibm dot com
15 年前
我能夠解決從 Apache PHP 無法執行 fork 和 exec 的問題。

我透過在 Linux 上呼叫系統 'at' 命令來解決這個問題。「at run something now」。你必須在 crontab 檔案中設定 atrun -s(每分鐘執行一次),以確保即使機器負載過重,事情也能快速啟動。

如果你是 Linux 機器上唯一執行批次作業的人,這會有效。
ben at gelbnet dot com
22 年前
我正在編寫一個 shell 腳本以從使用者取得輸入,但是,如果使用者沒有輸入足夠的資料,我需要我的腳本在一定的秒數後逾時。下面的程式碼描述了我使用的方法。它有點複雜,但它確實有效。

-Ben

#!/home/ben/php/bin/php -q
<?php
// 全域變數
$RETURN_CHAR = "\n";
$TIMEOUT = 5; // 輸入逾時的秒數
$PID = getmypid();
$CHILD_PID = 0;

// 確保程式執行不會逾時
set_time_limit(0);

function
set_timeout() {
global
$PID;
global
$CHILD_PID;
global
$TIMEOUT;

$CHILD_PID = pcntl_fork();
if(
$CHILD_PID == 0) {
sleep($TIMEOUT);
posix_kill($PID, SIGTERM);
exit;
}
}

function
clear_timeout() {
global
$CHILD_PID;
posix_kill($CHILD_PID, SIGTERM);
}

// read_data()
// 從 STDIN 取得一行資料並傳回
function read_data() {

$in = fopen("php://stdin", "r");
set_timeout();
$in_string = fgets($in, 255);
clear_timeout();
fclose($in);
return
$in_string;
}

// write_data($outstring)
// 將資料寫入 STDOUT
function write_data($outstring) {
$out = fopen("php://stdout", "w");
fwrite($out, $outstring);
fclose($out);
}

while(
1) {
write_data("說些什麼->");
$input = read_data();
write_data($RETURN_CHAR.$input);
}

?>
kentmussell at mindspring dot com
17 年前
這是我寫的一個有趣的腳本。它示範了如何將 pcntl_fork() 作為一個有用的工具來使用。

<?php
/* 這個腳本的目的是測試一個演算法,該演算法設計用於:
a.) 比較密碼雜湊值,或者有效率地嘗試密碼,其中嘗試單一密碼的時間為 10 秒。
b.) 產生執行緒以同時比較雜湊值。
c.) 限制一次開啟的執行緒數量。
*/
//檢查是否可整除
function divby($num,$den) {
$result = $num/$den;
$result2 = floor($result);
if (
$result == $result2) {
return
true;
}
else {
return
false;
}
}
//檢查一段時間是否符合每 10 秒發生一次的 2 秒間隔。間隔大小可能會增加或減少,以使用更多或更少的記憶體。
function goodTime($elapsed) {
$num = floor($elapsed);
$num = $num/12;
$min = floor($num);
$min = 12*$min;
$max = $min+2;
if (
$elapsed >= $min && $elapsed <= $max) {
return
"yes";
}
else {
return
"no";
}
}

$x = 30; //子執行緒的數量
$pid = 1; //需要建立第一個執行緒
$xpass = md5('29');//要破解的雜湊值
$time = time();
$i = 1;
//父程序產生 $x 個子程序。
while ($i <= $x) {
if (
file_exists('childcall.txt')) {
unlink('childcall.txt');
exit;
}
$elapsed = time()-$time;
//只有在每 10 秒發生的間隔期間才會產生子程序,這樣有足夠的時間讓前一批子程序完成其任務。
if (goodTime($elapsed)=="yes") {
//我們是父程序嗎?
if ($pid != 0) {
//產生一個子程序。
$pid = pcntl_fork();
//建立一個記錄,記錄已產生多少個子程序。
$arr[$i] = $i;
$time2 = $elapsed;
}
//將子程序導出迴圈。
if ($pid == 0) {
$i = $x+1;
}
$i++;
}
}
//父程序等待子程序完成執行。
if ($pid) {
$value = 1;
while (!
file_exists('childcall.txt')) {
//等待
}
unlink('childcall.txt');
$time = time()+2;
while (
time()<$time) {
//等待
}
exit;
}
//子程序輪流找出最高的陣列值,並將其變更為 0
rsort($arr);
$value = max($arr);
$arr[$value] = 0;
$time = time()+10;
//模擬延遲
while (time() < $time) {
//等待
}
//將高陣列值的雜湊值與我們想要破解的雜湊值進行比較。
if (md5($value) == $xpass) {
echo
"$value \n";
}
if (
$value == $x || md5($value) == $xpass) {
$file = "childcall.txt";
$content = true;
file_put_contents($file,$contents);
}
?>
匿名
15 年前
關於資料庫連線,可以使用 kill 9 或 sleep 來處理,真正的問題是如果兩個執行緒同時發出資料庫查詢,PHP 會開始出現隨機的資料庫錯誤,而這些錯誤不一定清楚地指出問題所在。

您應該為每個執行緒建立一個單獨的連線。
php at mx dot magic-lamp dot org
15 年前
MySQL「查詢期間失去連線」的變通方法,或任何由子程序退出引起的其他物件相關問題,是強制子程序以 kill -9 自行終止,從而避免任何清理。當然,這不太優雅,但確實有效。

<?php
$pid
= pcntl_fork();
if (
$pid == 0 ) {
// 這是子程序。在這裡執行一些操作。
// 我們使用 posix_kill() 而不是呼叫 exit()
posix_kill(getmypid(),9);
}
?>

請注意不要產生過多的程序,因為這會產生自己的問題。
xuecan at google dot com
18 年前
我認為這段簡單的程式碼可以幫助了解 fork 的運作方式

<?php
echo "posix_getpid()=".posix_getpid().", posix_getppid()=".posix_getppid()."\n";

$pid = pcntl_fork();
if (
$pid == -1) die("無法 fork");
if (
$pid) {
echo
"pid=".$pid.", posix_getpid()=".posix_getpid().", posix_getppid()=".posix_getppid()."\n";
} else {
echo
"pid=".$pid.", posix_getpid()=".posix_getpid().", posix_getppid()=".posix_getppid()."\n";
}
?>
To Top