uppes/coroutine

该软件包已被 废弃 且不再维护。作者建议使用 symplely/coroutine 软件包。

使用生成器的协作多任务。协程、异步和 await 的基础知识!

1.9.9 2021-03-01 03:48 UTC

README

LinuxWindowsmacOScodecovCodacy BadgeMaintainability

这是第 2x 版本,它会破坏 1x 版本,因为全局函数的 命名空间,将所有必需的 常量 移动到依赖包中。本版本包含新的 Fiber 实现,无需 PHP ext-fibers 扩展,计划用于 PHP 8.1。所有 ext-fibers 测试示例 都已在本地的 examples/fibertests/fibertests/FiberTest.php 中实现。

为了获得最佳性能,建议安装跨平台的 libuv 库和 PHP 扩展 ext-uv。请参阅在线 书籍,以获得有关其使用的全面教程概述。

所有 libuv socket/stream/udp/tcp 类型的功能都需要在 Windows 上重新实现,之前的假设是它们已损坏,但这并非事实,而是由于使用的 PHP ext-uv 版本的 libuv 和对功能应如何工作的假设存在问题。因此,当前使用原生 stream_selectWindows 实现的方案将进行重构/重构。

该软件包的下一个版本在 2x 发布之后,将需要 uv-ffi,这是 libuvext-uvFFI 版本,因此无需下载/安装额外的二进制库包,并且一些当前依赖项将不再必要。

为了了解基本概念概述,请阅读 "Concurrency and PHP in relation to modern programming languages, Python, Go, NodeJS, Rust, Etc",目前处于 草稿 形式,完成后将在 dev.io 上发布。

本版本还实现了 PHP ext-parallel 扩展,使用 libuv 的子 进程 功能。要快速了解 线程进程 之间的性能比较,请参阅 Launching Linux threads and processes with clone

本实现遵循 parallel\Runtimeparallel\Futureparallel\ChannelFunctional API 规范,但没有任何 限制

通过使用 opis/closure 包来克服这些限制。所有 ext-parallel 扩展的 测试 和示例,都已在 examples/paralleltests/parallel 中进行修改,以包含此包库并移除对可变数组参数的要求。一些测试文件名中增加了 .1,因为它们与之前有较大差异,一些可能需要 yield 才能正确使用的测试被跳过了。

未实现 ext-paralleleventssync API,未发现使用案例,并且这些功能已经作为此 coroutine 包的内部部分。

目录

简介

什么是异步? 简单地说,避免下一个指令的阻塞。这是一种 捕获稍后运行 的机制。

此包解决了当前 PHP 实现的哪些问题? 易用性AmpReactPHP 都提供了异步结果。但两者都需要用户/开发者进行更多设置才能使简单的代码行运行。还有 SwooleHhvm,但它们都不是标准的 PHP 安装。

当使用 AmpReactPHP 以及基于它们的某些包时,你必须不仅要管理你提供的 Callbacks,还要管理返回的 Promise 对象和 Event Loop 对象。这些库模仿旧的 Javascript,而现在的 Javascript 正在向简单的 async/await 语法转变。这引发了一个问题。

易用性意味着什么? 我们可以先回顾一下其他编程语言的实现。并且你已经有了一个正在运行的应用程序,你希望各个部分运行得更加响应,这相当于要做更多。

Go 有其 Goroutine 关键字 go,然后是你的函数和语句。

以下链接详细介绍了 PythonRubyRustNimC#ElixirJavaC++async/await 关键字与函数和语句的结合。

使用它们时,它们返回你真正想要的结果,无需干预任何对象,无需回调,在调用后,一切到此为止的代码继续运行。

关于每个语言的,它们的历史、实际行为、底层概念都是相同的,调用结构简单。它们都以各种方式引用了 yield迭代器 的使用,这正是此包所依赖的,并使以下情况成为可能。

要使代码异步,你只需在代码中放入yield,并在调用代码前加上yield前缀,以获取你想要的实际results

使用此包,你可以通过使用yield来获得一个async/awaitPHP版本。

有一些辅助函数可以将一切结合起来。主要的是away(),它类似于Python的create_task(),其行为类似于Google的go()关键字,此处作为别名函数go()包含在内。

