PHP使用pthreads存在竞态条件问题

3

我有一个小代码示例,演示如何在PHP多线程中执行竞态条件。

想法是我和我的朋友共享做饭的锅。如果锅里已经有食材,那么锅就无法煮菜。

class Pot:

class Pot
{
    public $id;
    function __construct()
    {
        $this->id = rand();
    }

    public $ingredient;

    public function cook($ingredient, $who, $time){
        if ($this->ingredient==null){
            $this->ingredient = $ingredient;
            print "pot".$this->id.'/'.$who." cooking ".$this->ingredient. " time spent: ".$time." \n";
            sleep($time);
            print "pot".$this->id.'/'.$who." had flush ingredient \n";
            $this->ingredient = null;
            
        }else{
            throw new Exception("Pot still cook ".$this->ingredient);
        }
    }
}

朋友类:

class Friend extends Thread
{
    /**
     * @var Pot
     */
    protected $pot;

    function run() {
        Cocking::cleanVegetable("Friend");
        print "Friend will cook: \n";
        $this->pot->cook("vegetable", 'Friend',4);
        Cocking::digVegetable("Friend");
    }

    public function __construct($pot)
    {
        $this->pot = $pot;
    }
}

类 My:

class My
{
    /**
     * @var Pot
     */
    private $pot;
    public function doMyJob(){
        Cocking::cleanRice("I");
        print "I will cook: \n";
        $this->pot->cook("rice", "I",10);


        Cocking::digRice("I");
    }

    public function playGame(Friend $friend){
        print "play with friend \n";
    }

    public function __construct($pot)
    {
        $this->pot = $pot;
    }
}

类 厨师:

<?php


class Cocking
{
    static function cleanRice($who){
        print $who." is cleaning rice \n";
    }
    static function cleanVegetable($who){
        print $who."is cleaning vegetable \n";
    }


    static function digRice($who){
        print $who." is digging rice \n";
    }

    static function digVegetable($who){
        print $who." is digging vegetable \n";
    }
}

运行脚本:

require_once "Friend.php";
require_once "My.php";
require_once "Cocking.php";
require_once "Pot.php";

$pot = new Pot();
$friend = new Friend($pot);
$my = new My($pot);

$friend->start();
$my->doMyJob();
$friend->join();
$my->playGame($friend);

输出从未抛出异常,这太奇怪了,我以为这总是会发生的。

root@e03ed8b56f21:/app/RealLive# php index.php
Friendis cleaning vegetable
I is cleaning rice
Friend will cook:
I will cook:
pot926057642/I cooking rice time spent: 10
pot926057642/Friend cooking vegetable time spent: 4
pot926057642/Friend had flush ingredient
Friend is digging vegetable
pot926057642/I had flush ingredient
I is digging rice
play with friend

我用过这个Pot,但我的朋友仍然可以用它来煮蔬菜。这太神奇了吧?

Friend will cook:
I will cook:
pot926057642/I cooking rice time spent: 10
PHP Fatal error:  Uncaught Exception: Pot still cook rice in /app/RealLive/Pot.php:23
Stack trace:
#0 /app/RealLive/My.php(14): Pot->cook('rice', 'I', 10)
#1 /app/RealLive/index.php(12): My->doMyJob()
#2 {main}
  thrown in /app/RealLive/Pot.php on line 23

提示:我的环境是

PHP 7.0.10 (cli) (built: Apr 30 2019 21:14:24) ( ZTS )
Copyright (c) 1997-2016 The PHP Group
Zend Engine v3.0.0, Copyright (c) 1998-2016 Zend Technologies

非常感谢您的评论。

1
你能否尽可能减少示例中的代码量?或者它已经是导致错误的最小量了吗?此外,如果您编写了应该发生异常的完整控制台输出(包括您预期的位置),那将非常有帮助。 - E. T.
1
是的,我已经更新了所有的代码,并且我的期望是,在我和朋友同时做饭时会抛出异常。 - Erics Nguyen
3个回答

