symplely/coroutine

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

1.9.9 2021-03-01 03:48 UTC

README

LinuxWindowsmacOScodecovCodacy BadgeMaintainability

这是版本 2x,它打破了版本 1x,通过将所有必需的 CONSTANTS 移动到依赖包中,更改了 namespace 中全局函数的基本设置。本版本包括新的 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尚未实现,没有看到用例,并且已经作为此协程软件包的内部部分。

目录

简介

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

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

当使用AmpReactPHP及其一些基于它们的软件包时,您不仅必须管理您提供的回调,还要管理返回的Promise对象和事件循环对象。这些库模仿了旧的JavaScript,而现在的JavaScript正朝着简单的async/await语法发展。这引发了一个问题。

易用性是什么意思?我们可以先回顾其他编程语言的实现。事实上,您已经有一个正在运行的应用程序,您希望各个部分运行得更灵敏,这意味着要做更多。

Go有它的goroutine关键字go,然后是您的函数和语句。

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

使用它们时,它们会返回您实际想要的结果,无需任何对象,无需回调,并且调用一切之后,所有内容都会继续运行。

关于这些语言,其历史、实际行为和底层概念都是相同的,调用结构简单。它们都以各种方式引用了yield迭代器的使用,该软件包依赖于这些,并使得以下内容成为可能。

要使您的代码异步,您只需在代码中放置yield,并在调用您的代码之前用yield作为前缀,以获得您想要的实际结果

通过使用此软件包,您将获得一个使用yieldPHP版本的async/await

本程序提供了一些辅助函数来整合一切。主要有 away() 函数,类似于 Python 的 create_task(),其行为类似于 Google 的 go() 关键字,这里作为别名函数 go() 包含在内。

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

此包遵循新的范式 行为编程,其概念为 B-threads功能生成器

Swoole 协程FaceBook 的 Hhvm PHP 的基本整体使用遵循与其他类似的实现方式,并在此提出。

为了进一步说明,可以参考 Python 与 Node.js 的异步并发入门 中对 NodeJSPython 的比较。

// 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 异步实现重新创建这个,它们首先需要重写才能接近。

这里的 协程 是特别设计的基于 生成器 的函数,使用 yieldyield from。当使用时,它们 控制上下文,这意味着 捕获/释放 应用程序的执行流程。

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

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

task 是一个表示一些要执行的工作的对象,可能最终会有一个结果。这些任务被 注册 在一个负责运行它们的调度器上。

由于 PHP(没有扩展的情况下)的 单线程 特性,我们不能将 task 视为一个执行单个 长运行 计算的操作——这将 阻塞 单一线程,直到任务完成。

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

调度器负责对计划的任务进行“tick”,每个计划的任务都会重复“tick”直到完成。调度器实现如何以允许所有计划的任务运行的方式进行,由调度器实现来决定。

一个 task 可以通过以下三种方式之一完成

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 点时,这些点定义了如果存在其他任务,可以发生 上下文切换 的位置,如果没有其他任务,则不会发生。这也可以视为 断点/traps,就像使用调试器时一样,当触发时,调试器介入,你可以查看状态并执行剩余的代码。

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

在此处,协程被定义为包含 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 时,Windows 和 Linux 上的 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 支持,或者真正转换/重写某些包。
  • 添加 数据库 支持,基于我对 ezsql 的维护。
  • 添加更多来自其他语言的标准化示例,并转换为其他语言。
  • 根据 Python、Go 或任何其他语言中类似的功能部分更新文档。

致谢/参考文献

尼基塔·波波夫 关于 在 PHP 中使用协程进行协同多任务处理。此包 分支Ditaio,重构/重写。

Parallel 类是对 spatie/async 的重构/重写。Parallel 类依赖于 symplely/spawn 作为依赖项,用于 子进程 管理/执行,它使用 libuvuv_spawn 来启动进程。该 Spawn 包依赖于 opis/closure,用于克服 PHP 序列化限制,并且作为回退使用 symfony/process 来启动进程,如果未安装 libuvPHP-UV 扩展。

贡献

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

许可

MIT 许可证 (MIT)。有关更多信息,请参阅 许可文件