使用它将立即返回一个数字,可以与另一个类似Python的函数一起使用,该函数也类似于Google的WaitGroup。这将等待并返回运行在后台的代码的远端/分离的结果。

此包遵循一种新的范例行为编程,该编程使用B-threads,即功能生成器的概念。

Swoole 协程和Facebook的Hhvm PHP的基本使用遵循与其他相同的概述实现,在此提出。

为了进一步说明,请比较Python与Node.js的异步并发简介中关于NodeJS和Python的比较。

// async_scrape.js (tested with node 11.3)
const sleep = ts => new Promise(resolve => setTimeout(resolve, ts * 1000));

async function fetchUrl(url) {
    console.log(`~ executing fetchUrl(${url})`);
    console.time(`fetchUrl(${url})`);
    await sleep(1 + Math.random() * 4);
    console.timeEnd(`fetchUrl(${url})`);
    return `<em>fake</em> page html for ${url}`;
}

async function analyzeSentiment(html) {
    console.log(`~ analyzeSentiment("${html}")`);
    console.time(`analyzeSentiment("${html}")`);
    await sleep(1 + Math.random() * 4);
    const r = {
        positive: Math.random()
    }
    console.timeEnd(`analyzeSentiment("${html}")`);
    return r;
}

const urls = [
    "https://www.ietf.org/rfc/rfc2616.txt",
    "https://en.wikipedia.org/wiki/Asynchronous_I/O",
]
const extractedData = {}

async function handleUrl(url) {
    const html = await fetchUrl(url);
    extractedData[url] = await analyzeSentiment(html);
}

async function main() {
    console.time('elapsed');
    await Promise.all(urls.map(handleUrl));
    console.timeEnd('elapsed');
}

main()
# async_scrape.py (requires Python 3.7+)
import asyncio, random, time

async def fetch_url(url):
    print(f"~ executing fetch_url({url})")
    t = time.perf_counter()
    await asyncio.sleep(random.randint(1, 5))
    print(f"time of fetch_url({url}): {time.perf_counter() - t:.2f}s")
    return f"<em>fake</em> page html for {url}"

async def analyze_sentiment(html):
    print(f"~ executing analyze_sentiment('{html}')")
    t = time.perf_counter()
    await asyncio.sleep(random.randint(1, 5))
    r = {"positive": random.uniform(0, 1)}
    print(f"time of analyze_sentiment('{html}'): {time.perf_counter() - t:.2f}s")
    return r

urls = [
    "https://www.ietf.org/rfc/rfc2616.txt",
    "https://en.wikipedia.org/wiki/Asynchronous_I/O",
]
extracted_data = {}

async def handle_url(url):
    html = await fetch_url(url)
    extracted_data[url] = await analyze_sentiment(html)

async def main():
    t = time.perf_counter()
    await asyncio.gather(*(handle_url(url) for url in urls))
    print("> extracted data:", extracted_data)
    print(f"time elapsed: {time.perf_counter() - t:.2f}s")

asyncio.run(main())

以这种方式使用此包,它具有相同的简单性

// This is in the examples folder as "async_scrape.php"
include 'vendor/autoload.php';

function fetch_url($url)
{
  print("~ executing fetch_url($url)" . \EOL);
  \timer_for($url);
  yield \sleep_for(\random_uniform(1, 5));
  print("time of fetch_url($url): " . \timer_for($url) . 's' . \EOL);
  return "<em>fake</em> page html for $url";
};

function analyze_sentiment($html)
{
  print("~ executing analyze_sentiment('$html')" . \EOL);
  \timer_for($html . '.url');
  yield \sleep_for(\random_uniform(1, 5));
  $r = "positive: " . \random_uniform(0, 1);
  print("time of analyze_sentiment('$html'): " . \timer_for($html . '.url') . 's' . \EOL);
  return $r;
};

function handle_url($url)
{
  yield;
  $extracted_data = [];
  $html = yield fetch_url($url);
  $extracted_data[$url] = yield analyze_sentiment($html);
  return yield $extracted_data;
};

