Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tick function is not working correctly inside fibers #8960

Closed
Alimadadi opened this issue Jul 9, 2022 · 28 comments
Closed

Tick function is not working correctly inside fibers #8960

Alimadadi opened this issue Jul 9, 2022 · 28 comments

Comments

@Alimadadi
Copy link

Description

The following code:

<?php

declare(ticks=1);

register_tick_function(function(){
  if(Fiber::getCurrent() !== null) {
    Fiber::suspend();
  }
});


for($i = 3; $i--;)
{
  $fibers[$i] = new fiber(function (){
    echo "1\n";
    echo "2\n";
    echo "3\n";
  });

  $fibers[$i]->start();
}

Resulted in this output:

1
1
2
3
1
2
3

But I expected this output instead:

1
1
1

Explanation:
If we are inside a fiber, after each tick, the tick function should call Fiber::suspend().
But currently this tick function only works inside first Fiber.

PHP Version

PHP 8.1.8

Operating System

macOS

@Alimadadi Alimadadi changed the title Tick function doesn't work inside Fibers Tick function is not working correctly inside fibers Jul 9, 2022
@KapitanOczywisty
Copy link

Tick is working fine (https://3v4l.org/tGSha#v8.1.8), just by design Fiber cannot be stopped from outside the Fiber:

If this method is called from outside a fiber, a FiberError will be thrown.
-- https://www.php.net/manual/en/fiber.suspend.php

Only thing missing is that FiberError.

@Alimadadi
Copy link
Author

Let's try another example to see how things work:

<?php

declare(ticks=1);

register_tick_function(function(){
  if(Fiber::getCurrent() === null)
  {
    echo "\tTICK - outside fiber\n";
    return;
  }

    echo "\tTICK - inside fiber\n";
    // Fiber::suspend();
    // echo "\tTICK - fiber suspended!\n";
});

echo "BEFORE:1\n";
echo "BEFORE:2\n";
echo "BEFORE:3\n";

foreach (range('A', 'C') as $v)
{
  $fibers[$v] = new fiber(function () use ($v){
    echo "FIBER {$v}:1\n";
    echo "FIBER {$v}:2\n";
    echo "FIBER {$v}:3\n";
  });

  $fibers[$v]->start();
}

echo "AFTER:1\n";
echo "AFTER:2\n";
echo "AFTER:3\n";

Here is the output:

	TICK - outside fiber
BEFORE:1
	TICK - outside fiber
BEFORE:2
	TICK - outside fiber
BEFORE:3
	TICK - outside fiber
	TICK - outside fiber
FIBER A:1
	TICK - inside fiber
FIBER A:2
	TICK - inside fiber
FIBER A:3
	TICK - inside fiber
	TICK - outside fiber
	TICK - outside fiber
FIBER B:1
	TICK - inside fiber
FIBER B:2
	TICK - inside fiber
FIBER B:3
	TICK - inside fiber
	TICK - outside fiber
	TICK - outside fiber
FIBER C:1
	TICK - inside fiber
FIBER C:2
	TICK - inside fiber
FIBER C:3
	TICK - inside fiber
	TICK - outside fiber
	TICK - outside fiber
AFTER:1
	TICK - outside fiber
AFTER:2
	TICK - outside fiber
AFTER:3
	TICK - outside fiber

Let's see what is happening here:

  • Expected: Tick function is called after every tick, no matter if it is inside a fiber or outside of fibers.
  • Expected: When we are inside a fiber, tick function is also called inside that fiber. In another word, Fiber::getCurrent() does not return null. So, we should also be able to call Fiber::suspend() too, right?

Now let's uncomment the // Fiber::suspend(); and // echo "\tTICK - fiber suspended!\n"; lines of this example's tick function, and see what is the results.

	TICK - outside fiber
BEFORE:1
	TICK - outside fiber
BEFORE:2
	TICK - outside fiber
BEFORE:3
	TICK - outside fiber
	TICK - outside fiber
FIBER A:1
	TICK - inside fiber
FIBER B:1
FIBER B:2
FIBER B:3
FIBER C:1
FIBER C:2
FIBER C:3
AFTER:1
AFTER:2
AFTER:3

Let's see what happened after uncommenting the // Fiber::suspend(); and // echo "\tTICK - fiber suspended!\n"; lines of the tick function:

  • Expected: Tick function is called on every tick, just as normal.
  • Expected: When PHP interpreter tries to execute code inside a fiber, Fiber::suspend() suspends the fiber. We no longer see FIBER A:2 and FIBER A:3 lines in the output.
  • Unexpected: All instructions inside tick function after Fiber::suspend() no longer executes. We should see \tTICK - fiber suspended!\n in the output.
  • Unexpected: Tick function no longer is called. No matter if we are inside another fiber or outside of fibers.

Clearly, this is a bug that somehow unregisters (and also returns) the tick function when Fiber::suspend() is called inside a tick function.
And no, FiberError should not be thrown, because tick function is called inside fiber, which is the expected behavior.

@Alimadadi
Copy link
Author

Alimadadi commented Jul 9, 2022

I just found out what is exactly happening here.

Let's add these lines just after the code in my last comment:

echo "Let's resume the Fiber A...\n";
$fibers['A']->resume();

echo "Let's resume the Fiber A, again...\n";
$fibers['A']->resume();

echo "Let's resume the Fiber A, again...\n";
$fibers['A']->resume();

echo "Done!\n";

Our code should look like this now:

<?php

declare(ticks=1);

register_tick_function(function(){
  if(Fiber::getCurrent() === null)
  {
    return print("\tTICK - outside fiber\n");
  }

    echo "\tTICK - inside fiber\n";
    Fiber::suspend();
    echo "\tTICK - fiber suspended!\n";
});

echo "BEFORE:1\n";
echo "BEFORE:2\n";
echo "BEFORE:3\n";

foreach (range('A', 'C') as $v)
{
  $fibers[$v] = new fiber(function () use ($v){
    echo "FIBER {$v}:1\n";
    echo "FIBER {$v}:2\n";
    echo "FIBER {$v}:3\n";
  });

  $fibers[$v]->start();
}

echo "AFTER:1\n";
echo "AFTER:2\n";
echo "AFTER:3\n";

echo "Let's resume the Fiber A...\n";
$fibers['A']->resume();

echo "Let's resume the Fiber A, again...\n";
$fibers['A']->resume();

echo "Let's resume the Fiber A, again...\n";
$fibers['A']->resume();

echo "Done!\n";

The output will be this:

	TICK - outside fiber
BEFORE:1
	TICK - outside fiber
BEFORE:2
	TICK - outside fiber
BEFORE:3
	TICK - outside fiber
	TICK - outside fiber
FIBER A:1
	TICK - inside fiber
FIBER B:1
FIBER B:2
FIBER B:3
FIBER C:1
FIBER C:2
FIBER C:3
AFTER:1
AFTER:2
AFTER:3
Let's resume the Fiber A...
	TICK - fiber suspended!
FIBER A:2
	TICK - inside fiber
Let's resume the Fiber A, again...
	TICK - fiber suspended!
FIBER A:3
	TICK - inside fiber
Let's resume the Fiber A, again...
	TICK - fiber suspended!
	TICK - outside fiber
Done!
	TICK - outside fiber

As you see, unlike what I said before, running Fiber::suspend() inside a tick function does not unregisters the tick function, nor it will not returns the tick function, but It just suspends both the fiber and the tick function. And when we resume the suspended fiber, it resumes from the tick function!

That's because the tick function is running inside the fiber, and when we suspend the fiber, the tick function suspends too.

Even though I believe that we should be able to call Fiber::suspend() inside a tick function (Which can open a world of opportunity to create flow control tools, such as coroutines), the tick function, by definition, should not stick to the fibers and suspend itself:

register_tick_function — Register a function for execution on each tick
https://www.php.net/manual/en/function.register-tick-function.php

@cmb69
Copy link
Contributor

cmb69 commented Jul 12, 2022

Fibers use cooperative multitasking; what you're trying to accomplish would be preemtive multitasking. It's pretty unlikely that this will be supported.

I think we need to document this limitation, and perhaps we should block switching fibers in tick functions in the first place. @trowski, thoughts?

@Alimadadi
Copy link
Author

@cmb69 Is there any reason or technical limitation to not allow calling of Fiber::suspend() in tick function?
Just trying to understand why "It's pretty unlikely that this will be supported" and why "we should block switching fibers in tick functions in the first place".

@cmb69
Copy link
Contributor

cmb69 commented Jul 13, 2022

I don't think there is a hard technical limitation; it's rather something that we may not really want to support.

@Alimadadi
Copy link
Author

@cmb69
Ok, I see.
Using tick functions to schedule fibers somehow complicate things, I believe.

Ummm... what about adding a tiny but useful feature to Fiber class?

Just adding two $maxTicks parameters to Fiber class's methods can achieve the same goal, but is much cleaner and easier:

public Fiber::__construct(callable $callback, int $maxTicks = null)
public Fiber::resume(mixed $value = null, int $maxTicks = null)

Default value should be set to null, which will work as normal (like current version of PHP).

A $maxTicks = 0 in constructor will suspend the fiber immediately after start(), any other positive value will run the fiber for exact same number of ticks and then suspends the fiber, unless suspend() is called sooner inside that fiber.

Constructor's $maxTicks parameter will be forced on next calls to resume() too, unless a new value is set during resume() call, which replaces that value for next resume() calls too.

resume() can accept $maxTicks = null, which will reset settings to default behavior (no tick limits). And maybe resume() should not accept $maxTicks = 0.

I understand that Fibers try to add cooperative multitasking to PHP's core, but this small feature can be very useful in many cases, and there will be no harm I can think of, including breaking the backward compatibility. I personally can't see any problem on Fibers supporting preemptive multitasking too.

I don't know if I should submit a RFC or what to request this feature.

@cmb69
Copy link
Contributor

cmb69 commented Jul 13, 2022

I don't know if I should submit a RFC or what to request this feature.

Well, at least some discussion on the internals list seems to be in order.

@trowski
Copy link
Member

trowski commented Jul 13, 2022

@cmb69 The most sensible thing to do IMO is block switching fibers in the tick function. Should that target 8.1 or master? I can look a providing a PR.

The tick function may be able to be made re-entrant, but preemptive scheduling was not the design goal of Fibers, so is not something I'm interested in implementing. Allowing arbitrary suspension of a fiber even further complicates design logic. Fibers do not exist completely independently like OS processes, so arbitrary suspension can lead to unexpected state changes at any time, not just during a function call.

@cmb69
Copy link
Contributor

cmb69 commented Jul 13, 2022

@trowski, I agree. I'd target "master" only.

@kelunik
Copy link
Member

kelunik commented Jul 13, 2022

As you see, unlike what I said before, running Fiber::suspend() inside a tick function does not unregisters the tick function, nor it will not returns the tick function, but It just suspends both the fiber and the tick function. And when we resume the suspended fiber, it resumes from the tick function!

That's because the tick function is running inside the fiber, and when we suspend the fiber, the tick function suspends too.

I haven't really followed your examples closely, but given this reasoning, everything seems to be working as expected to me? You should be able to implement these $maxTicks yourself using a tick function, no?

@Alimadadi
Copy link
Author

@kelunik No it's not possible. Calling Fiber::suspend() will suspend both the fiber and the tick function, and the tick function will never is called because it is suspended too (until the fiber that called suspend() resumes).

Yesterday I tried to register multiple tick functions for each fiber, and by that I managed to successfully implement preemptive multitasking that I wanted, but I didn't really tested it against possible bugs. unfortunately I coded it in /tmp directory and now it's gone, maybe I can reimplement it again if you need to. Anyway, I'm not sure if it's ok to register multiple tick functions or not (maybe another bug is found here?!).

Anyway, it seems like calling suspend() inside tick functions is going to be prohibited soon, and probably no alternative is going to be implemented. The only thing I can say here is: Curse the mouth that opens prematurely (a Persian proverb 🤦).

@kelunik
Copy link
Member

kelunik commented Jul 13, 2022

@Alimadadi I missed that the tick function has its own guard that prevents it from being executed again if it still executes. In that case, we should probably forbid suspensions in tick functions in PHP 8.2, same for pcntl_dispatch_signals and async signals, until it is properly supported.

@Alimadadi
Copy link
Author

@kelunik I totally understand It. I wish you internal guys find a proper way to support this feature soon. It can be very useful in many cases, from running multiple non-blocking sockets, database queries and disk I/O calls in parallel, to processing multiple image or video files using imagemagick or ffmpeg through shell functions, to I don't know, maybe implementing a complete small database server, cache server or web server in pure PHP, maybe?!

Anyway It seems like I did backed up the file that mentioned earlier and I found it now. Following code seems to work fine, which implements a simple custom thread-like preemptive multitasking using fibers and multiple tick functions, as I mentioned earlier.

This Thread class tries to run 26 concurrent thread() calls.
each thread() tries to prints a character in output for random number of times, then returns a message.
The return value of Thread::run() is automatically sorted by that random number, which that behavior is because how Thread::scheduler() - the tick function - is scheduling these threads.
Just comment out the register_tick_function() to disable scheduler function, to see how output is changed.

<?php

declare(ticks=1);

class Thread {
  protected static $names = [];
  protected static $fibers = [];
  protected static $params = [];

  public static function register(string|int $name, callable $callback, array $params)
  {
    self::$names[]  = $name;
    self::$fibers[] = new Fiber($callback);
    self::$params[] = $params;
  }

  public static function run() {
    $output = [];

    while (self::$fibers) {
      foreach (self::$fibers as $i => $fiber) {
          try {
              if (!$fiber->isStarted()) {
                  // Register a new tick function for scheduling this fiber
                  register_tick_function('Thread::scheduler');
                  $fiber->start(...self::$params[$i]);
              } elseif ($fiber->isTerminated()) {
                  $output[self::$names[$i]] = $fiber->getReturn();
                  unset(self::$fibers[$i]);
              } elseif ($fiber->isSuspended()) {
                $fiber->resume();
              }                
          } catch (Throwable $e) {
              $output[self::$names[$i]] = $e;
          }
      }
    }

    return $output;
  }

  public static function scheduler () {
    if(Fiber::getCurrent() === null) {
      return;
    }

    // running Fiber::suspend() will prevent an infinite loop!
    if(count(self::$fibers) > 1)
    {
      Fiber::suspend();
    }
  }
}


// defining a non-blocking thread, so multiple calls will run in concurrent mode using above Thread class.
function thread (string $print, int $loop)
{
  $i = $loop;
  while ($i--){
    echo $print;
  }

  return "Thread '{$print}' finished after printing '{$print}' for {$loop} times!";
}


// registering 26 Threads
foreach(range('A', 'Z') as $c) {
  Thread::register($c, 'thread', [$c, rand(10, 50)]);
}

// run threads
$outputs = Thread::run();

// print outputs
echo PHP_EOL, '-------------- OUTPUT --------------', PHP_EOL, print_r($outputs, true);

Output:

ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABC
DEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEF
GHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHI
JKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKL
MNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMN
OPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQ
RSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRST
WXYZABCDEFHIJKLMNOPQRSTWXYZACDEFIJKLMNOPQRSTYZADEFHJKLMN
OPQRSTWYZACEFHIKLMNOPQRSTWZACDHIJLMNOPQRSTWXZACDHIJKMNO
PQRSTWXACDHIJKLNOPQRSTWXYACDKLMOPQRSTWXYCDHJLMPQSTWXYZ
CDKMQTWXYZDJLOSWXYZDILMTXYZALMOSWYZACDJOQXZCDKMQSWYDIK
OSXZDILOQYJLQSZJQSCLSCJLMQWISXOSWKWKOSXJSWZJSWXJKSWXLWX
KOSXLWCLSXXCOXCIOXILSWILSLOXLOXJJJXXQQQSJJSJJJJJKXXXXXXSSSS

-------------- OUTPUT --------------

Array
(
    [U] => Thread 'U' finished after printing 'U' for 14 times!
    [G] => Thread 'G' finished after printing 'G' for 15 times!
    [V] => Thread 'V' finished after printing 'V' for 14 times!
    [B] => Thread 'B' finished after printing 'B' for 16 times!
    [E] => Thread 'E' finished after printing 'E' for 19 times!
    [F] => Thread 'F' finished after printing 'F' for 19 times!
    [N] => Thread 'N' finished after printing 'N' for 22 times!
    [R] => Thread 'R' finished after printing 'R' for 23 times!
    [H] => Thread 'H' finished after printing 'H' for 22 times!
    [P] => Thread 'P' finished after printing 'P' for 24 times!
    [A] => Thread 'A' finished after printing 'A' for 25 times!
    [T] => Thread 'T' finished after printing 'T' for 26 times!
    [Y] => Thread 'Y' finished after printing 'Y' for 27 times!
    [D] => Thread 'D' finished after printing 'D' for 30 times!
    [M] => Thread 'M' finished after printing 'M' for 28 times!
    [Z] => Thread 'Z' finished after printing 'Z' for 29 times!
    [C] => Thread 'C' finished after printing 'C' for 31 times!
    [O] => Thread 'O' finished after printing 'O' for 35 times!
    [I] => Thread 'I' finished after printing 'I' for 28 times!
    [W] => Thread 'W' finished after printing 'W' for 36 times!
    [Q] => Thread 'Q' finished after printing 'Q' for 34 times!
    [L] => Thread 'L' finished after printing 'L' for 37 times!
    [K] => Thread 'K' finished after printing 'K' for 30 times!
    [J] => Thread 'J' finished after printing 'J' for 40 times!
    [X] => Thread 'X' finished after printing 'X' for 46 times!
    [S] => Thread 'S' finished after printing 'S' for 47 times!
)

@kelunik
Copy link
Member

kelunik commented Jul 13, 2022

While this might work, I highly recommend you to take a look at existing event loop implementations like Revolt and concurrency libraries like Amp instead of relying on tick functions for that. For non-blocking sockets you want to suspend at exactly the point where you'd like to read or write (but can't in a non-blocking way), and then react to the readability / writability event to resume the current fiber. It's a similar thing with timers and signals.

