看到很多PHP程序員職業規划的文章,都是直接上來就提Linux、PHP、MySQL、Nginx、Redis、Memcache、jQuery這些,然后就直接上手搭環境、做項目,中級就是學習各種PHP框架和類庫,高級階段就是MySQL優化、PHP內核與擴展、架構設計這些了。
這些文章都存在一個嚴重的缺陷,不重視基礎。就好比練武功,只求速成,不修煉內功和心法,只練各種招式,這樣能高到哪里去?我所見過的PHP大牛每一個都是具備非常扎實的基礎,他們之所以能成為大牛,是因為基礎足夠好。基礎不穩,面對技術復雜的系統,如同盲人摸象、管中窺豹,只得其門不得其法。而且如果基礎不扎實,也沒辦法進入大公司。國外的Google、Facebook,國內的騰訊、阿里、百度、滴滴、京東、新浪等知名互聯網企業,無論哪一家公司面試必然會考驗應聘者的技術功底。無法進入一個擁有大規模並發請求的項目中得到歷練,不堅持提升自己,那也只能在小公司混日子了。
我最開始工作也是在2家小公司,后來加入騰訊阿里,主要原因還是我堅持學習基礎知識,從而得倒了這個機會。有幾個方面的基礎知識,我建議每一位PHP程序員都應該好好學習一下。我推薦幾本書給大家,包括深入理解計算機系統、現代操作系統、C程序設計語言、C語言數據結構和算法、Unix環境高級編程、TCP/IP網絡通信詳解。另外我建議大家學習一下面向對象方面知識,PHP這方面的書不太多,建議看Java面向對象編程、Java編程思想、J2EE這些書。PHP語言基礎方面,建議認真地把PHP5權威編程這本書好好讀完。另外不光要讀,還要照着書中的講解動手去編程實踐。
總之有一個好的基礎,再去學LAMP、Redis、PHP框架、前端,這樣取得的成就更大。這與年齡無關、與學歷無關、與智力無關,與天賦也無關。只要肯努力學習,人人可以成為技術大牛。
PHP+Swoole的閉包寫法
JS程序員總是嘲笑PHP沒有閉包,今天抽空寫一篇文章來專門介紹一下PHP的閉包。從5.3版本開始PHP就增加了匿名函數支持,經過數個版本迭代到現在的PHP5.6、PHP7,PHP語言的閉包已經非常完善了。再結合Swoole提供的事件驅動支持,PHP的閉包功能非常強大而且很優雅。
匿名函數
匿名函數是閉包的核心,匿名函數在PHP里實際上是一個Closure類的對象(請注意是對象)。與普通的面向對象編程方式不同,匿名函數的代碼是直接寫在調用處的,不需要額外寫一個類,編寫方法的代碼。這樣的好處就是更直接。下面的示例是設置一個定時器,每2秒輸出hello world。
傳統寫法
function timer () { echo "hello world"; } Swoole\Timer::tick(2000, 'timer');
閉包寫法
Swoole\Timer::tick(2000, function () { echo "hello world"; });
非閉包的傳統寫法,先要聲明一個函數,再轉入函數名稱字符串。兩段代碼是分離的,不夠直觀。而閉包的寫法把定時器的聲明和定時器要執行的代碼寫在了一起,邏輯非常清晰直觀。使用閉包語法可以很方便編寫回調函數。在事件驅動編程、排序、array_walk等需要用戶傳入一段執行代碼的場景中,閉包的寫法非常優雅。
閉包更強大的地方在於它可以直接在調用處引入外部變量。PHP中實現的方法就是use關鍵詞。
Use語法
如果剛才的定時器需要傳入一個變量,傳統的寫法只能通過全局變量來實現。與JS不同,PHP的變量引入是顯式的,如果要引用外部變量必須使用use來聲明。而JS是隱式的,匿名函數內部可以隨意操作外部變量,無需聲明。這樣好處是少寫了一點代碼,缺點是存在風險和混亂。
傳統寫法
$str = "hello world"; function timer () { global $str; echo $str; } Swoole\Timer::tick(2000, 'timer');
閉包寫法
$str = "hello world"; Swoole\Timer::tick(2000, function () use ($str) { echo $str; });
閉包寫法使用use直接引入了當前的$str變量,而不需要使用global全局變量。另外如果是在swoole的事件驅動編程模式,使用global就無法實現異步並發了,因為global全局變量只有1個,如果同時有多個客戶端請求,每個請求要查詢數據庫,輸出不同的內容,傳統的編程方法就不太容易實現,需要使用全局變量數組,以客戶端的ID為KEY保存各自的數據。
傳統寫法
$requestArray = array(); $dbResultArray = array(); function my_request($request, $response) { global $dbResultArray, $requestArray; $queryId = $db->query($sql, 'get_result'); $requestArray[$request->fd] = array($request, $response); $dbResultArray[$queryId] = $request->fd; } function get_result($queryId, $queryResult) { global $dbResultArray, $requestArray; list($request, $response) = $requestArray[$dbResultArray[$queryId]]; $response->end($queryResult); } $server->on('request', 'my_request');
閉包寫法
$server->on('request', function ($request, $response) { $queryId = $db->query($sql, function ($queryId, $queryResult) use ($request, $response) { $response->end($queryResult); }); });
傳統的寫法非常復雜,需要反復多次從全局數組保存/提取數據。而閉包的寫法非常簡潔優雅,只用了幾行代碼就實現了同樣的功能。閉包寫法非常適合用來編寫異步非阻塞回調模式的服務器程序。目前熱門的編程語言中只有PHP和JS具備這種能力。
閉包更多特性
在類的方法中使用匿名函數,5.4以上的版本無需使用use引入$this,直接可以在匿名函數中使用$this來調用當前對象的方法。在swoole編程中,可以利用此特性減少$serv對象的use引入傳遞。
class Server extends Swoole\Server { function onReceive($serv, $fd, $reactorId, $data) { $db->query($sql, function ($queryId, $queryResult) use ($fd) { $this->send($fd, $queryResult); } } }
另外如果希望在閉包函數中修改外部變量,可以在use時為變量增加&引用符號即可。注意對象類型不需要加&,因為在PHP中對象默認就是傳引用而非傳值。
基於swoole的task功能實現程序內的map-reduce
Swoole擴展自帶的Task進程功能非常強大,可以用來實現各種復雜的業務邏輯。本文主要介紹使用task/finish功能實現程序內的Map-Reduce並發任務處理。一個聊天服務經常會有群聊需求,我的群組和群組內成員,另外群組內成員需要按照積分排序,類似與這樣的功能就可以使用Swoole簡單實現。
傳統多線程方案
創建2個全局變量Map,group_map以group_id為Key,存儲成員set。user_map以uid為Key存儲當前用戶加入的所有group。
多線程環境下實際上不能直接操作這2個Map,必須要加鎖。當添加用戶到一個組或者用戶退出一個組時需要操作這2個map,必須要加鎖。如果操作很頻繁,實際上鎖的碰撞是很嚴重的,這部分操作就會變成串行的。同時只有一個線程可以對map進行操作。鎖的爭搶也會帶來大量線程切換浪費很多CPU資源。
lock.lock(); group_map[group_id].append([uid, score]); user_map[uid].append(group_id); group_map.sortByScore(); lock.unlock();
基於Swoole的Task功能
基於Swoole的Task功能,可以將任務切片,然后hash投遞到不同的Task進程,完成任務。排序功能可以直接使用PHP提供的SplHeap實現,時間復雜度為O(logn),如果要實現查詢功能,如根據UID查詢用戶加入的所有群組,根據GroupId查詢有哪些成員。可以先計算Hash找到對應Task進程,然后通過task/taskwait發送指令,直接讀取進程的變量查找到信息。
$serv->set(array("task_worker_num" => 24)); $serv->task(array("cmd" => "user", "uid" => $uid, "gid" => $gid, "score" => $score), $gid % $task_worker_num); $serv->task(array("cmd" => "group", "uid" => $uid, "gid" => $gid), $uid % $task_worker_num); class MyMaxHeap extends SplHeap { public function compare($value1, $value2) { return ($value1['score'] - $value2['score']); } } function onTask($serv, $taskId, $srcWorkerId, $data) { static $userMap = array(); static $groupMap = array(); if ($data['cmd'] == 'group') { if (!isset($groupMap[$data['gid']])) { $groupMap[$data['gid']] = new MyMaxHeap(); } $heap = $groupMap[$data['gid']]; $heap->insert(array("uid" => $data['uid'], "score" => $data['score'])); } elseif ($data['cmd'] == 'user') { $userMap[$data['uid']][] = $data['gid']; } }
由於Task進程只有數組操作,所以是非阻塞的,只需要開啟與CPU核數相同的進程數量即可。進程間無任何加鎖爭搶,性能非常好。Swoole的Task進程通信使用UnixSocket,是內核提供的全內存通信方式無任何IO,一寫一讀單進程可達100萬/秒。雖然沒有直接讀變量的速度快,但性能也足夠了。