译者注:前阵子打算用到PHP的多线程, 但搜了一下, 才发现PHP多线程的资料少之又少。尽管PHP官方文档里有讲到多线程,但是讲太少了,连多线程在PHP中是怎么运行的也没有说。所以说要真正能够使用多线程的功能, 还有很多的概念需要理清楚,也有很多的资料需要整理。看过这篇文章你就知道, PHP的多线程跟其它语言的多线程使用方法是有比较大的区别的,所以其它语言的那一套也并不完全适用。上Github找到了这篇文章, 觉得好的地方就是对于pthreads的运行过程有所介绍,也解答了我的一些困惑。再者发现我在网上,比较少有资料讲到PHP的多线程, 更没有讲得这么详细的(其实也并没有达到我所要的那种详细程度),所以就找了个时间把它翻译了出来。当然由于原文章也比较口语化,一些比喻我也并不觉得恰当,翻译成中文就更是变了味,所以翻译后内容有一些微小的改动,但重要的都有保留。大家在看的时候关注文章要表达的总体思想,还有一些重要的概念即可,不需要对一些细节钻牛角尖。
目录
- 简介
- 运行
- 分享
- 同步
- 陷井
- 怎么回事??
前言
如果你是一个PHP程序员, 平时花比较多的时间在console上,或者对PHP的高性能编程方式比较感兴趣,这篇文章就是专为你写的。
写这篇文章有一点我想要做到的就是尽量使内容更具体更简洁,使你们能够印象深刻。同时也希望有一天,这些内容能够成为每个PHP程序员都知道的知识。
读完这篇文章,你将对pthreads为什么存在和怎么样运行有一个清晰的认识。
如果你有任何的评论,建议或者咨询,请发送到krakjoe@php.net
咨询会被忽略
简介
自从PHP4(2000年5月22号)以来,PHP就已经有能力来运行相互独立的解析器实例了,这些实例运行在不同的线程中(同一个进程),且相互之间不会有干扰。我们称这种技术是TSRM,它是PHP中几乎无处不在但又鲜有人讨论的一部分。
如果有人使用过XAMPP或者PHP windows版,那很可能你已经在不知情的情况下一直在用着带有线程功能的PHP了。
TSRM可以创建解析器的相互独立的实例,而这也正是pthreads在PHP中运行用户级(线程分内核级线程和用户级的线程)线程的方式。这些实例相互独立到什么程度呢?就好比使用Apache2 Worker MPM PHP5 Module一样,服务器接收请求时创建的实例是彼此没有关联的。
而具体TSRM是怎么工作的,这不在本文的讨论范围之内,因为这只会起到使读者困惑的作用,你需要知道的就是PHP已经在十年前就有能力运行在多线程的环境当中了,它的实现方式很稳定。但这里面也有一个大家所熟知的,但又完全被误解的陷井,针对这个陷井,我要解释,或者说阐述一下这样一个事实:PHP是第三方库的包裹者,PHP的每一部分都几乎是以这样的方式来实现的,如果有一个第三方的库并没有实现可重入(线程安全)的功能,那么由PHP包裹后的库在运行中会失效,或者导致未知的行为,比如说locale就是这样的一个例子。需要明确的是,这种情况是超出PHP或者说pthreads的控制的。好的一点是,这样子的库都会在文档中有注明,或者比较明显。实现使用中,大部分的extension都可以被用在pthreads的程序当中。
PHP团队从来都不考虑加入用户级的多线程,直到现在也是这样子。你需要知道的是,在世界上有PHP存在的地方,已有一个扩大PHP应用的方法了——那就是加硬件。PHP存在的这许多年里,硬件变得越来越便宜,于是多线程也就更少会被PHP团队考虑了。硬件在变便宜的同时,功能也变得更加强大。在今天,我们的手机还有平板几乎者有双核、四核的CPU,而且有大量的RAM。我们的手提电脑,服务器通常者有8核或16核,16G和32G的RAM了。
除了PHP团队的问题外,PHP程序员也是另一个要考虑的问题:PHP是写给非程序员的,它跟自然语言比较相似。PHP容易掌握的一个原因就是因为它是一门容易学习也容易编写的语言。而多线程编程对于很多人来讲并不是那么简单,就算有最可靠最条理清晰的API也一样,多线程有很多不同的东西要考虑,还有很多容易使人误解的概念。PHP团队不想让用户级的多线程成为PHP内核的一部分,于是多线程没有被加以更多的关注。总的来讲,PHP对于所有的人来讲,都不应该是复杂的。
但综合考虑一下,多线程的引入仍然的有好处的,因为多线程允许我们实现更多的功能,尽管对于大多数的任务来讲,使用PHP原有的功能就可以实现。
一个关于”完全没有分享”的笔记:PHP的架构是”完全没有分享”的,当服务端的PHP脚本响应一个请求时,通过任何SAPI(Server Application Programming Interface),PHP的环境(PHP运行的需要的数据结构)都会从其它的响应中完全独立开来。从表面上看,pthreads的出现好像是跟这种PHP的架构相互冲突了, 而实际上并非如此。pthreads的另一个任务就是维护PHP原来的架构方式(这对程序员是不可见的)。pthreads通过”读时复制”和”写时复制”还有mutex操作的方式来实现这一点。结果就是,任何时间,用户做任何事情——从程序的角度来讲,当你读或者写一个对象,或者运行对象里的方法时,可以认为操作是线程安全的(其它语言需要显式地使用mutex),且不需要更进一步的动作,比如说显式地使用mutex。
简介里如果有什么是你搞不清楚的,你需要再重新啄磨一下,因为这些知识贯穿了整篇文章
运行
线程的工作原理是把代码割分成一个个的运行单元,然后把这些单元分布到进程&|CPU核中,以达到提高系统运行效率的作用。
线程的数量应该控制在最少的范围之内
pthreads提供了两种运行的模型。线程模型(Thread Model)和工作者模型(Worker Model),它们大部分功能都是一样的,内部也是差不多的,只有一个重要的区别,就是以它们的视角来看, 它们的运行单元是不一样的。
一个线程(Thread)代表了一个解析器上下文(context)和一个运行单元(它的run方法)。
一个工作者(Worker)代表了一个解析器上下文,它的run方法(用于配置上下文)。在工作者模型中运行单元是Stackables中的run方法。
当程序调用了Thread::start,一个新的线程就会被创建,一个PHP解析器上下文会被初始化,然后被从原来的上下文中分离出来。然后两个上下文中的运行单元开始并发地运行。线程运行的是Thread中的run方法,至到run方法结束,该线程的上下文也会被销毁。
当程序调用了Worker::start,一个新的线程会被创建,一个PHP解析器上下文会被以相当的方式初始化并分离,当Worker中的run方法执行完时,Workder就会开始从栈中把Stackables出栈,然后以它们入栈的顺序来运行它们。如果在栈中并没有Stackables,则Worker会等待,至到有Stackables出现。Worker会一直运行,直到Worker::shutdown被调用。如果Worker::shutdown被调用的时候,还有Stackables中栈里面,则它们会被先运行,Worker这时会先阻塞,直到shutdown可以运行。
上下文资源可能会被不必要地浪费,你应该注意到,开启线程或工作者并不是没有代价的,有一个误解就是认为多线程总能使系统变得更快,然而并不是这样子的。只要有可能,应该更多地使用工作者模型而不是线程模型,工作者模型会减少多线程中的上下文资源的浪费(当然也并不是绝对)。
分享
如果多个线程不能操作同一组数据,那么多线程的意义也就不是很大了。操作同一组数据在PHP”完全没有分享”的架构中是一个需要解决的问题。然而我看到的”完全没有分享”并不是一个障碍,反而是把我推向了一个正确的方向。
程序员在写多线程程序的时候,有一个普遍性的问题,就是数据安全与同步。一个数组如果有10个线程同时对它进行操作,是很容易发生冲突的。
而”没全没有分享”正好解决了这个问题。如果两个上下文有可能操作同一个数据,那么它们也不会冲突,因为它们都在各自的栈中,这种架构方式被pthreads维护下来。
源自pthreads的对象使用了线程安全的数据存储表,拥有这个表的对象跟其它的对象有一个小小的不同,当你把数据写到这样的对象中时,表会被锁住,数据被复制然后存到表中,最后再释放表中的锁。当有一个后续的读数据操作时,表会被锁住,表中的数据被复制并返回,然后表中的锁被释放。这种方式保证了没有两个上下文有可能操作同一个物理空间————也即保证了”没全没有分享”的原则。
有一些数据本身并不轻易被复制, PHP对此有一种解决方法,就是通过序列化(serialization)API来实现。序列化被使用在非来源于pthreads的对象(或数组对象)当中。来源于pthreads的对象不会被序列化,正因为如此,你如果有需要多个上下文(即多个线程)来操作的数据,你就应该使用pthreads中的对象来存储这样的数据。
所有来源于pthreads的对象都可以被多线程操作(只要该线程有指向该对象的引用,不管是以数组的方式还是以对象的方式)。它们同时也包含了以线程安全方式操作数据的方法(methods)。不应该存在有pthreads不能实现的数据集合,而基本数据集合(数组)也是支持的。
多线程就是以尽量少的内存消耗而又保留PHP原来的架构和保证安全性的方式完成的。保证安全性看上去似乎有一些浪费,但这只付出了很少的一点代价而已,也尽量减少也内存的消耗。
同步
线程中只有分享还是不够的,最后一个问题就是同步了。这对于很多的程序员来讲,可能比较特别。
当你在运行,分享数据的时候,你同时也要可以控制什么时候运行,什么时候分享。总不能出现有操作一个还没有存在的数据的情况吧!
同步可以让一个线程进入等待状态(waiting),这个线程可以被唤醒(nofifying)。
同步一个运行单元是很简单的,但也有使用不恰当的危险。对此,我给出一个简短的解释,希望这个解释能够留在你的心中,帮助你正确地使用同步。
$this->synchronized(function(){
$this->wait();
});
上面的代码看下去很简单,但是有一个问题,那就是没有指明它在等待什么,也没有表明它被唤醒后需要做什么事情。这可能会导致它无限地等待下去。
同步的代码看上去可能会有一点奇怪,但你也应该遵守这样的代码方式。这里我解释一下为什么:当你调用::synchronized的时候,一个mutex(锁)会被获取,当你调用等待(wait())的时候,mutex会锁住,然后解锁,这样,其它的上下文就可以获取这个锁(译者注:原文是上面这个意思,但让我觉得比较费解,mutex是这样子运行的吗?我觉得应该是进入::synchronized的时候,mutex会被获取,而调用完wait()之后,mutex会被释这样的意思,当mutex被获取时,其它线程就无法获取了,只能阻塞)。
等待的代码如下:
$this->synchronized(function(){
if (!$this->data) {
$this->wait();
}
});
/* 下面的代码可以操作 $this->data 且知道它是存在的 */
唤醒的代码如下:
$that->synchronized(function($that){
$that->data = “some”;
$that->notify();
}, $that);
(译者注:等待代码中的this和唤醒代码中的that是同一个对象)
在的唤醒代码中,你保证了正在等待的上下文不一直等待下去。
陷井
PHP内核中的垃圾回收器并不适用于pthreads的这种运行方式。如果pthreads在设计的时候要遵守拉圾回收的机制的话,会导致内存的使用增大,以至于保持对我们程序的控制变得很困难。
所以我们并没有这么做。在pthreads的应用程序中,你需要对你所创建的对象负责,当你有需要被运行的对象(thread,stackables)时,或者有对象需要被其它上下文获取时,你需要保留好对象的引用,直到它们不再被使用。
pthreads避免了内存的使用超出控制的问题,但也带来了一个新的问题,那就是”段错误”。
段错误发生在我们让处理器取一段它不能取到的地址时,而这会导致处理器放弃这次运行。当你遇到段错误的时候,你首先要定位问题是,你是否引用了已经被销毁的对象。
防止段错误听起来复杂,但其实做起来并不复杂。这可以通过以下的例子来说明:
class W extends Worker {
public function run(){}
}
class S extends Stackable {
public function run(){}
}
/* 1 */
$w = new W();
/* 2 */
$j = array(
new S(), new S(), new S()
);
/* 3 */
foreach ($j as $job)
$w->stack($job);
/* 4 */
$j = array();
$w->start();
$w->shutdown();
上面的例子会一直出现段错误,第1~3步是很正常的,没什么问题,但在Worker被运行之前,被入栈的所有对象都被删除掉了(
j=array()),在调用
->start()的时候会导致段错误。你自己的代码的错误可能不会有这么明显,但如果你发现了你的代码可能会导致这样的问题,你就需要小心了。
其它出现这种错误的信息如下:
Call to a member function member() on a non-object in /my/code.php
还有这个提醒:
Trying to get property of non-object in /my/code.php
如果你出现了这样的错误,你需要仔细检查一下你的代码,保证所有你传到其它上下文的数据没有中途被销毁。
怎么回事??
我听到说有人批评说一个简单的东西(PHP),提供这样的功能(pthreads)被变得复杂化了。我想反驳说:我把一个本来复制的东西,变得相当的简单才对。
有一个事情之所以复杂,或者说困难,是不能够被评判或者避免的。任何复的杂西都会随着你知识的提高而变简单,如果不是这样,那只能说你并没有学对知识。这就是学习的自然过程。
对于这种说我没有让任何东西变得简单我只能说:真的吗?如果任务本来就简单,几步就可以完成,那实现起来也是简单的。而实际上这些你至今都觉得复杂的东西正是你应该从现在开始关注的东西!
对那些其他的爱唱反调的人:只有向前推进事情才能发展,当我们都一起向前推的时候,我们就能取得更多的成就了!
就算你讨厌这种观点,我希望我已经说了足够多来使你来尝试一下,而不是说现在就形成了这样子的一种偏见,而这种偏见可能会影响你以后的决定。你说是前者好还是后者好呢!?
原文地址:Multi-Threading in PHP with pthreads
关于pthreads的其它资料和问题可以参考:
pthreads
Share Nothing, Do Everything
difference between Threads, Workers, Mutex, Stackable?