使用 pthreads PHP 中的竞争条件

race condition in PHP using pthreads

我有一些小代码演示了如何在多线程中执行竞争条件 PHP。

想法是我和我的朋友共用一个锅做饭。如果锅里已经有食材了,就不能煮了。

class锅:

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朋友:

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;
    }
}

class 我的:

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;
    }
}

class 上弦:

<?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

ps: 我的环境是

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

非常感谢您的评论。

您的假设似乎是您的 if 条件后跟直接成员分配总是需要一次性 运行。但是,完全有可能Friend 运行s线程中的这行代码:

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

... 并继续前进,但在它到达分配 $this->ingredient 的下一行之前,执行切换回 My/主线程,它也到达这一行:

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

并且由于Friend已经通过了if但还没有进行实际分配成分,My现在也可以通过了。接下来的 运行 无关紧要,您现在可以让两个线程同时访问烹饪锅。

附加 correction/note: 似乎该示例也不起作用,因为 $this->ingredient 不是 Volatile。然而,这仍然会使它容易出现上述竞争条件,因此仍然是一个坏主意。

如何正确地做到这一点:您确实需要使用互斥体或同步部分来进行正确的同步。此外,永远不会 假设线程不能在任何地方的中间切换,包括任何两行,如 if 后跟一个作为一对的变量赋值。

这是关于同步部分的 PHP 文档:https://www.php.net/manual/en/threaded.synchronized.php

在多线程应用中读写变量并不能保证同步,需要某种同步机制,变量应该声明为原子的,保证一次只有一个线程可以访问它进行读写,以保证两个线程之间的一致性,或者使用互斥锁来同步共享资源之间的访问(lock / trylock / unlock)。

目前的情况是两个线程运行并行,成分变量根据执行顺序取随机值,最长休眠结束时应用程序退出。

在下面的示例中,我使用了 flock,它是在多个进程之间同步访问的最简单的系统之一,在测试期间我遇到了问题,因为 Friend 构造函数可能与 运行 相同实例的功能...有很多因素需要考虑,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');

程序的每个线程都有自己的内存。在此示例中,它是 Pot 并保存在主内存中。并且其中一个线程已经读取并更改了它,并且更改不会反映到主内存中,

所以其他线程看不到更改。 所以我们应该 Pot extends Volatile 使改变的 can 反映到主内存。

或者使块同步:

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