function main()
{
  $urls = [
    "https://www.ietf.org/rfc/rfc2616.txt",
    "https://en.wikipedia.org/wiki/Asynchronous_I/O"
  ];
  $urlID = [];

  \timer_for();
  foreach ($urls as $url)
    $urlID[] = yield \away(handle_url($url));

  $result_data = yield \gather($urlID);
  foreach ($result_data as $id => $extracted_data) {
    echo "> extracted data:";
    \print_r($extracted_data);
  }

  print("time elapsed: " . \timer_for() . 's');
}

\coroutine_run(main());

尝试用其他纯PHP异步实现重新创建此功能,它们需要首先重写才能接近。

这里的协程是基于generators特别定制的函数,使用yieldyield from。当使用时,它们控制上下文,即捕获/释放应用程序的执行流。

当在代码块中放置yield时,它向调用函数指示,将返回一个对象,代码不会立即执行。

此包代表调用函数,即调度器,类似于事件循环。协程需要被调度以运行,一旦调度,协程就被包装在一个Task中,而Task是一种类型的Promise

Task是一个表示一些要完成的工作的对象,可能在结束时有一个结果。这些任务被注册在负责运行它们的调度器上。

由于PHP(没有扩展的情况下)的单线程本质,我们无法将Task视为进行单次long-running计算——这将block单线程,直到任务完成。

相反,tasks必须在可能的情况下执行小块/迭代(称为'ticks')的工作,并在适当的时候将控制权交回调度器。这被称为cooperative multi-tasking(之所以称为合作多任务,是因为任务必须通过自愿释放控制权来合作)。

调度器负责“tick”已调度的任务,每个已调度的任务都会被反复“tick”直到完成。调度器实现如何以允许所有已调度的任务运行的方式进行,取决于调度器实现。

一个 任务 可以通过以下三种方式之一完成:

The task reaches successful completion, and optionally produces a result
The task encounters an error and fails
The task is cancelled by calling cancel()

当使用这个包时,如果您正在工作的代码中包含 yield 点,这些点定义了当有其他任务等待时可能发生 上下文切换 的位置,如果没有其他任务等待则不会发生。这也可以被视为 断点/陷阱,就像在使用调试器时,当触发时,调试器会介入,您可以查看状态并逐步执行剩余的代码。

一个 上下文切换 代表 调度器 将控制流从一个 协程 切换到下一个。

在这里,协程 被定义为包含 yield 关键字的函数/方法,它将返回 生成器 对象。

立即返回的 生成器 对象提供了一些方法,允许它自身进行进步。

所以,这里有一个与 生成器 相关的非常特殊的情况,它作为 PHP 语言的一部分,当从 Promise 的工作方式的角度来看时,它不会阻塞,只是执行一行并返回。异步的主要思想。

Promises 返回一个对象,该对象被放入事件循环队列中。事件循环执行附加到对象的回调的实际执行。这是一个真正的手动过程,需要管理大量的代码状态/开销。这被称为 反应器模式 的执行,同步调度回调。

事件循环的 机制 在一个 生成器 被激活时已经存在。我认为这是一个 Proactor 模式。由于 yield 操作是初始化者,它开始检查资源可用性,在那一刻执行操作/动作,异步处理/返回完成事件。

阅读这篇帖子,C++20 中的协程是什么?

There are two kinds of coroutines; stackful and stackless.

A stackless coroutine only stores local variables in its state and its location of execution.

A stackful coroutine stores an entire stack (like a thread).

Stackless coroutines can be extremely light weight. The last proposal I read involved basically rewriting your function into something a bit like a lambda; all local variables go into the state of an object, and labels are used to jump to/from the location where the coroutine "produces" intermediate results.

The process of producing a value is called "yield", as coroutines are bit like cooperative multithreading; you are yielding the point of execution back to the caller.

此包执行协作调度,多任务和异步编程的基础。

当引入 yield 时正在进行的步骤。

  1. 函数 现在是一个 协程
  2. 返回的 对象 被调度器捕获。
  3. 调度器将捕获的 生成器 对象包装在一个 任务 对象中。
  4. 任务对象具有额外的方法和功能,它可以被视为类似于 promise
  5. 任务现在被放置在由调度器控制的 任务队列 中。
  6. 运行 您的 函数,使一切开始动起来。在这里,您没有启动任何 事件循环。这可以被视为事件循环的是在调度器将 任务 放入 行动 之前或之后所做的 工作
  7. 这个 任务 将在哪里落地/返回? 答案:与调用它的同一个位置,这里没有 回调

