前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >浅谈非堵塞程序的理解

浅谈非堵塞程序的理解

作者头像
宣言言言
发布2019-12-15 21:39:08
6360
发布2019-12-15 21:39:08
举报

这篇文章,主要讲讲非堵塞编程带给程序的意义。

在我们谈到今天的主题之前,先来做一点基础知识的补充。

什么是I/O

我们的计算机系统架构简易可看成如下,I/O接口连接其他硬件如:网卡、键盘鼠标、磁盘等。

I代表Input,输入数据。 O代表Output,输出数据。

计算机基础架构图
计算机基础架构图

当程序需要发送网络请求或者从磁盘中读取文件等IO操作时 CPU发出指令,然后信号经过总线到达网卡或者磁盘 然后拿到数据,再经过总线到达主存中,CPU继续对主存中的数据进行操作。

CPU的执行速率:主频 比如3GHz = 一秒钟有30亿个时钟脉冲,执行一条指令一般只需要几个时钟脉冲。也就是一秒可以执行的指令经常是以亿计算的。

以网络请求为例(磁盘IO也是一样的原理),当CPU发出指令之后,想要得到结果需要经过很长的等待(比如网络延迟经常是几十ms时间,CPU都过了多少千万个时钟脉冲了)

同步、异步、堵塞、非堵塞的概念

相信看这篇文章的你也不是第一次看到这种概念,在很多文章中经常会以购物等场景做例子。

这里只做一个简单的介绍:

同步、异步分为一组概念; 堵塞、非堵塞分为一组概念;

(同步、异步):关注的是:数据的接收方式 (堵塞、非堵塞):关注的是:是否等待结果返回

这是两个分组(因为它们的关注点不同) 但是往往同步跟堵塞是一起的,异步跟非堵塞是一起的。

如果我们需要同步接收数据,肯定要让当前程序暂停,等待数据返回再做处理。 如果我们选择了异步接收数据,程序还堵塞的话那就没什么意义了,所以非堵塞模式,一般会返回发送调用请求的结果,然后程序继续执行,直到结果准备好了,再通过回调函数等方式触发程序做处理。

堵塞IO存在的不足

如果是堵塞IO的话,那么当前的进程会暂停执行,直到拿到数据才会继续执行。

文件锁堵塞

以PHP中自带的Session为例的文件锁 Session以生成文件储存的,如果同一个用户同时发起多个请求,先获取文件锁的请求可以执行,后面的拿不到文件锁,所以一直堵塞等待,假设前面的请求过了10s才执行完,后续的请求是要10s后才开始执行。

socket堵塞

写过tcp服务器的应该都会遇到这个问题

我们可以监听机器的某个端口,当有请求连接进来的时候,我们可以accept这个连接,然后读取客户端发过来的数据、发送数据回客户端等处理。

<?php
$socket = stream_socket_server("tcp://0.0.0.0:8000", $errno, $errstr);
if (!$socket) {
  echo "$errstr ($errno)<br />\n";
} else {
  // 循环接收客户端的连接
  while ($conn = stream_socket_accept($socket)) {
      $data = fread($conn, 8192); // 读取客户端发送过来的数据 读不到就一直堵塞着
      fwrite($conn, "hello world\n"); // 发送hello world
      fclose($conn);
  }
  fclose($socket);
}

以上代码实现了一个建议的TCP服务器,但是因为没有解决堵塞IO的问题,所以只能处理一个客户端的请求。

  • 当A连接进来,accept到,然后开始fread从缓冲区读取数据。 堵塞住了,进程执行暂停,等待数据结果。
  • 此时B连接进来,因为进程已经被堵塞住,所以无法被accept,更无法读取、发送数据。
  • A客户端发送了数据,进程恢复执行,开始读取,然后输出。
  • 然后才能accept B客户端(哪怕在此之前B已经发了很多数据,也只能从这个时候开始处理)。

非堵塞IO

为了让我们的网络服务器可以服务多个客户端,我们需要将程序改造为非堵塞的。

我们可以简单实现为:

  • 当A连接进来了,accept起来,存到一个列表中。
  • 继续等待监听,B连接进来了,accpet起来,存到一个列表中。
  • 多开一个线程,不断轮询连接列表,判断连接是否有发送数据过来,有的话就执行操作(比如发送数据、关闭连接)
  • 在PHP中默认没有线程操作,并且accept操作是堵塞的,但是可以设置超时时间

