管道和多路复用器

延迟情况是难以忍受的。现代计算机能以惊人的速度生成数据,并且高速互联网(经常是在重要的服务器之间有多个并行连接)提供了极大的带宽,但是这可恶的延迟意味着电脑花了大量时间等待数据。基于延续的编程变得越来越流行的几个原因之一。让我们考虑一些规则的程序代码:

string a = db.StringGet("a");
string b = db.StringGet("b");

按照这些关联的步骤,这看起来像:

    [req1]                         # 客户端 : 客户端库构造出一个请求1
         [c=>s]                    # 网络   : 请求1被发送到服务器
              [server]             # 服务器 : 服务器处理请求1
                     [s=>c]        # 网络   : 响应1被发送回客户端
                          [resp1]  # 客户端 : 客户端库解析响应数据1
                                [req2]
                                     [c=>s]
                                          [server]
                                                 [s=>c]
                                                      [resp2]

现在让我们突出客户端处理的部分:

[req1]
     [====waiting=====]
                      [resp1]
                            [req2]
                                 [====waiting=====]
                                                  [resp2]

记住这是不可测量的,如果是用时间来衡量,那么会一直等待下去(一直耗费时间在等待处理)。

管道

由于这个原因,很多Redis客户端允许你利用管道,处理发送多个消息而无需等待每一个的回复,并且当消息进来的时候,回复的处理将会延后。在.NET中,一个操作可以被初始化且尚未完成;在完成后或者发生错误后由TPL通过 Task/Task<T> 的API封装。本质上,Task<T> 表示的是一个"将来可能的T类型的值"(非泛型 Task 实际上是 Task<void>)。你可以二选其一:

  • .Wait() (阻塞执行,直到任务完成)
  • .ContinueWith(...)或者await (创建一个在目标任务完成时异步执行的延续任务)

例如:下面是Redis客户端利用管道的示例代码:

var aPending = db.StringGetAsync("a");
var bPending = db.StringGetAsync("b");
var a = db.Wait(aPending);
var b = db.Wait(bPending);

注意:在这里我使用了 db.Wait 因为他会自动的应用同步超时配置,如你你喜欢的话,你也可以使用 aPending.Wait() 或者Task.WaitAll(aPending, bPending);使用管道允许我们在网络中立即得到两个请求,从而消除大部分的延时。此外,它也可以帮助我们减少包碎片:20个请求单独的发送(等待每个响应)至少需要20个包,但是在管道中发送20个请求只需要少数几个包(甚至只需要一个包)。

即发即弃

一个特别的管道案例是当我们不关心操作的响应,允许代码继续执行且排队操作是在后台处理的时候。这通常意味着我们能把并发工作放在来自一个单独调用的连接中。我们可以使用 flags 参数来实现:

// 可调期限
db.KeyExpire(key, TimeSpan.FromMinutes(5), flags: CommandFlags.FireAndForget);
var value = (string)db.StringGet(key);

FireAndForget 标记会使客户端库去正常的排队工作,但是会立即返回一个默认值(KeyExpire 会返回一个 bool 类型,这将返回 false,因为默认值是 false - 然而返回的是毫无意义的值,我们应该忽略)。*Async 方法也会返回一个已完成的 Task<T>作为默认值(或者一个已完成的 Task 作为 void 返回 )。

多路复用(Multiplexing)

使用管道处理技术是非常好的,但是我们经常单独使用阻塞代码仅去取一个单独的值(或者可能只执行一些操作,这取决于各自的需要)。这意味着我们仍然有这样一个问题:我们花费大量的时间去等待数据从客户端传输到服务器端。现在我们考虑一个繁忙的应用,这可能是一个web服务。这类应用通常都具有高并发性,当你有20个并行应用请求所有需要的数据,你可能想旋转(spinning up)这20个连接,或者你可以同步访问一个单独连接(这意味着最后的调用者需要等待前面19个全部执行完成才开始)。或者作为一个妥协方式,也许是个出租5个连接的连接池--不管你怎么做,都会有大量的等待操作。StackExchange.Redis 不需要那样做;反而,它为你做了大量的工作,通过多路复用单个连接,使你可以有效的利用空余的时间。当不同的调用方同时访问时,它会自动使用管道分离访问请求,所以无论使用阻塞方式或者异步方式访问,这些工作都是被管道处理的。因此我们可以有10或者20个先前的"get a and b"的场景(来自不同应用的请求),并且它们会尽快的取得连接。从本质上讲,它填补了 waiting 时间与其他调用方的工作。

因此,StackExchange.Redis不会提供(并将永远不会提供)"阻塞弹出(blocking pops)"(BLPOPBRPOP 以及 BRPOPLPUSH) - 因为这将允许一个单独的调用方拖延整个多路复用器,进而阻塞所有的调用方。 StackExchange.Redis 需要保持的工作是为了验证某个事务的前提条件,这就是为什么StackExchange.Redis封装了这样的条件在内部管理 Condition 实例。更多事务信息。如果你想要"阻塞弹出(blocking pops)",那么我强烈建议你考虑使用发布/订阅功功能:

sub.Subscribe(channel, delegate {
    string work = db.ListRightPop(key);
    if (work != null) Process(work);
});
//...
db.ListLeftPush(key, newWork, flags: CommandFlags.FireAndForget);
sub.Publish(channel, "");

注意:无需阻塞操作即可达到相同的目的有:

  • 数据不是通过发布/订阅发送的;发布/订阅API仅被用来通知工人来检查更多的工作
  • 如果没有工人,那么新项仍然在缓冲列表中,工作不会执行
  • 仅有一个工人能弹出一个值;当消费者多于生产者,一些消费者将得到通知然后发现没有什么可做的
  • 当你重新启动工人,你应该假设有积压工作可以处理
  • 除此之外,对于阻塞弹出的语义是相同的

