python网络编程Twisted01 异步与同步编程

1、同步模型和异步模型

  • 下图展示了,同步单线程、同步多线程以及异步单线程三种模式下程序随着时间的推移所做的工作。这个程序有3个任务需要完成,每个任务都在等待I/O操作时阻塞自身。阻塞在I/O操作上所花费的时间已经用灰色框标示出来了。

1、单线程同步模型

  • 在单线程同步模型中,任务按照顺序执行。如果某个任务因为I/O而阻塞,其他所有的任务都必须等待,直到它完成之后它们才能依次执行。这种明确的执行顺序和串行化处理的行为是很容易推断得出的。如果任务之间并没有互相依赖的关系,但仍然需要互相等待的话这就使得程序不必要的降低了运行速度。

2、多线程同步模型

  • 在多线程中,这每个任务分别在独立的线程中执行。线程由操作系统管理,并且可以在具有多个处理器或多个内核的系统上真正并发运行,或者可以在单个处理器上交错运行。这使得当某个线程阻塞在某个资源的同时其他线程得以继续执行。与完成类似功能的单线程程序相比,这种方式更有效率,但程序员必须写代码来保护共享资源,防止其被多个线程同时访问。多线程程序更加难以推断,因为这类程序不得不通过线程同步机制如锁、可重入函数、线程局部存储或者其他机制来处理线程安全问题,如果实现不当就会导致出现微妙且令人痛不欲生的bug。

3、异步模型

  • 在异步模型程序中,任务交错执行,但仍在一个线程中。当处理I/O或者其他昂贵的操作时,它会转而执行一些仍然可以取得进展的其他任务。
  • 单线程异步系统将始终以交错方式执行,即使在多处理器系统上也是如此。注意,可以混合使用异步模型和线程模型,并在同一个系统中使用这两种模型。
  • 异步模型的基本思想是,当一个异步程序面临一个通常会在同步程序中阻塞的任务时,它将转而执行一些仍然可以取得进展的其他任务。因此,当没有任务可以取得进展时,异步程序才会“阻塞”,因此称为非阻塞程序。每次从一个任务转换到另一个任务都对应着前一个任务的完成,或者到达一个必须停止的点。有了大量潜在的阻塞任务,异步程序可以通过花费更少的总等待时间,而将大致相同的时间用于单个任务的实际工作,从而比同步程序的性能更好。

4、异步模型和同步模型的区别

  • 异步模型比线程模型更简单,因为有一个指令流和任务显式地放弃控制,而不是任意地挂起。
  • 异步模型的实现显然比同步模型更复杂。程序员必须把每个任务组织成一个小步骤序列,这些步骤间歇性地执行。如果一个任务使用另一个任务的输出,依赖的任务必须被写入以接受它的输入作为一系列的比特和片段,而不是全部在一起。
  • 由于没有实际的并行性,异步程序的执行时间可能和同步程序的执行时间一样长,也许更长因为异步程序可能表现出较差的引用局部性。

5、为什么要选择使用异步模型呢

  • 如果一个或多个任务负责为人类实现一个界面,那么通过将这些任务交织在一起,系统可以在“后台”执行其他工作的同时保持对用户输入的响应。因此,虽然后台任务可能不会执行得更快,但系统会让使用者感到更愉快。
  • 当任务被迫等待或阻塞时,异步系统的性能会明显优于同步系统,从更短的时间内完成所有任务的意义上来说,尤其优越。

6、与同步模型相比,异步模型在以下情况下性能最好:

  • 有大量的任务,所以很可能总有至少一项任务可以取得进展。
  • 这些任务执行大量的I/O,导致同步程序浪费大量时间阻塞,而其他任务可能正在运行。
  • 这些任务在很大程度上是相互独立的,因此几乎不需要任务间的通信(因此一个任务要等待另一个任务)。
  • 这些条件几乎完美地描述了客户机-服务器环境中典型的繁忙网络服务器(如web服务器)。每个任务用I/O表示一个客户端请求,其形式是接收请求并发送应答。客户端请求(主要是读取)在很大程度上是独立的。因此,网络服务器实现是异步模型的主要候选,这就是为什么Twisted首先是一个网络库的原因。