步骤 1 在其他语言中通过一个特定的关键字 async 实现。

步骤 26 在其他语言中通过一个特定的关键字 await 实现。

这里使用的术语/命名更符合 Python 的 AsyncioCurio 的用法。事实上,大多数源代码方法调用都已更改以匹配它们。

这个包应被视为一个 用户空间 扩展,它对 yield 的使用已经由 RFC 创建者设想。

函数

应仅使用位于此处和 Core.php 文件中的函数。不建议直接访问对象类库,其名称可能更改,或者如果未在此处列出,则可能完全删除。第三方库包 开发 是例外。

Stream.php 中与 网络 相关的函数、Path.php 中的 文件系统 以及 Worker.php 中的 进程 都已命名空间化,因此应按以下方式使用:

use function Async\Path\{ , };
use function Async\Worker\{ , };
use function Async\Stream\{ , };
/**
 * Returns a random float between two numbers.
 *
 * Works similar to Python's `random.uniform()`
 * @see https://docs.pythonlang.cn/3/library/random.html#random.uniform
 */
\random_uniform($min, $max);

/**
 * Return the value (in fractional seconds) of a performance counter.
 * Using either `hrtime` or system's `microtime`.
 *
 * The $tag is:
 * - A reference point used to set, to get the difference between the results of consecutive calls.
 * - Will be cleared/unset on the next consecutive call.
 *
 * returns float|void
 *
 * @see https://docs.pythonlang.cn/3/library/time.html#time.perf_counter
 * @see https://node.org.cn/docs/latest-v11.x/api/console.html#console_console_time_label
 */
\timer_for(string $tag = 'perf_counter');

/**
 * Makes an resolvable function from label name that's callable with `away`
 */
\async(string $labelFunction, $asyncFunction);

/**
 * Wrap the value with `yield`, when placed within this insure that
 * any *function/method* will be `awaitable` and the actual return
 * value is picked up properly by `gather()`.
 */
return \value($value)

/**
 * Add/schedule an `yield`-ing `function/callable/task` for background execution.
 * Will immediately return an `int`, and continue to the next instruction.
 * Returns an task Id
 * - This function needs to be prefixed with `yield`
 *
 * @see https://docs.pythonlang.cn/3.7/library/asyncio-task.html#asyncio.create_task
 */
yield \away($awaitedFunction, ...$args) ;

/**
 * Performs a clean application exit and shutdown.
 *
 * Provide $skipTask incase called by an Signal Handler. Defaults to the main parent task.
 * - Use `get_task()` to retrieve caller's task id.
 *
 * - This function needs to be prefixed with `yield`
 */
yield \shutdown($skipTask)

/**
 * Wrap the callable with `yield`, this insure the first attempt to execute will behave
 * like a generator function, will switch at least once without actually executing, return object instead.
 * This function is used by `away` not really called directly.
 *
 * @see https://docs.pythonlang.cn/3.7/library/asyncio-task.html#awaitables
 */
\awaitAble($awaitableFunction, ...$args);

/**
 * Run awaitable objects in the tasks set concurrently and block until the condition specified by race.
 *
 * Controls how the `gather()` function operates.
 * `gather_wait` will behave like **Promise** functions `All`, `Some`, `Any` in JavaScript.
 *
 * - This function needs to be prefixed with `yield`
 *
 * @see https://docs.pythonlang.cn/3.7/library/asyncio-task.html#waiting-primitives
 */
yield \gather_wait(array $tasks, int $race = 0, bool $exception = true, bool $clear = true)

/**
 * Run awaitable objects in the taskId sequence concurrently.
 * If any awaitable in taskId is a coroutine, it is automatically scheduled as a Task.
 *
 * If all awaitables are completed successfully, the result is an aggregate list of returned values.
 * The order of result values corresponds to the order of awaitables in taskId.
 *
 * The first raised exception is immediately propagated to the task that awaits on gather().
 * Other awaitables in the sequence won't be cancelled and will continue to run.
 * - This function needs to be prefixed with `yield`
 *
 * @see https://docs.pythonlang.cn/3.7/library/asyncio-task.html#asyncio.gather
 */