@Alimadadi
Copy link
Author

@kelunik Yeah I'm familiar with Amp alternatives and I use them from time to time.
I raised this issue here because I was just testing fibers and possibilities that it can offer, and I wasn't implementing a production code.
I continue using those frameworks for now, and keep reading change logs to see how this feature will be implemented in the future, hope it will happen soon!

Anyway, I just repost a possible easy and clean solution that I mentioned before, maybe a re-think can help to generate some new and better ideas:

public Fiber::__construct(callable $callback, int $maxTicks = null)
public Fiber::resume(mixed $value = null, int $maxTicks = null)

Thank you Niklas and all other internal guys, keep going, you all rock!

@KapitanOczywisty
Copy link

It can be very useful in many cases, from running multiple non-blocking sockets, database queries and disk I/O calls in parallel, to processing multiple image or video files using imagemagick or ffmpeg through shell functions, to I don't know, maybe implementing a complete small database server, cache server or web server in pure PHP, maybe?!

I feel it's very bad way to achieve all of the above, tick is more debugging tool. Any potential performance/convenience gain would be lost on running tick functions (if they even would be able to reliably pause socket or shell calls). All of the above are already possible with socket_select, mysqli_poll, proc_open etc.

@Alimadadi
Copy link
Author

Alimadadi commented Jul 14, 2022