所以我们可以让程序每等待0.1s连接进来,然后就去轮询一次连接列表,读取数据然后操作。

<?php
$socket = stream_socket_server("tcp://0.0.0.0:8000", $errno, $errstr);
if (!$socket) {
    echo "$errstr ($errno)<br />\n";
} else {
    $conns = [];// 全局连接
    while (true){
        $conn = @stream_socket_accept($socket, 0.1); // 0.1没有连接进来就不堵塞等待了 先检测有没有客户端发数据
        if($conn!== false){
            $conns[] = $conn;
            stream_set_blocking($conn, false);
        }
        foreach ($conns as $key=>$item) {
            $data = fread($item, 8192);
            if ($data !== ''){
                fwrite($item, "hello");
                fclose($item);
                unset($conns[$key]);
            }
        }
    }
    fclose($socket);
}

以上的I/O模型是同步非堵塞 ,当客户端连接数比较多的时候,以上代码还是有很大的问题。

我们还可以将对客户端的操作逻辑进行异步执行(因为我们的实际业务逻辑肯定不只是输出hello这么简单,还要数据库操作等等)

将对客户连接的操作逻辑异步分离的话,但是accept连接还是堵塞同步的,因此可见,程序同步、异步、堵塞、非堵塞是相对的,需要按功能点和模块来分析。

我们也可以依赖扩展,比如Event等,实现异步非堵塞模型。 当有客户连接、断开、读写数据时,底层扩展会通过我们设置的回调函数触发,而不需要我们在程序代码中accpet、read(堵塞或者轮询)

可以参考简单的demo。

这不是完整的demo,并且需要安装扩展,大家了解一下使用的方式即可 有兴趣可以继续深入学习Event扩展的使用

class MyListenerConnection {
    private $bev, $base;

    public function __destruct() {
        $this->bev->free();
    }
    // 新链接进来 并且监听 这个时候就设置链接的事件回调
    public function __construct($base, $fd) {
        $this->base = $base;
        $this->bev = new EventBufferEvent($base, $fd, EventBufferEvent::OPT_CLOSE_ON_FREE);
        // 设置回调事件
        $this->bev->setCallbacks(
            array($this, "echoReadCallback"),
            array($this, "writeCallback"),
            array($this, "echoEventCallback"),
            NULL
        );

        if (!$this->bev->enable(Event::READ)) {
            echo "Failed to enable READ\n";
            return;
        }
    }
    // 读回调
    public function echoReadCallback($bev, $ctx) {
        // 在这里处理 handleRequest $bev->input就是客户端发送的数据
        $bev->output->addBuffer($bev->input);
        // $bev->output设置内容就是会发送给客户端的数据  这里原样返回
    }
    // 写回调  是输出之后才回调的 而不是在输出之前 
    public function writeCallback($bev, $ctx){
        // 释放监听 断开连接
        $bev->free();
    }

    // 除了读写之外其他事件的回调
    public function echoEventCallback($bev, $events, $ctx) {
        if ($events & EventBufferEvent::ERROR) {
            echo "Error from bufferevent\n";
        }

        if ($events & (EventBufferEvent::EOF | EventBufferEvent::ERROR)) {
            $this->__destruct();
        }
    }
}

通过这种方式,我们写一个网络服务器就很简单了,只需要给事件设置回调事件,由底层维护客户端连接的可读写状态, 这种模型是I/O复用里的epoll模型。

总结

通过上面文件锁、几种TCP服务器的写法,我们可以理解到堵塞和非堵塞程序之间的区别了。

再做一下小小的总结。

  • 同步和异步是指决定结果返回的接收方式
  • 堵塞和非堵塞是指是否需要等待结果返回
  • 如果发生磁盘IO等操作,因为CPU执行速率和总线信号传递、磁盘速率的不对等,CPU如果堵塞等待读取结果,就不能最大化地利用机器资源。
  • 非堵塞程序,可以提高机器的利用率,可以提高并发支持。
  • 常见的I/O模型有:阻塞式I/O;非阻塞式I/O;I/O复用(select和poll);异步I/O;
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019.06.29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 什么是I/O
  • 同步、异步、堵塞、非堵塞的概念
  • 堵塞IO存在的不足
  • 非堵塞IO
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档