2、同步和异步网络编程的实现

1、同步(阻塞)服务端

#D:\twisted-intro-master3\blocking-server\slowpoetry3.py
import argparse, os, socket, time

def parse_args():
    usagehh = """usage: %(prog)s [options] poetry-file

This is the Slow Poetry Server, blocking edition.
Run it like this:

  python3 slowpoetry.py <path-to-poetry-file>

If you are  in the base directory of the twisted-intro package,
you could run it like this:

  python blocking-server/slowpoetry.py poetry/ecstasy.txt

to serve up John Donne's Ecstasy, which I know you want to do.
"""

    parser = argparse.ArgumentParser(usage = usagehh)

    help = "The port to listen on. Default to a random available port."
    parser.add_argument('--port', type=int, help=help)

    help = "The interface to listen on. Default is localhost."
    parser.add_argument('--iface', help=help, default='localhost')

    help = "The number of seconds between sending bytes."
    parser.add_argument('--delay', type=float, help=help, default=.7)

    help = "The number of bytes to send at a time."
    parser.add_argument('--num-bytes', type=int, help=help, default=10)

    help = "Poetry file"
    parser.add_argument('file', help=help)
    
    options = parser.parse_args()

    if not options.file:
        parser.error('Provide exactly one poetry file.')

    if not os.path.exists(options.file):
        parser.error('No such file: %s' % file)

    return options

def send_poetry(sock, file, num_bytes, delay):
    """Send some poetry slowly down the socket."""

    inputf = open(file)

    while True:
        bytes = inputf.read(num_bytes)
        bytes = bytes.encode('utf8')

        if not bytes: # no more poetry :(
            sock.close()
            inputf.close()
            return

        print('Sending %d bytes' % len(bytes))

        try:
            sock.sendall(bytes) # this is a blocking call
        except socket.error:
            sock.close()
            inputf.close()
            return

        time.sleep(delay)

def serve(listen_socket, file, num_bytes, delay):
    while True:
        sock, addr = listen_socket.accept()
        print('Somebody at %s wants poetry!' % (addr,))
        send_poetry(sock, file, num_bytes, delay)

def main():
    options = parse_args()

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind((options.iface, options.port or 0))
    sock.listen(5)

    print('Serving %s on port %s.' % (options.file, sock.getsockname()[1]))

    serve(sock, options.file, options.num_bytes, options.delay)

if __name__ == '__main__':
    main()

2、同步(阻塞)客户端

#D:\twisted-intro-master3\blocking-client\get-poetry3.py

import datetime, argparse, socket

def parse_args():
    usagehh = """usage: %(prog)s [options] [hostname]:port ...

This is the Get Poetry Now! client, blocking edition.
Run it like this:

  python3 get-poetry.py port1 port2 port3 ...

If you are in the base directory of the twisted-intro package,
you could run it like this:

  python blocking-client/get-poetry.py 10001 10002 10003

to grab poetry from servers on ports 10001, 10002, and 10003.

Of course, there need to be servers listening on those ports
for that to work.
"""

    parser = argparse.ArgumentParser(usage = usagehh)
    help = "port lists"
    parser.add_argument('port', nargs='*', help=help)

    addresses = parser.parse_args().port
    if not addresses:
        print(parser.format_help())
        parser.exit()

    def parse_address(addr):
        if ':' not  in addr:
            host = '127.0.0.1'
            port = addr
        else:
            host, port = addr.split(':', 1)

        if not port.isdigit():
            parser.error('Ports must be integers.')

        return host, int(port)

    return list(map(parse_address, addresses))

def get_poetry(address):
    """Download a piece of poetry from the given address."""

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect(address)

    poem = ''

    while True:

        # This is the 'blocking' call in this synchronous program.
        # The recv() method will block for an indeterminate period
        # of time waiting for bytes to be received from the server.

        data = sock.recv(1024)

        if not data:
            sock.close()
            break

        poem += data.decode('utf8')

    return poem

def format_address(address):
    host, port = address
    return '%s:%s' % (host or '127.0.0.1', port)