yield \gather(...$taskId);

/**
 * Block/sleep for delay seconds.
 * Suspends the calling task, allowing other tasks to run.
 * A result is returned If provided back to the caller
 * - This function needs to be prefixed with `yield`
 */
yield \sleep_for($delay, $result);

/**
 * Creates an communications Channel between coroutines, returns an object
 * Similar to Google Go language - basic, still needs additional functions
 * - This function needs to be prefixed with `yield`
 */
yield \make();

/**
 * Send message to an Channel
 * - This function needs to be prefixed with `yield`
 */
yield \sender($channel, $message, $taskId);

/**
 * Set task as Channel receiver, and wait to receive Channel message
 * Will continue other tasks until so.
 * - This function needs to be prefixed with `yield`
 */
yield \receiver($channel);

/**
 * A goroutine is a function that is capable of running concurrently with other functions.
 * To create a goroutine we use the keyword `go` followed by a function invocation
 * @see https://www.golang-book.com/books/intro/10#section1
 */
yield \go($goFunction, ...$args);

/**
 * Modeled as in `Go` Language.
 *
 * The behavior of defer statements is straightforward and predictable.
 * There are three simple rules:
 * 1. *A deferred function's arguments are evaluated when the defer statement is evaluated.*
 * 2. *Deferred function calls are executed in Last In First Out order after the* surrounding function returns.
 * 3. *Deferred functions can`t modify return values when is type, but can modify content of reference to
 *
 * @see https://golang.ac.cn/doc/effective_go.html#defer
 */
\defer(&$previous, $callback)

/**
 * Modeled as in `Go` Language.
 *
 * Regains control of a panicking `task`.
 *
 * Recover is only useful inside `defer()` functions. During normal execution, a call to recover will return nil
 * and have no other effect. If the current `task` is panicking, a call to recover will capture the value given
 * to panic and resume normal execution.
 */
\recover(&$previous, $callback);

/**
 * Modeled as in `Go` Language.
 *
 * An general purpose function for throwing an Coroutine `Exception`,
 * or some abnormal condition needing to keep an `task` stack trace.
 */
\panic($message, $code, $previous);

/**
 * Return the task ID
 * - This function needs to be prefixed with `yield`
 */
yield \get_task();

/**
 * kill/remove an task using task id
 * - This function needs to be prefixed with `yield`
 */
yield \cancel_task($tid);

/**
 * Wait for the callable/task to complete with a timeout.
 * Will continue other tasks until so.
 * - This function needs to be prefixed with `yield`
 */
yield \wait_for($callable, $timeout);

/**
 * Wait on read stream/socket to be ready read from.
 * Will continue other tasks until so.
 * - This function needs to be prefixed with `yield`
 */
yield \read_wait($stream);

/**
 * Wait on write stream/socket to be ready to be written to.
 * Will continue other tasks until so.
 * - This function needs to be prefixed with `yield`
 */
yield \write_wait($stream);

/**
 * Wait on keyboard input.
 * Will continue other tasks until so.
 * Will not block other task on `Linux`, will continue other tasks until `enter` key is pressed,
 * Will block on Windows, once an key is typed/pressed, will continue other tasks `ONLY` if no key is pressed.
 * - This function needs to be prefixed with `yield`
 */
yield \input_wait($size);

/**
 * An PHP Functional Programming Primitive.
 * Return a curryied version of the given function. You can decide if you also
 * want to curry optional parameters or not.
 *
 * @see https://github.com/lstrojny/functional-php/blob/master/docs/functional-php.md#currying
 */
\curry($function, $required);

\coroutine_instance();

\coroutine_clear();

\coroutine_create($coroutine);

/**
 * This function runs the passed coroutine, taking care of managing the scheduler and
 * finalizing asynchronous generators. It should be used as a main entry point for programs, and
 * should ideally only be called once.
 *
 * @see https://docs.pythonlang.cn/3.7/library/asyncio-task.html#asyncio.run
 */
\coroutine_run($coroutine);
use function Async\Worker\{ add_process, spawn_task, spawn_await };