并发

应该注意的是管道/多路复用器/future-value 等方式与基于 延续的异步代码也是做得非常好的;例如:

string value = await db.StringGet(key);
if (value == null) {
    value = await ComputeValueFromDatabase(...);
    db.StringSet(key, value, flags: CommandFlags.FireAndForget);
}
return value;
时间: 2024-10-09 05:34:23

管道和多路复用器的相关文章

StackExchange.Redis 文档翻译

ServiceStack.Redis从4.0版本开始收费使用,好在好的东西不存在没有开源免费的: 大名鼎鼎的Stack Overflow就提供了它的Redis客户端库:StackExchange.Redis免费给我们使用. 看到网上很少有关于StackExchange.Redis的翻译,个人本着学习共享精神翻译了一下, 翻译应该有一些错误(由于英语水平有限,所以请读者原谅), 读者可以综合原文阅读,如果发现错误可以告诉我,我会改正. 目录 基本用法 配置 管道和多路复用器 键,值以及通道 事务

Linux基础入门--IO重定向及管道

IO重定向及管道 一直都提到,程序:指令+数据 其实程序也有IO,数据的来源有多个地方:文件.外部 可用于输入的设备:文件(linux一切皆文件) 键盘设备.文件系统上的常规文件加载内容.网卡等: 可用于输出的设备:文件(linux一切皆文件) 显示器.文件系统上的常规文件输出保存.网卡等: 程序的数据流有三种: 输入的数据流:<-- 标准输入(stdin),是键盘: 输出的数据流:--> 标准输出(stdout),显示器: 错误输出流:  --> 错误输出(stderr),显示器: f

Java NIO (五) 管道 (Pipe)

Java NIO 管道是2个线程之间的单向数据连接.Pipe有一个source通道和一个sink通道.数据会被写到sink通道,从source通道读取. 如下图: 向管道写数据: 从管道读数据: 1. 从读取管道的数据,需要访问source通道. 2. 调用source通道的read()方法来读取数据

004-用户、组、权限管理命令及bash配置文件、管道、重定向、grep

l  1.用户管理命令 1.)创建用户命令 useradd  [OPTIONS] USERNAME -u(UID):指定用户UID -g(GID):指定基本组 -G( group,...):指定附加组 -c"COMMENT":指定注释信息 -d(home_dir):指定家目录/path/to/directory -s:指定SHELL路径(/etc/shells指定了当前系统可用的安全shell) -m –k:强行为用户创建家目录 -M:不创建用户家目录 /etc/login.defs

探秘linux-文件管理(inode理解)及管道和IO重定向

一.文件管理 1.Linux系统上各主要目录的简介 / 根,所有文件的起点 bin 存放操作系统启动时的引导程序,以及操作系统内核文件 boot 存放操作系统启动时的引导程序,以及操作系统内核文件 dev 存放设备文件和特殊文件(如字符设备) etc 存放配置文件的目录 home 普通用户的家目录默认都在此目录下 lib 存放系统库和内核模块文件 (/lib/modules) lib64 存放x86_64位系统上共享库文件 media 系统上提供的设备挂载点 misc 系统上提供的设备挂载点 m

通过重建Hosting系统理解HTTP请求在ASP.NET Core管道中的处理流程[下]:管道是如何构建起来的?

在<中篇>中,我们对管道的构成以及它对请求的处理流程进行了详细介绍,接下来我们需要了解的是这样一个管道是如何被构建起来的.总的来说,管道由一个服务器和一个HttpApplication构成,前者负责监听请求并将接收的请求传递给给HttpApplication对象处理,后者则将请求处理任务委托给注册的中间件来完成.中间件的注册是通过ApplicationBuilder对象来完成的,所以我们先来了解一下这究竟是个怎样的对象.[本文已经同步到<ASP.NET Core框架揭秘>之中] [

标准的I/O和管道使用方法

标准的I/O和管道 程序:指令+数据 系统自带了许多二进制程序,这些二进制程序共有两部分组成,一部分是指令,一部分是数据. 指令就是指二进制程序中的内容,然而程序的运行,也需要数据来支撑. 标准输出  STDOUT  -1   standard  output 输入命令所呈现在屏幕上的内容称之为标准输出,但是不是所有的命令都有标准输出.例如cd  rm后面不跟参数时,就没有标准输出. 标准输出的默认设备就是当前终端窗口. 标准错误  STDERR  -2  standard  error 当命令

linux中的管道

管道是一种最基本的IPC机制,由pipe函数创建: #include <unistd.h> int pipe(int filedes[2]); 调用pipe函数就是在内核区开辟一块缓冲区(称为管道).filedes[0]指向管道的读端,filedes[1]指向管道的写端.管道实际上就是一个打开的文件.pipe函数成功返回0,失败返回-1. 如何用管道实现两个进程间的通信? 1.父进程调用pipe函数开辟管道,得到两个文件描述符指向管道的两端. 2.父进程调用fork()创建子进程,那么子进程也

管道及其容量

1.管道 管道是一种最基本的IPC机制,由pipe函数创建: #include <unistd.h> int pipe(int filedes[2]); 调用pipe函数时在内核中开辟一块缓冲区(称为管道)用于通信,它有一个读端一个写端,然后通过filedes参数传出给用户程序两个文件描述符,filedes[0]指向管道的读端,filedes[1]指向管道的写端.所以管道在用户程序看起来就像一个打开的文件,通过read(filedes[0]);或者write(filedes[1]);向这个文件