def main():
    addresses = parse_args()

    elapsed = datetime.timedelta()

    for i, address  in enumerate(addresses):
        addr_fmt = format_address(address)

        print('Task %d: get poetry from: %s' % (i + 1, addr_fmt))

        start = datetime.datetime.now()

        # Each execution of 'get_poetry' corresponds to the
        # execution of one synchronous task in Figure 1 here:
        # http://krondo.com/?p=1209#figure1

        poem = get_poetry(address)

        time = datetime.datetime.now() - start

        msg = 'Task %d: got %d bytes of poetry from %s in %s'
        print(msg % (i + 1, len(poem), addr_fmt, time))

        elapsed += time

    print('Got %d poems in %s' % (len(addresses), elapsed))

if __name__ == '__main__':
    main()

3、异步(非阻塞)客户端

#D:\twisted-intro-master3\async-client\get-poetry3.py

import datetime, errno, argparse, select, socket

def parse_args():
    usagehh = """usage: %(prog)s [options] [hostname]:port ...

This is the Get Poetry Now! client, asynchronous edition.
Run it like this:

  python get-poetry.py port1 port2 port3 ...

If you are in the base directory of the twisted-intro package,
you could run it like this:

  python async-client/get-poetry.py 10001 10002 10003

to grab poetry from servers on ports 10001, 10002, and 10003.

Of course, there need to be servers listening on those ports
for that to work.
"""

    parser = argparse.ArgumentParser(usage = usagehh)
    help = "port lists"
    parser.add_argument('port', nargs='*', help=help)
    addresses = parser.parse_args().port
    if not addresses:
        print(parser.format_help())
        parser.exit()

    def parse_address(addr):
        if ':' not  in addr:
            host = '127.0.0.1'
            port = addr
        else:
            host, port = addr.split(':', 1)
        if not port.isdigit():
            parser.error('Ports must be integers.')

        return host, int(port)
    return list(map(parse_address, addresses))

def get_poetry(sockets):
    """Download poety from all the given sockets."""

    poems = dict.fromkeys(sockets, '') # socket -> accumulated poem
    # socket -> task numbers
    sock2task = dict([(s, i + 1) for i, s  in enumerate(sockets)])
    sockets = list(sockets) # make a copy

    # we go around this loop until we've gotten all the poetry
    # from all the sockets. This is the 'reactor loop'.

    while sockets:
        # this select call blocks until one or more of the
        # sockets is ready for read I/O
        rlist, _, _ = select.select(sockets, [], [])

        # rlist is the list of sockets with data ready to read

        for sock  in rlist:
            # collect data from socket as bytes
            data = b''

            while True:
                try:
                    new_data = sock.recv(1024)
                except socket.error as e:
                    if e.args[0] == errno.EWOULDBLOCK:
                        # this error code means we would have
                        # blocked if the socket was blocking.
                        # instead we skip to the next socket
                        break
                    raise
                else:
                    if not new_data:
                        break
                    else:
                        data += new_data

            # Each execution of this inner loop corresponds to
            # working on one asynchronous task in Figure 3 here:
            # http://krondo.com/?p=1209#figure3

            task_num = sock2task[sock]

            if not data:
                sockets.remove(sock)
                sock.close()
                print('Task %d finished' % task_num)
            else:
                addr_fmt = format_address(sock.getpeername())
                msg = 'Task %d: got %d bytes of poetry from %s'
                print(msg % (task_num, len(data), addr_fmt))

            # convert network stream of bytes to unicode
            poems[sock] += data.decode('utf8')

    return poems

def connect(address):
    """Connect to the given server and return a non-blocking socket."""

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect(address)
    sock.setblocking(0)
    return sock

def format_address(address):
    host, port = address
    return '%s:%s' % (host or '127.0.0.1', port)

def main():
    addresses = parse_args()

    start = datetime.datetime.now()

    sockets = list(map(connect, addresses))

    poems = get_poetry(sockets)

    elapsed = datetime.datetime.now() - start

    for i, sock  in enumerate(sockets):
        print('Task %d: %d bytes of poetry' % (i + 1, len(poems[sock])))

    print('Got %d poems in %s' % (len(addresses), elapsed))

if __name__ == '__main__':
    main()

4、使用服务端与客户端

1、开启三个同步(阻塞)服务端