@KapitanOczywisty

Just to clarify, in the text you quoted, I was talking about an internal tick-like mechanism just for Fibers. There are probably many ways to implement it, some of them could be super fast too (like manipulating opcode and checking value of a counter after each operation, but honestly I don't know anything about PHP core codes at all so I can't help much here).

And yes, current tick functions are too heavy, but sometimes even this solution could be good enough, like when we have small number of jobs to do and PHP's performance do not matter that much (like converting lots videos/images using ffmpeg/imagemagick in parallel and storing their data in a database, or a custom client that periodically checks some email/web/etc apis, or a web crawler, etc...).

And yes, almost everything is possible in programming, your solution is good for some problems and we use it too, there are other ways to implement concurrency or even parallelism in PHP too, one other solution is using curl_multi_*() functions to call a local/private PHP API server (RESTful or anything else) to implement parallelism in pure PHP (supported on PHP 5.0+)!

The only thing that matters is what tools we have in our toolbox, and what possibilities these tools offer, and which of these tools offer a cleaner, faster, easier, better or preferred solution for our problem. After all, all of them can solve a problem, one way or another.

The benefit of an internal $maxTicks parameter and tick-like mechanism in Fiber class (or even ability to suspend fibers inside tick functions - like the Thread class I already introduced above) is freedom of dynamically adding new fibers (threads?!) to the pool so that each of these fibers can do something completely different with an automatic/custom scheduling mechanism (one fiber can plays with database, another execute shell commands, one controls a socket server, another grabs data from a API server, or even another fiber adds new fibers to the pool), but your solution is not capable of doing so (or is too hard/complicated to implement).

