× 警告!旧版文档已经暂停维护,请查看新版文档。点击前往新版文档

Swoole4 协程与 Go 协程有哪些区别

Swoole4Go协程在设计上是完全一致的,均是stackful的,每个协程拥有独立的运行栈。协程调度器使用汇编代码,切换协程上下文。

Swoole4Go协程在实现细节上存在一些差异。主要是以下几方面:

多线程

  • Swoole4的协程调度器是单线程的,因此不存在数据同步问题,同一时间只会有一个协程在运行
  • Go协程调度器是多线程的,同一时间可能会有多个协程同时执行

因此在Swoole4协程中操作全局变量是不需要加锁的。而Go的程序由于依然是类似Java的多线程模式,因此务必要对临界资源加锁,避免出现数据同步问题。或者使用官方sync包提供的各种并发容器。

实际上Gochan和并发容器,底层仍然使用了Mutex进行锁操作,锁的争抢是普遍存在的。

Swoole4由于是单线程多进程的,底层没有使用任何Mutex锁,不存在锁的争抢。 同样带来的问题是,没有超全局变量。只有进程级全局变量,读写PHP全局变量只在当前进程内有效。如果希望多进程共享数据,有3种解决方案:

  • 使用TableAtomic对象,或者其他共享内存数据结构
  • 使用IPC进程间通信
  • 借助存储实现数据的共享和中转,如RedisMySQL文件操作

Defer

Swoole4提供的defer关键词与Godefer存在差异。

Godefer是绑定函数的,在当前函数退出时会执行defer任务。这是由于Go没有析构函数,可能会出现资源泄漏。而PHP是有析构函数的,所有资源类对象,析构时会自动释放。

Go

func test() {
db := new(database)
  close := db.connect()
  defer close()

  fmt.Println("query db...")
}

由于db对象在gc时仅释放内存,没有对应的析构函数释放连接资源,可能会产生资源泄漏。因此需要增加defer任务关闭连接释放资源。

PHP

function test() {
    $db = new Database;
    $db->connect('127.0.0.1', 6379);
    $db->query($sql);
}

上面代码不会产生资源泄漏,因为PHP有基于引用计数的gc和析构函数,在函数退出时,db对象销毁,析构函数中会自动执行$db->close()。使用defer反而是多次一举。

function () {
    $db = new Database;
    $db->connect('127.0.0.1', 6379);
    $db->query($sql);
    defer(function () use ($db) {
        $db->close();
    });
}
  • 函数退出时,$db对象被defer引用了,因此不会析构
  • defer任务执行完毕,引用解除,对象析构。在析构方法中,因为连接已关闭,不会再执行$db->close()

Swoole4defer设计为在协程退出时一起执行,在多层函数调用嵌套中添加大量defer任务,与Coroutine是绑定的。在这个Coroutine结束时,会按照先进后出的顺序,执行defer任务。

共用 Socket 资源

Go的程序中,可以多个协程同时并发地去读同一个Socket,底层完成了协程的排队和调度。

这要求开发者清楚自己的行为,否则可能会产生逻辑错误。

Go

func test() {
    db := new(database)
    close := db.connect()

    go func(db) {
        db.query(sql);
    } (db);

    go func(db) {
        db.query(sql);
    } (db);
}

Go是允许这样操作的,实际上这个可能会存在严重问题。socket读写操作产生并发,可能产生数据包错乱。

Swoole中禁止了这种行为。不允许多个协程同时读取同一个Socket。否则会产生致命错误。

PHP

function test() {
    $db = new Database;
    $db->connect('127.0.0.1', 6379);

    go(function () use ($db) {
        $db->query($sql);
    });

    go(function () use ($db) {
        $db->query($sql);
    });
}

以上代码中有2个协程同时操作$db对象,可能会产生严重错误。底层会直接抛出致命错误,错误信息为:

"%s has already been bound to another coroutine#%ld, 
reading or writing of the same socket in multiple coroutines at the same time is not allowed."

错误码:SW_ERROR_CO_HAS_BEEN_BOUND

使用ChannelSplQueue实现连接池,管理资源对象,就可以很好地解决此问题。


  • 兆子本人

    OnWorkerStart 中创database客户端,在Onreceive中调用客户端,会不会有多个协程同时操作¥db对象的情况产生。

  • cslistener

    @兆子本人 个人理解,如果共用的是一个同步阻塞型客户端比如PDO连接,则没有这个问题.因为正在运行的协程会被PDO一系列操作阻塞,这时是无法切换到其他协程的.