#使用10000端口,每秒发送50个字节
python3 D:\twisted-intro-master3\blocking-server\slowpoetry3.py  --port  10000 --num-bytes  50 --delay  1 D:\twisted-intro-master3\poetry\ecstasy.txt
#使用10001端口,每5秒发送500个字节
python3 D:\twisted-intro-master3\blocking-server\slowpoetry3.py  --port  10001 --num-bytes  500 --delay  5 D:\twisted-intro-master3\poetry\fascination.txt
#使用10002端口,每10秒发送5000个字节
python3 D:\twisted-intro-master3\blocking-server\slowpoetry3.py  --port  10002 --num-bytes  5000 --delay  10 D:\twisted-intro-master3\poetry\science.txt

2、开启一个同步(阻塞)客户端,依次连接服务端并下载文件

python3 D:\twisted-intro-master3\blocking-client\get-poetry3.py  10000 10001 10002

3、开启一个异步(非阻塞)客户端,同时连接3个服务端并下载文件

python3 D:\twisted-intro-master3\async-client\get-poetry3.py  10000 10001 10002

4、仔细观察

1、现在看一下异步客户机的源代码。注意它和同步客户端之间的主要区别:

  1. 异步客户机不是一次连接到一个服务器,而是一次连接到所有服务器。
  2. 通过调用setblocking(0),用于通信的套接字对象处于非阻塞模式。
  3. select模块中的select方法用于等待(阻塞),直到任何套接字准备好给我们一些数据。
  4. 当从服务器读取数据时,我们只读取尽可能多的数据,直到套接字阻塞,然后移动到下一个有数据要读取的套接字(如果有的话)。这意味着我们必须记录到目前为止从每个服务器收到的诗歌。

2、异步客户机的核心是get_poetry函数中的顶级循环。这个循环可以分解为以下步骤:

  1. 使用select等待(阻塞)所有打开的套接字,直到一个(或多个)套接字有数据要读取。
  2. 对于每个要读取数据的套接字,都要读取它,但只能读取当前可用的数据。不阻塞。
  3. 重复,直到所有的套接字都被关闭。
  • 异步客户机不能没有顶级循环——为了获得异步的好处,我们需要同时等待所有套接字,并且只处理每个套接字在任何给定迭代中能够交付的尽可能多的数据。

3、反应器模式

  • 反应器模式:等待事件发生然后处理它们的循环。
  • Twisted:一个健壮的、跨平台的反应器模式的实现,还有很多额外的功能。

  • 这个循环是一个“反应器”,因为它等待事件,然后对事件作出反应。因此,它也被称为事件循环
    • 由于反应器系统经常等待I/O,这些循环有时也被称为select循环,因为select调用用于等待I/O。
    • 在select循环中,“事件”是指套接字可用于读取或写入。
  • 请注意,select并不是等待I/O的唯一方法,它只是最古老的方法之一(因此可以广泛使用)。在不同的操作系统上有一些较新的api,它们可以做与select相同的事情,但(希望)提供更好的性能。它们都做同样的事情:取一组套接字(实际上是文件描述符)并阻塞,直到其中一个或多个套接字准备好进行I/O。
  • 请注意,可以使用select和它的同类工具简单地检查一组文件描述符是否为I/O做好了准备,而不会阻塞。这个特性允许反应式系统在循环内执行非I/O工作。但在反应式式系统中,通常所有的工作都是I/O绑定的,因此阻塞所有文件描述符会节省CPU资源。
  • 反应器模式的正真实现会将循环作为一个独立的抽象。
    1. 接受一组您感兴趣的用于执行I/O的文件描述符。
    2. 重复地告诉您任何文件描述符何时可以用于I/O。
  • 一个很好的反应堆模式的实现也会:
    1. 处理在不同系统上出现的所有奇怪的极端情况。
    2. 提供大量优秀的抽象来帮助您以最少的努力使用反应器。
    3. 提供可以开箱即用的公共协议的实现。
  • 请注意:严格地说,我们的异步客户端程序中的循环不是反应器模式,因为循环逻辑不是与特定于blocking-server3.py程序的“业务逻辑”分开实现的。它们只是混合在一起。
posted @ 2021-07-01 01:16  麦恒  阅读(233)  评论(0编辑  收藏  举报