@KapitanOczywisty
Copy link

Just to clarify, in the text you quoted, I was talking about an internal tick-like mechanism just for Fibers.

You'd still need to set any socket to non-blocking and check with socket_select after, so just as easily you can suspend fiber after socket call.

sometimes even this solution could be good enough, like when we have small number of jobs to do and PHP's performance do not matter that much (like converting lots videos/images using ffmpeg/imagemagick in parallel and storing their data in a database, or a custom client that periodically checks some email/web/etc apis, or a web crawler, etc...).

I don't see any advantage of using ticks instead of suspending after system calls. Seriously what ticks are achieving? And besides for video conversion you need to queue task not start all at once.

The benefit of an internal $maxTicks parameter and tick-like mechanism in Fiber class (or even ability to suspend fibers inside tick functions - like the Thread class I already introduced above) is freedom of dynamically adding new fibers (threads?!) to the pool so that each of these fibers can do something completely different with an automatic/custom scheduling mechanism (one fiber can plays with database, another execute shell commands, one controls a socket server, another grabs data from a API server, or even another fiber adds new fibers to the pool), but your solution is not capable of doing so (or is too hard/complicated to implement).

It's simply not true, ticks won't stop system call midway. And all of that was already possible way before fibers are even proposed, not to mention that $maxTicks won't replace need to use functions like socket_select, so it would be as hard/complicated as before (it's not that complicated now).

