zoukankan      html  css  js  c++  java
  • Swoole 协程与 Go 协程的区别

    进程、线程、协程的概念#

    • 进程是什么?

    进程就是应用程序的启动实例。
    例如:打开一个软件,就是开启了一个进程。
    进程拥有代码和打开的文件资源,数据资源,独立的内存空间。

    • 线程是什么?

    线程属于进程,是程序的执行者。
    一个进程至少包含一个主线程,也可以有更多的子线程。
    线程有两种调度策略,一是:分时调度,二是:抢占式调度。

    • 协程是什么?

    协程是轻量级线程, 协程的创建、切换、挂起、销毁全部为内存操作,消耗是非常低的。
    协程是属于线程,协程是在线程里执行的。
    协程的调度是用户手动切换的,所以又叫用户空间线程。
    协程的调度策略是:协作式调度。

    Swoole 协程#

    • Swoole 的协程客户端必须在协程的上下文环境中使用。
    Copy
    // 第一种情况:Request 回调本身是协程环境
    $server->on('Request', function($request, $response) {
        // 创建 Mysql 协程客户端
        $mysql = new SwooleCoroutineMySQL();
        $mysql->connect([]);
        $mysql->query();
    });
    
    // 第二种情况:WorkerStart 回调不是协程环境
    $server->on('WorkerStart', function() {
        // 需要先声明一个协程环境,才能使用协程客户端
        go(function(){
            // 创建 Mysql 协程客户端
            $mysql = new SwooleCoroutineMySQL();
            $mysql->connect([]);
            $mysql->query();
        });
    });
    
    • Swoole 的协程是基于单线程的, 无法利用多核CPU,同一时间只有一个在调度。
    Copy
    // 启动 4 个协程
    $n = 4;
    for ($i = 0; $i < $n; $i++) {
        go(function () use ($i) {
            // 模拟 IO 等待
            Co::sleep(1);
            echo microtime(true) . ": hello $i " . PHP_EOL;
        });
    };
    echo "hello main 
    ";
    
    // 每次输出的结果都是一样
    $ php test.php 
    hello main 
    1558749158.0913: hello 0 
    1558749158.0915: hello 3 
    1558749158.0915: hello 2 
    1558749158.0915: hello 1
    
    • Swoole 协程使用示例及详解
    Copy
    // 创建一个 Http 服务
    $server = new SwooleHttpServer('127.0.0.1', 9501, SWOOLE_BASE);
    
    // 调用 onRequest 事件回调函数时,底层会调用 C 函数 coro_create 创建一个协程,
    // 同时保存这个时间点的 CPU 寄存器状态和 ZendVM stack 信息。
    $server->on('Request', function($request, $response) {
        // 创建一个 Mysql 的协程客户端
        $mysql = new SwooleCoroutineMySQL();
        
        // 调用 mysql->connect 时发生 IO 操作,底层会调用 C 函数 coro_save 保存当前协程的状态,
        // 包括 Zend VM 上下文以及协程描述的信息,并调用 coro_yield 让出程序控制权,当前的请求会挂起。
        // 当协程让出控制权之后,会继续进入 EventLoop 处理其他事件,这时 Swoole 会继续去处理其他客户端发来的 Request。
        $res = $mysql->connect([
            'host'     => '127.0.0.1',
            'user'     => 'root',
            'password' => 'root',
            'database' => 'test'
        ]);
        
        // IO 事件完成后,MySQL 连接成功或失败,底层调用 C 函数 coro_resume 恢复对应的协程,恢复 ZendVM 上下文,继续向下执行 PHP 代码。
        if ($res == false) {
            $response->end("MySQL connect fail");
            return;
        }
        
        // mysql->query 的执行过程和 mysql->connect 一致,也会进行一次协程切换调度
        $ret = $mysql->query('show tables', 2);
        
        // 所有操作完成后,调用 end 方法返回结果,并销毁此协程。
        $response->end('swoole response is ok, result='.var_export($ret, true));
    });
    
    // 启动服务
    $server->start();
    

    Go 的协程 goroutine#

    goroutine 是轻量级的线程,Go 语言从语言层面就支持原生协程。
    Go 协程与线程相比,开销非常小。
    Go 协程的堆栈开销只用2KB,它可以根据程序的需要增大和缩小,
    而线程必须指定堆栈的大小,并且堆栈的大小都是固定的。

    goroutine 是通过 GPM 调度模型实现的。
    M: 表示内核级线程,一个 M 就是一个线程,goroutine 跑在 M 之上的。
    G: 表示一个 goroutine,它有自己的栈。
    P: 全称是 Processor,处理器。它主要用来执行 goroutine 的,同时它也维护了一个 goroutine 队列。

    Go 在 runtime、系统调用等多个方面对 goroutine 调度进行了封装和处理,当遇到长时间执行或进行系统调用时,
    会主动把当前协程的 CPU 转让出去,让其他协程调度执行。

    • Go 语言原生层面就支持协层,不需要声明协程环境。
    Copy
    package main
    
    import "fmt"
    
    func main() {
        // 直接通过 Go 关键字,就可以启动一个协程。
    	go func() {
    		fmt.Println("Hello Go!")
    	}()
    }
    
    • Go 协程是基于多线程的,可以利用多核 CPU,同一时间可能会有多个协程在执行。
    Copy
    package main
    
    import (
    	"fmt"
    	"time"
    )
    
    func main() {
        // 设置这个参数,可以模拟单线程与 Swoole 的协程做比较
        // 如果这个参数设置成 1,则每次输出的结果都一样。
    	// runtime.GOMAXPROCS(1)
    
        // 启动 4 个协程
    	var i int64
    	for i = 0; i < 4; i++ {
    		go func(i int64) {
    		    // 模拟 IO 等待
    			time.Sleep(1 * time.Second)
    			fmt.Printf("hello %d 
    ", i)
    		}(i)
    	}
    
    	fmt.Println("hello main")
    
        // 等待其他的协程执行完,如果不等待的话,
        // main 执行完退出后,其他的协程也会相继退出。
    	time.Sleep(10 * time.Second)
    }
    
    // 第一次输出的结果
    $ go run test.go
    hello main
    hello 2 
    hello 1 
    hello 0 
    hello 3 
    
    // 第二次输出的结果
    $ go run test.go
    hello main
    hello 2 
    hello 0 
    hello 3 
    hello 1 
    
    // 依次类推,每次输出的结果都不一样
    
    • go 协程使用示例及详解
    Copy
    package main
    
    import (
    	"fmt"
    	"github.com/jinzhu/gorm"
    	"net/http"
    	"time"
    )
    import _ "github.com/go-sql-driver/mysql"
    
    func main() {
    	dsn := fmt.Sprintf("%v:%v@(%v:%v)/%v?charset=utf8&parseTime=True&loc=Local",
    		"root",
    		"root",
    		"127.0.0.1",
    		"3306",
    		"fastadmin",
    	)
    	db, err := gorm.Open("mysql", dsn)
    	if err != nil {
    		fmt.Printf("mysql connection failure, error: (%v)", err.Error())
    		return
    	}
    	db.DB().SetMaxIdleConns(10)  // 设置连接池
    	db.DB().SetMaxOpenConns(100) // 设置与数据库建立连接的最大数目
    	db.DB().SetConnMaxLifetime(time.Second * 7)
    
    	http.HandleFunc("/test", func(writer http.ResponseWriter, request *http.Request) {
    		// http Request 是在协程中处理的
    		// 在 Go 源码 src/net/http/server.go:2851 行处 `go c.serve(ctx)` 给每个请求启动了一个协程
    		var name string
    		row := db.Table("fa_auth_rule").Where("id = ?", 1).Select("name").Row()
    		err = row.Scan(&name)
    		if err != nil {
    			fmt.Printf("error: %v", err)
    			return
    		}
    		fmt.Printf("name: %v 
    ", name)
    	})
    	http.ListenAndServe("0.0.0.0:8001", nil)
    }
    

    案例分析#

    背景:

    在我们的积分策略服务系统中,使用到了 mongodb 存储,但是 swoole 没有提供 mongodb 协程客户端。 那么这种场景下,在连接及操作 Mongodb 时会发生同步阻塞,无法发生协程切换,导致整个进程都会阻塞。在这段时间内,进程将无法再处理新的请求,这使得系统的并发性大大降低。

    使用同步的 mongodb 客户端

    Copy
    $server->on('Request', function($request, $response) {
        // swoole 没有提供协程客户端,那么只能使用同步客户端
        // 这种情况下,进程阻塞,无法切换协程
        $m = new MongoClient();    // 连接到mongodb
        $db = $m->test;            // 选择一个数据库
        $collection = $db->runoob; // 选择集合
        // 更新文档
        $collection->update(array("title"=>"MongoDB"), array('$set'=>array("title"=>"Swoole")));
        $cursor = $collection->find();
        foreach ($cursor as $document) {
            echo $document["title"] . "
    ";
        }
    }}
    

    通过使用 Server->taskCo 来异步化对 mongodb 的操作

    Copy
    $server->on('Task', function (swoole_server $serv, $task_id, $worker_id, $data) {
        $m = new MongoClient();    // 连接到mongodb
        $db = $m->test;            // 选择一个数据库
        $collection = $db->runoob; // 选择集合
        // 更新文档
        $collection->update(array("title"=>"MongoDB"), array('$set'=>array("title"=>"Swoole")));
        $cursor = $collection->find();
        foreach ($cursor as $document) {
            $data = $document["title"];
        }
        return $data;
    });
    
    $server->on('Request', function ($request, $response) use ($server) {
        // 通过 $server->taskCo() 把对 mongodb 的操作,投递到异步 task 中。
        // 投递到异步 task 后,将发生协程切换,可以继续处理其他的请求,提供并发能力。
        $tasks[] = "hello world";
        $result = $server->taskCo($tasks, 0.5);
        $response->end('Test End, Result: '.var_export($result, true));
    });
    

    上面两种使用方式就是 Swoole 中常用的方法了。
    那么我们在 Go 中怎么处理这种同步的问题呢 ?

    实际上在 Go 语言中就不用担心这个问题了,如我们之前所说到的,
    Go 在语言层面就已经支持协程了,只要是发生 IO 操作,网络请求都会发生协程切换。
    这也就是 Go 语言天生以来就支持高并发的原因了。

    Copy
    package main
    
    import (
    	"fmt"
    	"gopkg.in/mgo.v2"
    	"net/http"
    )
    
    func main() {
    	http.HandleFunc("/test", func(writer http.ResponseWriter, request *http.Request) {
    		session, err := mgo.Dial("127.0.0.1:27017")
    		if err != nil {
    			fmt.Printf("Error: %v 
    ", err)
    			return
    		}
    		session.SetMode(mgo.Monotonic, true)
    		c := session.DB("test").C("runoob")
    		fmt.Printf("Connect %v 
    ", c)
    	})
    	http.ListenAndServe("0.0.0.0:8001", nil)
    }
    

    并行:同一时刻,同一个 CPU 只能执行同一个任务,要同时执行多个任务,就需要有多个 CPU。
    并发:CPU 切换时间任务非常快,就会感觉到有很多任务在同时执行。

    协程 CPU 密集场景调度#

    我们上面说到都是基于 IO 密集场景的调度。
    那么如果是 CPU 密集型的场景,应该怎么处理呢?

    在 Swoole v4.3.2 版本中,已经支持了协程 CPU 密集场景的调度。
    想要支持 CPU 密集调度,需要在编译时增加编译选项 --enable-scheduler-tick 开启 tick 调度器。
    其次还需要我们手动声明 declare(tick=N) 语法功能来实现协程调度。

    Copy
    <?php
    declare(ticks=1000);
    
    $max_msec = 10;
    SwooleCoroutine::set([
        'max_exec_msec' => $max_msec,
    ]);
    
    $s = microtime(1);
    echo "start
    ";
    $flag = 1;
    go(function () use (&$flag, $max_msec){
        echo "coro 1 start to loop for $max_msec msec
    ";
        $i = 0;
        while($flag) {
            $i ++;
        }
        echo "coro 1 can exit
    ";
    });
    
    $t = microtime(1);
    $u = $t-$s;
    echo "shedule use time ".round($u * 1000, 5)." ms
    ";
    go(function () use (&$flag){
        echo "coro 2 set flag = false
    ";
        $flag = false;
    });
    echo "end
    ";
    
    // 输出结果
    start
    coro 1 start to loop for 10 msec
    shedule use time 10.2849 ms
    coro 2 set flag = false
    end
    coro 1 can exit
    

    Go 在 CPU 密集运算时,有可能导致协程无法抢占 CPU 会一直挂起。
    这时候就需要显示的调用代码 runtime.Gosched() 挂起当前协程,让出 CPU 给其他的协程。

    Copy
    package main
    
    import (
    	"fmt"
    	"time"
    )
    
    func main() {
        // 如果设置单线程,则第一个协程无法让出时间片
        // 第二个协程一直得不到时间片,阻塞等待。
        // runtime.GOMAXPROCS(1)
    
    	flag := true
    
    	go func() {
    		fmt.Printf("coroutine one start 
    ")
    		i := 0
    		for flag {
    			i++
    			// 如果加了这行代码,协程可以让时间片
    			// 这个因为 fmt.Printf 是内联函数,这是种特殊情况
    			// fmt.Printf("i: %d 
    ", i)
    		}
    		fmt.Printf("coroutine one exit 
    ")
    	}()
    
    	go func() {
    		fmt.Printf("coroutine two start 
    ")
    		flag = false
    		fmt.Printf("coroutine two exit 
    ")
    	}()
    
    	time.Sleep(5 * time.Second)
    	fmt.Printf("end 
    ")
    }
    
    // 输出结果
    coroutine one start 
    coroutine two start 
    coroutine two exit 
    coroutine one exit 
    end 
    

    注:time.sleep() 模拟 IO 操作,for i++ 模拟 CPU 密集运算。

  • 相关阅读:
    Android开发-MediaRecorder使用
    ScrollView嵌套ListView只显示一行的解决方法
    Android 通知栏Notification的整合 全面学习 (一个DEMO让你完全了解它)
    Dialog向Activity传递数据
    Android WebView使用深入浅出
    Android四大基本组件介绍与生命周期
    Java泛型的应用
    Java final关键字特点
    Java static关键字特点
    Java匿名内部类
  • 原文地址:https://www.cnblogs.com/liliuguang/p/13927140.html
Copyright © 2011-2022 走看看