PHP学习笔记18:协程

PHP学习笔记18:协程

PHP学习笔记18:协程_第1张图片

图源:php.net

正如Python关于协程的PEP所讲,异步编程和并发已经是编程的一个热门领域,所以无论是老派语言如Python,或者是新语言Go,要么是添加新特性以支持协程,要么是天生就对协程和并发有完整支持。

但在这方面php就相当落(la)后(kua)了。

或许这和语言的应用领域和使用方式有一些关系,php作为一个和Apache等web service紧密结合的Web开发语言,绝大部分php项目都是依托于web service处理和转发请求的,php本身并不需要花大力气去管理并发和进程,至少开发者不需要。这也就意味着协程和并发对传统的php项目可有可无。

但这也不完全没有用途,否则Swoole也就不会有商业价值。在某些追求高并发高性能而抛弃web service直接监听TCP套接字进行服务的场景(比如游戏或即时聊天服务器等),就需要协程了,而Swoole的价值正在于此。

幸运的是php8.1正式引入了一个核心的协程机制:内置的Fliber类。

虽然围绕该提案有很多讨(si)论(bi),但至少必须要引入协程这是绝大多数人的观点,至于是仅引入一个最小核心类Fliber还是完整的包含了“事件循环”、“线程调度”等的完整框架,这些问题很难在短期内社区达成一致。就我看来,有至少比没有强。

如果你还不知道什么是协程以及协程的基本概念,可以阅读Python学习笔记33:协程,虽然那篇文章讨论的是Python的协程,但其实和php的协程概念是几乎一致的,毕竟本质上Python和php一样是单线程的。

这里我会提供一个使用协程的案例,分别用php、Go lang、Python实现,以对比它们之间的差异。

php


$create_number = new Fiber(function (): void {
    Fiber::suspend();
    for ($i = 0; $i <= 10; $i++) {
        Fiber::suspend($i);
    }
    Fiber::suspend(null);
});
$double_number = new Fiber(function (Fiber $create_number): void {
    Fiber::suspend();
    while (true) {
        $number = $create_number->resume();
        if ($number === null) {
            $create_number->resume();
            Fiber::suspend(null);
            break;
        }
        Fiber::suspend($number * 2);
    }
});;
$print_number = new Fiber(function (Fiber $double_number) {
    while (true) {
        $number = $double_number->resume();
        if (null === $number) {
            $double_number->resume();
            break;
        }
        echo $number . " ";
    }
});
$create_number->start();
$double_number->start($create_number);
$print_number->start($double_number);
// 0 2 4 6 8 10 12 14 16 18 20 

这里创建了三个协程:

  • $create_number负责产出数据,示例中是1...10
  • $double_number$create_number产出的数据*2,结果是2...20
  • $print_number$double_number处理过的数据输出到屏幕

php的协程是新引入的Fiber类的实例,该类的构造方法接受一个callable类型的参数。这个参数可以是匿名函数、函数变量或者实现了__invoke的对象。callable类型可以接收参数,该参数在调用Fiber实例的start方法时传入。

php的协程由start方法激活。激活后会进入协程绑定的callable的代码执行,直到遇到Fiber::suspend()挂起,该静态方法会将当前正在运行的协程(也就是代码所在callable绑定的协程)挂起。如果suspend没有参数,会向外部传递一个null值,如果有参数,会向外传递给激活或让它恢复执行的调用方。

这也是为什么$create_number$double_number两个协程需要在首行添加Fiber::suspend(),因为需要它们在激活后挂起,直到第三个协程$print_number激活后来间接恢复前两个协程来执行“产出数据”的工作。

在这个示例中协程$print_number依赖于$double_number,而$double_number依赖于$create_number,所以在最里层的协程$create_number结束的时候,必须通知外部的$double_number,而$double_number也必须通知最外层的$print_number

我这里使用向外传递一个null值的方式通知,外部协程在检测到null后,调用$innerCoroutine->resume()来让内侧协程恢复执行,以正常退出。然后外部协程也就可以退出了。

我这里讲的还是很笼统,建议阅读Python学习笔记33:协程,我使用了时序图来说明协程的调用机制。

Python

from typing import Coroutine


def create_number():
    for num in range(11):
        yield num


def double_num(cn: Coroutine):
    while True:
        try:
            num = next(cn)
        except StopIteration:
            break
        num = num*2
        yield num


def print_num(dn: Coroutine):
    while True:
        try:
            num = next(dn)
        except StopIteration:
            break
        print("{:d} ".format(num), sep=' ', end='')


print_num(double_num(create_number()))

Python和php的协程非常相似,不过Python并没有选择使用新的类型或者关键字,而是直接让生成器演化为协程。此外Python用next()来驱动协程,并且协程在执行完毕退出时,会抛出一个StopIteration的异常。外层协程可以通过捕获该异常来判断内层协程是否执行完毕。

Go lang

package main

import (
	"fmt"
	"sync"
)

func create_num(out_chan chan<- int, swg *sync.WaitGroup) {
	defer swg.Done()
	for i := 0; i <= 10; i++ {
		out_chan <- i
	}
	close(out_chan)
}

func double_num(in_chan <-chan int, out_chan chan<- int, swg *sync.WaitGroup) {
	defer swg.Done()
	for {
		num, ok := <-in_chan
		if !ok {
			close(out_chan)
			break
		}
		num = num * 2
		out_chan <- num
	}
}

func print_num(in_chan <-chan int, swg *sync.WaitGroup) {
	defer swg.Done()
	for {
		num, ok := <-in_chan
		if !ok {
			break
		}
		fmt.Printf("%d ", num)
	}
}

func main() {
	chan1 := make(chan int)
	chan2 := make(chan int)
	var swg sync.WaitGroup
	swg.Add(1)
	go create_num(chan1, &swg)
	swg.Add(1)
	go double_num(chan1, chan2, &swg)
	swg.Add(1)
	go print_num(chan2, &swg)
	swg.Wait()
}

Go语言和前两者差别有点大,因为Go是支持多线程的,Go的协程其实叫做“Goroutine”而非"Coroutine"。goroutine是真正的多线程,只不过可以通过非缓冲通道来同步,这样就表现得像是普通协程。在使用非缓冲通道的时候,上面的异步代码大致执行过程和前两者是类似的。只不过Go需要添加一个sync.WaitGroup来实现“线程计数”,以阻塞主goroutine,等待三个子goroutine执行完毕后再退出。

参考资料

  • php官方手册-Fibers
  • 关于 PHP 8.1 的 Fiber RFC
  • PHP 8.1: Fibers
  • PHP创始人和Swoole创始人投反对票,协程提案Fiber引激辩

往期内容

  • PHP学习笔记17:迭代器和生成器
  • PHP学习笔记16:错误处理
  • PHP学习笔记15:枚举
  • PHP学习笔记14:命名空间
  • PHP学习笔记13:类和对象 V
  • PHP学习笔记12:类和对象IV
  • PHP学习笔记11:类和对象 III
  • PHP学习笔记10:类和对象 II
  • PHP学习笔记9:类和对象 I
  • PHP学习笔记8:函数
  • PHP学习笔记7:控制流
  • PHP学习笔记6:表达式和运算符
  • PHP学习笔记5:常量
  • PHP学习笔记4:变量
  • PHP学习笔记3:其它类型和类型声明
  • PHP学习笔记2:数组
  • PHP学习笔记1:基础

你可能感兴趣的:(PHP,php,开发语言,后端,协程,Fiber)