Fork me on GitHub
22/7/2006

От Perl-скрипта к Twisted-приложению: Делаем приложение асинхронным

В прошлый раз был создан "черновик" Twisted-приложения, в котором описание протокола разделено с логикой получения и преобразования данных из БД. Однако, у написанного варианта есть существенный недостаток: он не позволяет организовать одновременный доступ нескольких клиентов.

Демонстрирую: ставлю паузу в DummyClient в 20 секунд, пишу многопоточного клиента (взял пример многопоточного приложения у Марка Лутца):

import thread
import telnetlib
import datetime
import time

HOST = "localhost"
PORT = 3000

def connect_to_twisted_pythy(sending_data, myId):
    stdout_mutex.acquire()
    print "send @", datetime.datetime.now(), sending_data
    stdout_mutex.release()
    tn = telnetlib.Telnet(host=HOST, port=PORT)
    tn.write(sending_data+'\\r\\n')
    data = tn.read_some()
    tn.write('\\r\\n')
    tn.close()
    stdout_mutex.acquire()
    print "receive @", datetime.datetime.now(), data
    stdout_mutex.release()
    exit_mutexes[myId] = 1

stdout_mutex = thread.allocate_lock()
exit_mutexes = [0]*2
sdata = ['12345678901234567890','abcdefghijklmnopqrst']
for d in sdata:
    thread.start_new_thread(connect_to_twisted_pythy, (d,sdata.index(d)))
while 0 in exit_mutexes:
    time.sleep(1)
    pass

И запускаю его:

send @ 2006-07-22 22:07:35.654955 12345678901234567890
send @ 2006-07-22 22:07:35.660285 abcdefghijklmnopqrst
receive @ 2006-07-22 22:07:55.659855 dummypacketmaker012345678Dummy_12345
receive @ 2006-07-22 22:08:15.661068 dummypacketmaker012345678Dummy_klmno

В логах сервера вижу

2006/07/22 22:07 OMSST [TwistedPythy.proto.PythyFactory] got connection from ('127.0.0.1', 2282)
2006/07/22 22:07 OMSST [PythyProto,0,127.0.0.1] data received from ('127.0.0.1', 2282): `12345678901234567890'
2006/07/22 22:07 OMSST [PythyProto,0,127.0.0.1] data send: dummypacketmaker012345678Dummy_12345
2006/07/22 22:07 OMSST [TwistedPythy.proto.PythyFactory] got connection from ('127.0.0.1', 2283)
2006/07/22 22:07 OMSST [PythyProto,1,127.0.0.1] data received from ('127.0.0.1', 2283): `abcdefghijklmnopqrst'
2006/07/22 22:08 OMSST [PythyProto,1,127.0.0.1] data send: dummypacketmaker012345678Dummy_klmno

Очевидно, что второй клиент ждал не 20, а 40 секунд - 20 за себя, и 20 за "того парня" - первый пакет. И хотя отослали они одновременно, по логам сервера ясно видно, что сервер принял запросы последовательно, т.е. вначале полностью обработал первый и только потом взялся за второй. Нас так, конечно, нисколько не устраивает. Иначе говоря, наш метод getClient оказался блокирующим. В некоторых случаях удается переписать такой код на неблокирующий (асинхронный), однако есть одно ухищрение, позволяющее интегрировать блокирующий код в Twisted-приложение. Это deferredToThread, выделяющее блокирующий код в отдельный поток и "оборачивающий" этот поток в Deferred.

Deferred - это специальный объект в Twisted, который и организует асинхронное взаимодействие. Принцип примерно таков: функция возвращает не значение, а некий отложенный результат - "как закончу, сообщу" - к которому "навешивается" цепочка callback- и errback-функций. Callback-функции выполняются при удачном завершении, errback - при не удачном.

Итак, переписываю протокол для выделения блокирующего кода в отдельный поток:

from twisted.python import log
from twisted.internet import threads

# полагается, что PythyProto у нас либо импортирован,
# либо находится в том же модуле

class AsyncPythyProto(PythyProto):
    """Simple text demo protocol with async (deferred) method for fetching data"""

    def lineReceived(self, line):
        log.msg("data received from %s: `%s'" % (str(self.transport.client), line))
        if line == '':
            self.sendLine("Good bye")
            self.transport.loseConnection()
            return
        agr = line[10:15]
        deferred = threads.deferToThread(self.factory.clients.getClient, agr)
        deferred.addCallback(self._cbGetCleint)

    def _cbGetClient(self, result):
        self.sendAnswer(result)

Соответствующим образом изменяю тип протокола в PythyFactory (точнее, наследую от него AsyncPythyFactory, и уже в нем меняю), запускаю:

send @ 2006-07-22 22:35:59.451674 12345678901234567890
send @ 2006-07-22 22:35:59.459583 abcdefghijklmnopqrst
receive @ 2006-07-22 22:36:19.466258 dummypacketmaker012345678Dummy_klmno
receive @ 2006-07-22 22:36:19.466707 dummypacketmaker012345678Dummy_12345

и, соответственно, в логах сервера:

2006/07/22 22:35 OMSST [TwistedPythy.proto.AsyncPythyFactory] got connection from ('127.0.0.1', 4187)
2006/07/22 22:35 OMSST [AsyncPythyProto,0,127.0.0.1] data received from ('127.0.0.1', 4187): `12345678901234567890'
2006/07/22 22:35 OMSST [TwistedPythy.proto.AsyncPythyFactory] got connection from ('127.0.0.1', 4188)
2006/07/22 22:35 OMSST [AsyncPythyProto,1,127.0.0.1] data received from ('127.0.0.1', 4188): `abcdefghijklmnopqrst'
2006/07/22 22:36 OMSST [-] data send: dummypacketmaker012345678Dummy_12345
2006/07/22 22:36 OMSST [-] data send: dummypacketmaker012345678Dummy_klmno

Чего и добивался.

Как водится, код можно взять на RapidShare.

Комментарии

Все статьи