-
Notifications
You must be signed in to change notification settings - Fork 7.8k
Tick function is not working correctly inside fibers #8960
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
Comments
Tick is working fine (https://3v4l.org/tGSha#v8.1.8), just by design Fiber cannot be stopped from outside the Fiber:
Only thing missing is that FiberError. |
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:
Let's see what is happening here:
Now let's uncomment the
Let's see what happened after uncommenting the
Clearly, this is a bug that somehow unregisters (and also returns) the tick function when |
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:
As you see, unlike what I said before, running 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
|
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? |
@cmb69 Is there any reason or technical limitation to not allow calling of Fiber::suspend() in tick function? |
I don't think there is a hard technical limitation; it's rather something that we may not really want to support. |
@cmb69 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:
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. |
Well, at least some discussion on the internals list seems to be in order. |
@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. |
@trowski, I agree. I'd target "master" only. |
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 |
@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 🤦). |
@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 |
@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. <?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:
|
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. |
@kelunik Yeah I'm familiar with Amp alternatives and I use them from time to time. 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:
Thank you Niklas and all other internal guys, keep going, you all rock! |
I feel it's very bad way to achieve all of the above, |
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). |
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.
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.
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 |
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).
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! 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? 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. |
PHP can do real multithreading for much longer, e.g. by using https://github.com/krakjoe/parallel. |
@cmb69 Yeah, I already tried that years ago. If I remember correctly, that project is abandoned, right? |
Or was it pthreads project that abandoned? I don't remember correctly. 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. |
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. |
@cmb69 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? |
Closed via #9028. |
` AAAAAAAABBBBBBBBBBBBBCCCCCCCCCCCCCCCDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEEFFFFFFFFFFFF | -------------- RETURN VALUES -------------- |
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).
Let's take this last line of output. Could you explain the order of scheduling here? |
Description
The following code:
Resulted in this output:
But I expected this output instead:
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
The text was updated successfully, but these errors were encountered: