併發

葡萄酒不吐葡萄皮發表於2019-02-16

面試中有一個問題沒有很好的回答出來,題目為:併發3個http請求,只要其中一個請求有結果,就返回,並中斷其他兩個。

當時考慮的內容有些偏離題目原意, 一直在考慮如何中斷http請求,大概是在 client->recv() 之前去判斷結果是否已經產生,所以回答的是用 socket 去傳送一個 http 請求,把 socket 加入 libevent 迴圈監聽,在callback中判斷是否已經得到結果,如果已經得到結果,就直接 return。

後來自己越說越覺得不對,既然已經recv到結果,就不能算是中斷http請求。何況自己從來沒用過libevent。後來說了還說了兩種實現,一個是用 curl_multi_init, 另一個是用golang實現併發。
golang的版本當時忘了close的用法,結果並不太符合題意。

這題沒答上來,考官也沒為難我。但是心裡一直在考慮,直到面試完走到樓下有點明白什麼意思了,可能考的是併發,程式執行緒的應用。所以總結了這篇文章,來講講PHP中的併發。本文大約總結了PHP程式設計中的五種併發方式,最後的Golang的實現純屬無聊,可以無視。如果有空,會再補充一個libevent的版本。
curl_multi_init

文件中說的是 Allows the processing of multiple cURL handles asynchronously. 確實是非同步。這裡需要理解的是select這個方法,文件中是這麼解釋的Blocks until there is activity on any of the curl_multi connections.。瞭解一下常見的非同步模型就應該能理解,select, epoll,都很有名,這裡引用一篇非常好的文章,有興趣看下解釋吧。

<?php
// build the individual requests as above, but do not execute them
$ch_1 = curl_init(`http://www.baidu.com/`);
$ch_2 = curl_init(`http://www.baidu.com/`);
curl_setopt($ch_1, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch_2, CURLOPT_RETURNTRANSFER, true);

// build the multi-curl handle, adding both $ch
$mh = curl_multi_init();
curl_multi_add_handle($mh, $ch_1);
curl_multi_add_handle($mh, $ch_2);

// execute all queries simultaneously, and continue when all are complete
$running = null;
do {
curl_multi_exec($mh, $running);
$ch = curl_multi_select($mh);
if($ch !== 0){

   $info = curl_multi_info_read($mh);
   if($info){
       var_dump($info);
       $response_1 = curl_multi_getcontent($info[`handle`]);
       echo "$response_1 
";
       break;
   }

}
} while ($running > 0);

//close the handles
curl_multi_remove_handle($mh, $ch_1);
curl_multi_remove_handle($mh, $ch_2);
curl_multi_close($mh);

這裡我設定的是,select得到結果,就退出迴圈,並且刪除 curl resource, 從而達到取消http請求的目的。
swoole_client

swoole_client提供了非同步模式,我竟然把這個忘了。這裡的sleep方法需要swoole版本大於等於1.7.21, 我還沒升到這個版本,所以直接exit也可以。

<?php
$client = new swoole_client(SWOOLE_SOCK_TCP, SWOOLE_SOCK_ASYNC);
//設定事件回撥函式
$client->on(“connect”, function($cli) {

$req = "GET / HTTP/1.1

Host: www.baidu.com

Connection: keep-alive

Cache-Control: no-cache

Pragma: no-cache

";

for ($i=0; $i < 3; $i++) {
    $cli->send($req);
}

});
$client->on(“receive”, function($cli, $data){

echo "Received: ".$data."
";
exit(0);
$cli->sleep(); // swoole >= 1.7.21

});
$client->on(“error”, function($cli){

echo "Connect failed
";

});
$client->on(“close”, function($cli){

echo "Connection close
";

});
//發起網路連線
$client->connect(`183.207.95.145`, 80, 1);

process

哎,竟然忘了 swoole_process, 這裡就不用 pcntl 模組了。但是寫完發現,這其實也不算是中斷請求,而是哪個先到讀哪個,忽視後面的返回值。

<?php

$workers = [];
$worker_num = 3;//建立的程式數
$finished = false;
$lock = new swoole_lock(SWOOLE_MUTEX);

for($i=0;$i<$worker_num ; $i++){

$process = new swoole_process(`process`);
//$process->useQueue();
$pid = $process->start();
$workers[$pid] = $process;

}

foreach($workers as $pid => $process){

//子程式也會包含此事件
swoole_event_add($process->pipe, function ($pipe) use($process, $lock, &$finished) {
    $lock->lock();
    if(!$finished){
        $finished = true;
        $data = $process->read();
        echo "RECV: " . $data.PHP_EOL;
    }
    $lock->unlock();
});

}

function process(swoole_process $process){

$response = `http response`;
$process->write($response);
echo $process->pid,"	",$process->callback .PHP_EOL;

}

for($i = 0; $i < $worker_num; $i++) {

$ret = swoole_process::wait();
$pid = $ret[`pid`];
echo "Worker Exit, PID=".$pid.PHP_EOL;

}

pthreads

編譯pthreads模組時,提示php編譯時必須開啟ZTS, 所以貌似必須 thread safe 版本才能使用. wamp中多php正好是TS的,直接下了個dll, 文件中的說明覆制到對應目錄,就在win下測試了。 還沒完全理解,查到文章說 php 的 pthreads 和 POSIX pthreads是完全不一樣的。程式碼有些爛,還需要多看看文件,體會一下。

<?php
class Foo extends Stackable {

public $url;
public $response = null;
public function __construct(){
    $this->url = `http://www.baidu.com`;
}
public function run(){}

}

class Process extends Worker {

private $text = "";
public function __construct($text,$object){
    $this->text = $text;
    $this->object = $object;
}
public function run(){
    while (is_null($this->object->response)){
        print " Thread {$this->text} is running
";
        $this->object->response = `http response`;
        sleep(1);
    }
}

}

$foo = new Foo();

$a = new Process(“A”,$foo);
$a->start();

$b = new Process(“B”,$foo);
$b->start();

echo $foo->response;

yield

yield生成的generator,可以中斷函式,並用send向 generator 傳送訊息。稍後補充協程的版本。還在學習中。
Golang

用Go實現比較簡單, 回家後查了查 close,處理一下 panic就ok了。程式碼如下:

package main

import (

"fmt"

)

func main() {

var result chan string = make(chan string, 1)
for index := 0;  index< 3; index++ {
    go doRequest(result)
}

res, ok := <-result
if ok {
    fmt.Println("received ", res)
}

}

func doRequest(result chan string) {

response := "http response"
defer func() {
    if x := recover(); x != nil {
        fmt.Println("Unable to send: %v", x)
    }
}()
result <- response
close(result)

}

上面的幾個方法,除了 curl_multi_* 貌似符合題意外(不確定,要看下原始碼),其他的方法都沒有中斷請求後recv()的操作, 如果得到response後還有後續操作,那麼是有用的,否則並沒有什麼意義。想想可能是PHP操作粒度太大, 猜測用 C/C++ 應該能解決問題。

寫的時候沒有注意到一個問題,有些方式是返回值,有些直接列印了,這樣不好,應該統一使用返回值得到請求結果。能力有限,先這樣吧。

相關文章