Skip to content

PHP & System.Threading.Tasks.Task

Imagine having all the C#/.NET objects readily available in your PHP code, including the entire .NET runtime. In this article, we'll take a glance at .NET threading, Tasks, and CLR debugging.

System.Threading.Tasks.Task

A task is an object that encapsulates a delegate, its state and the state of the task itself. Its delegate is invoked in a "context". The C# language allows for all sorts of whacky things with tasks, such as scheduling them in various contexts, awaiting them asynchronously, and so on.

We won't be doing that in PHP code. We'll "only" create a Task and we'll run it on a background thread while the main thread will continue executing its code.

The Code

We're writing a standard .php file (i.e. main.php) with the following valid PHP code:

<?php

use System\Threading\Tasks\Task;
use System\Threading\CancellationToken;
use System\Threading\CancellationTokenSource;

First, we introduce some class aliases, so we can then just write short names in our code. Notice there is no difference between a PHP class or .NET class. It's all the same to the compiler.

function main() {

Put our code into a global function main (the name does not matter). The reason is that the compiler works better with local variables - their types can be inferred better and the compiled code is much more efficient.

    $source = new CancellationTokenSource();
    $token = $source->Token;

Now create a CancellationTokenSource object - we'll use it to cancel the background task, as we usually do in C# programs.

    $task = Task::Run(
        function () use ($token) { ... },
        $token
    );

This is where the magic happens; the line above creates a Task object with a delegate that will be executed on a thread pool, and runs it.

Under the hood, this PHP anonymous function is compiled as a static CLR method with the following signature:

// static void anonymous@function(Closure? <closure>, PhpValue token)

The PeachPie runtime creates a Closure object, which wraps the delegate to this method together with the use $token argument. Then, the PeachPie runtime builds an in-memory Action delegate which calls the created Closure. Finally, the Task::Run(Action, CancellationToken) static method gets invoked.

Inside the anonymous function ({ ... }) running on a background thread we can do whatever we want to. For exmaple, we can run some computation and periodically check the $token for cancellation.

        for ($i = 0; ; $i++) {

            if ($token->IsCancellationRequested) {
                break;
            }
            usleep(100_000);
        }

After the task is created, we continue the execution on the main thread. In this sample, we schedule the CancellationTokenSource to cancel after 1 second, and wait synchronously for the background Task $task to finish.

    $source->CancelAfter(/*miliseconds:*/ 1000);
    $task->Wait();

Disclaimer

Some of the features necessary for this code to run - like passing a struct CancellationToken between closures, calling methods on value types, or better overload resolution - were implemented in the pre-release version of PeachPie 1.2.0-r17766. You'll need to grab PeachPie from the sources or get access to our private NuGet feed with all the SDKs, Runtime, and Compiler prepared for you. To get access to the private feed, nightly and release builds, a private forum and premium content, you need to become a Patron.

Building Code

As for any other C#/.NET project to run, we need a project file and the dotnet SDK.

Make sure you have dotnet (at least 8.0): https://dotnet.microsoft.com/en-us/download

Add the following project file (i.e. system-threading-tasks.msbuildproj):

<Project Sdk="Peachpie.NET.Sdk/1.2.0-r17797">
  <PropertyGroup>
    <OutputType>exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <StartupObject>main.php</StartupObject>
  </PropertyGroup>
  <ItemGroup>
    <Compile Include="**/*.php" />
  </ItemGroup>
</Project>

The project file is a standard MSBuild project that specifies

  • Peachpie.NET.Sdk as its "base" project. It defines how to call the compiler, it embeds the compiler binaries themselves, and it defines the default compilation options. dotnet will get it from the NuGet feed.

  • <OutputType>exe</OutputType> tells the compiler we're building an executable console app.

  • <TargetFramework>net8.0</TargetFramework> is our target ramework, in this case .NET 8.0.

  • <Compile Include="**/*.php" /> selects all .php files in project folder as source files. They will be compiled into the resulting .NET assembly.

Once you have the project file (.msbuildproj) and source file(s) (.php), run the build:

dotnet build

and/or run the program:

dotnet run

Project

The complete project can be found at https://github.com/iolevel/peachpie-samples/tree/patreon/system-threading-tasks

project in VSCode

You can open it in Visual Studio Code, Visual Studio (at least 2022), or Rider. In those IDE's you can click on Run/Debug and even place breakpoints and debug the code in CLR style.