@Alimadadi
Copy link
Author

Alimadadi commented Jul 14, 2022

@KapitanOczywisty

You'd still need to set any socket to non-blocking and check with socket_select after, so just as easily you can suspend fiber after socket call.

It's simply not true, ticks won't stop system call midway. And all of that was already possible way before fibers are even proposed, not to mention that $maxTicks won't replace need to use functions like socket_select, so it would be as hard/complicated as before (it's not that complicated now).

I never said we will never need non-blocking modes with my approach, of course we need them! After all, all fibers are running on a single CPU core that PHP process is running on, and one fiber can always block others, just like any other thread/process that can even block a whole operating system and all other apps (on a single core CPU).

I don't see any advantage of using ticks instead of suspending after system calls. Seriously what ticks are achieving? And besides for video conversion you need to queue task not start all at once.

That parallel video conversion was just an example. Think of it as a queue server for converting videos on many other servers (just an example, again!).

To answer you question, please first have a look at the source code of the Thread class I already coded and shared here before for better understanding of what I am saying. That class can attach/register multiple fibers to a pool, each fiber can do something completely different without knowing what other fibers are doing. Scheduling is completely custom but is also fully automatic (i.e. without a need to call suspend() inside fibers). Even a feature to control execution priority of each fiber can be easily added to this class, to execute some fibers for more ticks than others. That can be achieved just by adding a few line of codes to that same class!
This Thread class provides preemptive-multitasking/multithreading to the PHP (Although it runs in a single CPU core because of PHP's design). Yes, PHP 8.1 truly supports multithreading now!

I think now we have a common understanding on what that Thread class is trying do, and how that class is doing it. So, let's answer your question: What ticks are achieving?
The proposed $maxTicks or "Fiber Ticks" or just fixing the already mentioned bug in the tick function, can provide a way to schedule suspension/resume of fibers, without a need to manually call Fiber::suspend() inside that fiber, which provides a possibility to implement thread-like "preemptive multitasking" experience in a single PHP process.

I understand that Fibers are cooperative multitasking, maybe internal guys don't want to add $maxTicks to it, or even, sadly, even disable ability to call Fiber::suspend() inside tick function. But when PHP can almost do multithreading since version 8.1 by using fibers (which that mentioned simple class that I already coded can easily do it), what is the point of completely disabling this useful feature instead of just fixing a bug? I really don't understand it.

@cmb69
Copy link
Contributor

cmb69 commented Jul 14, 2022

PHP can do real multithreading for much longer, e.g. by using https://github.com/krakjoe/parallel.

@Alimadadi
Copy link
Author

@cmb69 Yeah, I already tried that years ago. If I remember correctly, that project is abandoned, right?

@Alimadadi
Copy link
Author

Or was it pthreads project that abandoned? I don't remember correctly.
Anyway, both those project and other projects like swoole and others, need an extension to be installed on server.

What I try to accomplish here, is fixing a bug that let PHP do the threading, without any extension, library or lots of other codes.

@cmb69
Copy link
Contributor

cmb69 commented Jul 14, 2022

The pthreads extension is abandoned, the parallel extension is more like on hold (for time reasons, and because it can't be used with Fibers). However, if ticks is the answer, you're asking the wrong question. Actually, ticks would likely have been deprecated if it wasn't for some using them in tricky ways, and probably none of the core developers can't be bothered to spend much time on them.

Anyhow, trying to use fibers as threads doesn't make sense to me. You never can have actual multi-threading with them.

@Alimadadi
Copy link
Author

@cmb69
So as true multi threading is not coming to PHP core soon (if it comes at all), and nobody is willing to fix this bug in tick functions, please don't forbid calling to Fiber::suspend() in tick functions too. It's not a security bug, it doesn't harm anyone, but with some cautions, it maybe can solve someone's problem.

As you already mentioned it, Just a small notice message in ticks and Fibers pages in the docs can inform programmers about this bug/limitation. Just let them decide how they wanna solve their own problem. Maybe someday, someone writes a patch to fix this bug, who knows?

@trowski
Copy link
Member

trowski commented Jul 16, 2022

Closed via #9028.

@trowski trowski closed this as completed Jul 16, 2022
@cighsen02
Copy link

@kelunik I totally understand It. I wish you internal guys find a proper way to support this feature soon. It can be very useful in many cases, from running multiple non-blocking sockets, database queries and disk I/O calls in parallel, to processing multiple image or video files using imagemagick or ffmpeg through shell functions, to I don't know, maybe implementing a complete small database server, cache server or web server in pure PHP, maybe?!

Anyway It seems like I did backed up the file that mentioned earlier and I found it now. Following code seems to work fine, which implements a simple custom thread-like preemptive multitasking using fibers and multiple tick functions, as I mentioned earlier.

This Thread class tries to run 26 concurrent thread() calls. each thread() tries to prints a character in output for random number of times, then returns a message. The return value of Thread::run() is automatically sorted by that random number, which that behavior is because how Thread::scheduler() - the tick function - is scheduling these threads. Just comment out the register_tick_function() to disable scheduler function, to see how output is changed.

<?php

declare(ticks=1);

class Thread {
  protected static $names = [];
  protected static $fibers = [];
  protected static $params = [];

  public static function register(string|int $name, callable $callback, array $params)
  {
    self::$names[]  = $name;
    self::$fibers[] = new Fiber($callback);
    self::$params[] = $params;
  }

  public static function run() {
    $output = [];

    while (self::$fibers) {
      foreach (self::$fibers as $i => $fiber) {
          try {
              if (!$fiber->isStarted()) {
                  // Register a new tick function for scheduling this fiber
                  register_tick_function('Thread::scheduler');
                  $fiber->start(...self::$params[$i]);
              } elseif ($fiber->isTerminated()) {
                  $output[self::$names[$i]] = $fiber->getReturn();
                  unset(self::$fibers[$i]);
              } elseif ($fiber->isSuspended()) {
                $fiber->resume();
              }                
          } catch (Throwable $e) {
              $output[self::$names[$i]] = $e;
          }
      }
    }

    return $output;
  }

  public static function scheduler () {
    if(Fiber::getCurrent() === null) {
      return;
    }

    // running Fiber::suspend() will prevent an infinite loop!
    if(count(self::$fibers) > 1)
    {
      Fiber::suspend();
    }
  }
}


// defining a non-blocking thread, so multiple calls will run in concurrent mode using above Thread class.
function thread (string $print, int $loop)
{
  $i = $loop;
  while ($i--){
    echo $print;
  }

  return "Thread '{$print}' finished after printing '{$print}' for {$loop} times!";
}


// registering 26 Threads
foreach(range('A', 'Z') as $c) {
  Thread::register($c, 'thread', [$c, rand(10, 50)]);
}

// run threads
$outputs = Thread::run();

// print outputs
echo PHP_EOL, '-------------- OUTPUT --------------', PHP_EOL, print_r($outputs, true);

Output:

ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABC
DEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEF
GHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHI
JKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKL
MNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMN
OPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQ
RSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRST
WXYZABCDEFHIJKLMNOPQRSTWXYZACDEFIJKLMNOPQRSTYZADEFHJKLMN
OPQRSTWYZACEFHIKLMNOPQRSTWZACDHIJLMNOPQRSTWXZACDHIJKMNO
PQRSTWXACDHIJKLNOPQRSTWXYACDKLMOPQRSTWXYCDHJLMPQSTWXYZ
CDKMQTWXYZDJLOSWXYZDILMTXYZALMOSWYZACDJOQXZCDKMQSWYDIK
OSXZDILOQYJLQSZJQSCLSCJLMQWISXOSWKWKOSXJSWZJSWXJKSWXLWX
KOSXLWCLSXXCOXCIOXILSWILSLOXLOXJJJXXQQQSJJSJJJJJKXXXXXXSSSS

-------------- OUTPUT --------------

Array
(
    [U] => Thread 'U' finished after printing 'U' for 14 times!
    [G] => Thread 'G' finished after printing 'G' for 15 times!
    [V] => Thread 'V' finished after printing 'V' for 14 times!
    [B] => Thread 'B' finished after printing 'B' for 16 times!
    [E] => Thread 'E' finished after printing 'E' for 19 times!
    [F] => Thread 'F' finished after printing 'F' for 19 times!
    [N] => Thread 'N' finished after printing 'N' for 22 times!
    [R] => Thread 'R' finished after printing 'R' for 23 times!
    [H] => Thread 'H' finished after printing 'H' for 22 times!
    [P] => Thread 'P' finished after printing 'P' for 24 times!
    [A] => Thread 'A' finished after printing 'A' for 25 times!
    [T] => Thread 'T' finished after printing 'T' for 26 times!
    [Y] => Thread 'Y' finished after printing 'Y' for 27 times!
    [D] => Thread 'D' finished after printing 'D' for 30 times!
    [M] => Thread 'M' finished after printing 'M' for 28 times!
    [Z] => Thread 'Z' finished after printing 'Z' for 29 times!
    [C] => Thread 'C' finished after printing 'C' for 31 times!
    [O] => Thread 'O' finished after printing 'O' for 35 times!
    [I] => Thread 'I' finished after printing 'I' for 28 times!
    [W] => Thread 'W' finished after printing 'W' for 36 times!
    [Q] => Thread 'Q' finished after printing 'Q' for 34 times!
    [L] => Thread 'L' finished after printing 'L' for 37 times!
    [K] => Thread 'K' finished after printing 'K' for 30 times!
    [J] => Thread 'J' finished after printing 'J' for 40 times!
    [X] => Thread 'X' finished after printing 'X' for 46 times!
    [S] => Thread 'S' finished after printing 'S' for 47 times!
)

`

AAAAAAAABBBBBBBBBBBBBCCCCCCCCCCCCCCCDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEEFFFFFFFFFFFF

  | -------------- RETURN VALUES --------------
  | Array
  | (
  | [A] => Thread 'A' finished after printing 'A' for 8 times!
  | [B] => Thread 'B' finished after printing 'B' for 13 times!
  | [C] => Thread 'C' finished after printing 'C' for 15 times!
  | [D] => Thread 'D' finished after printing 'D' for 14 times!
  | [E] => Thread 'E' finished after printing 'E' for 17 times!
  | [F] => Thread 'F' finished after printing 'F' for 12 times!
  | )

`

@kelunik
Copy link
Member

kelunik commented Sep 17, 2022

I wish you internal guys find a proper way to support this feature soon. It can be very useful in many cases, from running multiple non-blocking sockets, database queries and disk I/O calls in parallel, to processing multiple image or video files using imagemagick or ffmpeg through shell functions, to I don't know, maybe implementing a complete small database server, cache server or web server in pure PHP, maybe?!

All of those are better solved with an event loop instead of using tick functions. Please have a look at existing projects like AMPHP. Things you imagine maybe being possible are actually already a thing: https://github.com/amphp/http-server (look at the v3 branch here).

KOSXLWCLSXXCOXCIOXILSWILSLOXLOXJJJXXQQQSJJSJJJJJKXXXXXXSSSS

Let's take this last line of output. Could you explain the order of scheduling here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants