3

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

我的想法是我和我的朋友正在共用锅做饭。如果锅里已经有配料,那么锅就不能煮了。

锅类:

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

我的班级:

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

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

非常感谢您的评论。

4

3 回答 3

2

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

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

... 并结束继续,但在到达下一个 assigns 行之前$this->ingredient,执行切换回My/main 线程,它也到达这一行:

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

而既然Friend已经过了if但还没有进行实际分配的配料,My现在也可以进去了。无论接下来运行什么都没关系,您现在让两个线程同时访问锅烹饪。

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

如何正确地做到这一点:您确实需要使用互斥锁或同步部分来进行正确的同步。此外,永远不要假设线程不能在任何地方的中间切换,包括任何两行,比如 anif后面跟着一个变量 assign ,这意味着一对。

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

于 2020-10-11T15:36:21.910 回答
1

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

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

在下面的示例中,我使用了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');
于 2020-10-11T15:49:02.547 回答
0

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

所以其他线程看不到改变。所以我们应该进行Pot扩展Volatile以使更改的可以反映到主存中。

或者使块同步:

if ($this->ingredient==null)
  $this->ingredient = $ingredient;
于 2020-10-14T04:46:06.350 回答