/**
 * Add/execute a blocking `subprocess` task that runs in parallel.
 * This function will return `int` immediately, use `gather()` to get the result.
 * - This function needs to be prefixed with `yield`
 */
yield \spawn_task($command, $timeout);

/**
 * Add and wait for result of an blocking `I/O` subprocess that runs in parallel.
 * - This function needs to be prefixed with `yield`
 *
 * @see https://docs.pythonlang.cn/3.7/library/asyncio-subprocess.html#subprocesses
 * @see https://docs.pythonlang.cn/3.7/library/asyncio-dev.html#running-blocking-code
 */
 yield \spawn_await($callable, $timeout, $display, $channel, $channelTask, $signal, $signalTask);
/**
 * Add and wait for result of an blocking `I/O` subprocess that runs in parallel.
 * This function turns the calling function internal __state/type__ used by `gather()`
 * to **process/paralleled** which is handled differently.
 * - This function needs to be prefixed with `yield`
 *
 * @see https://docs.pythonlang.cn/3.7/library/asyncio-subprocess.html#subprocesses
 * @see https://docs.pythonlang.cn/3.7/library/asyncio-dev.html#running-blocking-code
 */
yield \add_process($command, $timeout);
use function Async\Path\file_***Any File System Command***;

/**
 * Executes a blocking system call asynchronously either natively thru `libuv`, `threaded`, or it's `uv_spawn`
 * feature, or in a **child/subprocess** by `proc_open`, if `libuv` is not installed.
 * - This function needs to be prefixed with `yield`
 */
yield \file_***Any File System Command***( ...$arguments);

安装

composer require symplely/coroutine

如果可用,此版本将使用 libuv 功能。请执行以下操作之一进行安装。

对于类似 Debian 的发行版,Ubuntu...

apt-get install libuv1-dev php-pear php-dev -y

对于类似 RedHat 的发行版,CentOS...

yum install libuv-devel php-pear php-dev -y

现在有 Pecl 自动编译、安装和设置。

pecl channel-update pecl.php.net
pecl install uv-beta

对于 Windows,有好消息,通过 libuv 的本地 async 已到来。

稳定 PHP 版本的 Windows 构建可以从 PECL 获取。

直接从 https://windows.php.net/downloads/pecl/releases/uv/0.2.4/ 下载最新版本

libuv.dll 解压缩到样本目录作为 PHP 二进制可执行文件,并将 php_uv.dll 解压缩到 ext\ 目录。

在 php.ini 中启用扩展 php_sockets.dllphp_uv.dll

cd C:\Php
Invoke-WebRequest "https://windows.php.net/downloads/pecl/releases/uv/0.2.4/php_uv-0.2.4-7.2-nts-vc15-x64.zip" -OutFile "php_uv-0.2.4.zip"
#Invoke-WebRequest "https://windows.php.net/downloads/pecl/releases/uv/0.2.4/php_uv-0.2.4-7.3-nts-vc15-x64.zip" -OutFile "php_uv-0.2.4.zip"
#Invoke-WebRequest "https://windows.php.net/downloads/pecl/releases/uv/0.2.4/php_uv-0.2.4-7.4-ts-vc15-x64.zip" -OutFile "php_uv-0.2.4.zip"
7z x -y php_uv-0.2.4.zip libuv.dll php_uv.dll
copy php_uv.dll ext\php_uv.dll
del php_uv.dll
del php_uv-0.2.4.zip
echo extension=php_sockets.dll >> php.ini
echo extension=php_uv.dll >> php.ini

注意:似乎在使用 uv_spawn 时,在 WindowsLinux 上的 PHP ZTS 都存在问题。

用法

一般来说,任何带有 yield 关键字的函数/方法都将作为中断点,暂停当前例程,执行其他操作,然后返回/继续。

function main() {
    // Your initialization/startup code will need to be enclosed inside an function.
    // This is required for proper operations to start.
}

\coroutine_run(\main());

之后,请参考以下内容,examples 文件夹中的脚本。

/**
 * @see https://docs.pythonlang.cn/3/library/asyncio-task.html#timeouts
 */
include 'vendor/autoload.php';

function eternity() {
    // Sleep for one hour
    print("\nAll good!\n");
    yield \sleep_for(3600);
    print(' yay!');
}

function keyboard() {
    // will begin outputs of `needName` in 1 second
    print("What's your name: ");
    // Note: I have three Windows systems
    // - Windows 10 using PHP 7.2.18 (cli) (built: Apr 30 2019 23:32:39) ( ZTS MSVC15 (Visual C++ 2017) x64 )
    // - Windows 10 using PHP 7.1.19 (cli) (built: Jun 20 2018 23:37:54) ( NTS MSVC14 (Visual C++ 2015) x86 )
    // - Windows 7 using PHP 7.1.16 (cli) (built: Mar 28 2018 21:15:31) ( ZTS MSVC14 (Visual C++ 2015) x64 )
    // Windows 10 blocks STDIN from the beginning with no key press.
    // Windows 7 does non-blocking STDIN, if no input attempted. only after typing something it blocks.
    return yield \input_wait();
}

function needName() {
    $i = 1;
    yield \sleep_for(1);
    while(true) {
        echo $i;
        yield \sleep_for(0.05);
        $i++;
        if ($i == 15) {
            print(\EOL.'hey! try again: ');
        }
        if ($i == 100) {
            print(\EOL.'hey! try again, one more time: ');
            break;
        }
    }
}

function main() {
    yield \away(\needName());
    echo \EOL.'You typed: '.(yield \keyboard()).\EOL;

    try {
        // Wait for at most 0.5 second
        yield \wait_for(\eternity(), 0.5);
    } catch (\RuntimeException $e) {
        print("\ntimeout!");
        // this script should have exited automatically, since
        // there are no streams open, nor tasks running, this exception killed `eternity` task
        // currently, will continue to run
        // task id 2 is `ioWaiting` task, the scheduler added for listening
        // for stream socket connections
        yield \cancel_task(2);
        // This might just be because `main` is task 1,
        // and still running by the exception throw, need more testing
    }
}

\coroutine_run(\main());
/**
 * @see https://golangbot.com/goroutines/
 * @see https://play.golang.org/p/oltn5nw0w3
 */
include 'vendor/autoload.php';

function numbers() {
    for ($i = 1; $i <= 5; $i++) {
        yield \sleep_for(250 * \MS);
        print(' '.$i);
    }
}

function alphabets() {
    for ($i = 'a'; $i <= 'e'; $i++) {
        yield \sleep_for(400 * \MS);
        print(' '.$i);
    }
}

function main() {
    yield \go(\numbers());
    yield \go(\alphabets());
    yield \sleep_for(3000 * \MS);
    print(" main terminated");
}

\coroutine_run(\main());

开发

/**
 * Template for developing an library package for access
 */
public static function someName($whatever, ...$args)
{
    return new Kernel(
        function(TaskInterface $task, Coroutine $coroutine) use ($whatever, $args){
            // Use/Execute/call some $whatever with ...$args;
            //
            if ($done) {
                // will return $someValue back to the caller
                $task->sendValue($someValue);
                // will return back to the caller, the callback
                $coroutine->schedule($task);
            }
        }
    );
}

// Setup to call
function some_name($whatever, ...$args) {
    return Kernel::someName($whatever, ...$args);
}

// To use
yield \some_name($whatever, ...$args);

待办事项

  • 添加 WebSocket 支持,或真正转换/重写某些包。
  • 添加 Database 支持,基于我对 ezsql 的维护。
  • 添加更多来自其他语言的标准化示例,并转换。
  • 更新文档,以参考 Python、Go 或任何其他语言中功能类似的部分。

致谢/参考

Nikita Popov 使用协程在 PHP 中实现合作多任务。该软件包 分支 Ditaio,重构/重写。

并行类是对spatie/async的重构/重写。该并行类依赖于symplely/spawn,用于子进程的管理/执行,它使用libuvuv_spawn来启动进程。Spawn包依赖于opis/closure,用于克服PHP序列化的限制,并且当没有安装PHP-UV扩展时,使用symfony/process作为备选方案来使用proc_open启动进程。

贡献

鼓励并欢迎贡献;我总是很高兴在Github上收到反馈或pull requests :) 为错误和新特性创建Github Issues,并对您感兴趣的问题进行评论。

许可证

MIT许可证(MIT)。请参阅许可证文件以获取更多信息。