2
您的假设似乎是,您的if条件紧接着一个立即成员分配总是需要一次性运行。然而,完全有可能Friend在线程中运行这行代码:
if ($this->ingredient==null){

...并最终执行到下一行,即分配$this->ingredient之前,执行被切换回My/主线程,在那里它也到达了这一行:

if ($this->ingredient==null){

由于`Friend`已经通过了`if`语句但是还没有开始分配原料,所以`My`现在也可以进入。无论接下来发生什么,现在你已经让两个线程同时访问了锅,开始烹饪。
额外的更正/注意事项:似乎示例也不起作用,因为`$this->ingredient`不是`Volatile`类型。然而,这仍然会导致上述竞态条件,因此仍然是一个坏主意。
正确的做法:您真的需要使用互斥锁或同步部分进行适当的同步。另外,请永远不要假设线程无法在任何地方中断,包括任何两行,比如一个`if`后面跟着一个变量分配,这被认为是一对。
这是PHP关于同步区域的文档:https://www.php.net/manual/en/threaded.synchronized.php

是的,谢谢您的信息,但我仍然不确定if($this->ingredient == null)语句。我在cooking时加了一个睡眠来确保线程My在某个时间内进行烹饪,之后线程Friend才会到来。但它并没有产生异常。我将线程My和Friend增加到1000,并运行了几次,但没有异常被创建。 - Erics Nguyen
线程执行可以在任何时候切换,不仅仅是在发生睡眠时。你的睡眠并不是一个神奇的“线程只能在这里切换”的标记。当然,当一个线程正在睡眠时,这是确保其他线程运行的好方法,但这并不意味着它们不会在其他时间点运行。 - E. T.
1
老实说,我不确定这两个线程是否看到了相同的$spot对象(它可能是一个复制写入的东西,因为它没有作为引用传递),在我的测试中,我在Friend构造函数中初始化了$fp,但在run() -> flock()中,我得到了一个无效的资源,我不得不将$fp移动到run()中。话虽如此,@E.T.是正确的,线程调度可以随时发生,对变量的访问必须受到保护,同步。 - Alex
是的,sleep 使所有线程都暂停,因此在这个例子中没有意义。 - Erics Nguyen
1
你们能否看一下我的答案? - Erics Nguyen

1
在多线程应用程序中读写变量并不保证同步,需要一些同步机制,变量应声明为原子性以确保每次只有一个线程可以访问它进行读取或写入,以保证两个线程之间的一致性,或者使用互斥锁来同步共享资源的访问(lock / trylock / unlock)。
当前发生的情况是两个线程并行运行,ingredient变量根据执行顺序取随机值,当最长的睡眠结束时,应用程序退出。
在下面的示例中,我使用了flock,这是一种最简单的系统,用于同步多个进程之间的访问,在测试期间我遇到了问题,因为Friend构造函数可能没有在与同一实例的run函数相同的线程中执行...有很多因素要考虑,对我来说,php中的Thread似乎已经过时,并且与像C这样的语言相比,实现有点复杂。
class Friend extends Thread
{
    protected $pot;

    function run() {
        $this->pot->cook("vegetable", 'Friend',2);
    }

    public function __construct($pot)
    {
        $this->pot = $pot;
    }
}


class Pot
{
    public $id;
    public $ingredient;

    function __construct()
    {
        $this->id = rand();
    }

    public function cook($ingredient, $who, $time)
    {
        $fp = fopen('/tmp/.flock.pot', 'r');
        if (flock($fp, LOCK_EX|LOCK_NB)) {
            if ($this->ingredient==null){
                $this->ingredient = $ingredient;
                print "pot".$this->id.'/'.$who." cooking ".$this->ingredient. " time spent: ".$time." \n";
                sleep($time);
                print "pot".$this->id.'/'.$who." had flush ingredient \n";
                $this->ingredient = null;
            }
            flock($fp, LOCK_UN);
        } else {
            // throw new Exception("Pot still cook ".$this->ingredient);
            print "ingredient busy for {$this->id}/$who\n";
        }
        fclose($fp);
    }
}

class My
{
    private $pot;

    public function run(){
        $this->pot->cook("rice", "I",3);
    }

    public function __construct($pot)
    {
        $this->pot = $pot;
    }
}

touch('/tmp/.flock.pot');
$pot = new Pot();
$friend = new Friend($pot);
$my = new My($pot);

$friend->start();
sleep(1); // try comment me
$my->run();
$friend->join();
unlink('/tmp/.flock.pot');

0
程序的每个线程都有自己的内存。 在这个例子中,它是Pot并保存在主内存中。 其中一个线程已经读取和更改了它,但更改不会反映到主内存中,
因此其他线程无法看到该更改。 我们应该使Pot扩展Volatile以使更改可以反映到主内存中。
或者将块同步:
if ($this->ingredient==null)
  $this->ingredient = $ingredient;

仅将其标记为Volatile仍然是不正确的,请查看我在答案中添加的关于Volatile的注释。即使在99%的情况下可能有效,这也无法解决竞态条件,并且因此是一个坏主意。阻塞同步会有所帮助。 - E. T.
1
@E.T. 是的,你说得对。在这种情况下,Volatile是不够的,因为两个线程都在Pot上进行操作。 - Erics Nguyen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接