В сопрограмме python socket.socket() может возвращать занятый файловый дескриптор

У меня есть кусок кода Python для практики совместных процедур Python. Как объяснил А. Джесси Джирю Дэвис.

  • Во-первых, я определяю сопрограмму с именем 'get' для получения содержимого некоторого URL-адреса.
  • Затем я определяю класс Task для итерации сопрограммы до завершения.
  • Затем я создаю две Task, которые открывают два разных URL.

Но я получил сообщение об ошибке: KeyError: '368 (FD 368) уже зарегистрирован' в строке selector.register(s.fileno(), EVENT_WRITE).

Эта ошибка вызвана тем, что два вызова socket.socket() возвращают один и тот же файловый дескриптор. На самом деле, этот файловый дескриптор 368 был выделен в предыдущем вызове, но все еще возвращен во втором вызове.

  • Затем я добавляю выражение, которое изменяет внешнюю переменную.

На этот раз сообщение об ошибке просто исчезло! Если вы хотите запустить код самостоятельно, вы можете раскомментировать arr.append(self.init) в методе Task.step, чтобы увидеть вывод без ошибок.

EDIT Если я явно вызову сборку мусора Python, иногда эта ошибка исчезнет. Но ПОЧЕМУ время от времени?

После нескольких дней поиска и чтения документов по Python я до сих пор не понимаю, почему это происходит. Я только что пропустил некоторые «питоновские ошибки», не так ли?

Я использую python 3.6 для тестирования. Код выглядит следующим образом, и я удалил весь ненужный код, чтобы сделать следующий код точным и соответствующим теме:

#! /usr/bin/python

from selectors import DefaultSelector, EVENT_WRITE
import socket
import gc

selector = DefaultSelector()
arr = [1, 2, 3]

class Task:

    def __init__(self, gen):
        self.gen = gen
        self.step()

    def step(self):
        next(self.gen)
        # arr.append(self.__init__)

def get(path, count = 0):
    s = socket.socket()
    print(count, 'fileno:', s.fileno())
    s.connect(('www.baidu.com', 80))
    selector.register(s.fileno(), EVENT_WRITE)
    yield

Task(get('/foo',1))
gc.collect()
Task(get('/bar',2))

person Charles    schedule 20.10.2017    source источник
comment
Сразу после создания и подключения каждого сокета функция завершает работу без сохранения какой-либо ссылки на объект сокета. Поэтому он автоматически закрывается как часть сборки мусора, и его номер файла немедленно становится доступным для повторного использования.   -  person jasonharper    schedule 20.10.2017
comment
Это не может объяснить, почему добавление какой-либо другой операции, такой как «object.append(self.step)», поможет получить новый номер файла. А 'object.append(1234)' - нет.   -  person Charles    schedule 20.10.2017
comment
Task() -> self.step -> object -> Task(). Вы создали циклическую ссылку, и один из этих объектов имеет ссылку на генератор, поэтому он будет очищен только при следующем запуске сборщика мусора. Что делает ошибку недетерминированной.   -  person dhke    schedule 20.10.2017
comment
Я думаю, что это Task() -> self.step -> object -> arr , где arr — это [1, 2, 3] из исходного кода. Вы имеете в виду, что после того, как я раскомментирую «object.append(self.__init__)», это циклическая ссылка «Task() -> self.step -> object -> Task()»?   -  person Charles    schedule 20.10.2017
comment
@GuochengLi Да, я имел в виду тот факт, что это работает с object.append(self.__init__) на месте. Это связано с тем, что консервативная очистка на основе подсчета ссылок работает только при отсутствии циклических ссылок.   -  person dhke    schedule 20.10.2017
comment
В любом случае, использование selector.register(s, EVENT_WRITE) должно решить вашу проблему. В этом случае selector содержит ссылку на объект сокета.   -  person dhke    schedule 20.10.2017
comment
@dhke Да, ты прав. Отличный ответ. Большое спасибо. Я вернусь к документации по сбору мусора python.   -  person Charles    schedule 20.10.2017


Ответы (2)


Примечание:

  1. ваш get('/foo', 1) является анонимным объектом генератора, Task(get('/foo', 1)) является анонимным объектом Task.
  2. GC — это сокращение от сборки/сборщика мусора python.

Цепочка ссылок вашего исходного кода:

selector --> # socket_fd
anonymous Task(get('/foo', 1)) --> anonymous get('/foo', 1) --> s

Таким образом, анонимный объект Task(get('/foo', 1)) собирается сборщиком мусора, как только он завершается. Это потому что:

Сборщик мусора Python соберет память объекта, как только обнаружит, что счетчик ссылок объекта == 0. Но сборщик мусора Python не работает как поток, поэтому, возможно, не сразу после того, как счетчик ссылок объекта уменьшится до 0.

То есть то будет собираться аноним get('/foo', 1), то будет s. Здесь s собирается, закрывается и соответствующий номер сокета #fd (#368 в вашем примере) освобождается.

Но номер сокета #fd (#368) зарегистрирован как selector.

Затем вы запускаете Task(get('/bar',2)), новый socket s пытается подать заявку на новый fd, поскольку #368 доступен (пока другие процессы в вашей системе не затребовали его), вы получите #368 как сокет fd agian.

раскомментировать arr.append(self.__init__) в Task.step()

После того, как вы раскомментируете arr.append(self.__init__) в методе Task.step(), глобальное arr будет содержать ссылку на Task(get('/foo', 1)). Тогда Task(get('/foo', 1)) имеет ссылку на get('/foo', 1). Тогда get('/foo', 1) имеет ссылку на ваш локальный сокет s. Эта цепочка ссылок выглядит так:

arr --> anonymous Task(get('/foo', 1)) --> anonymous get('/foo', 1) --> s

arr действует через вашу программу, поэтому s не будет собираться сборщиком мусора. Позже s = socket.socket() не получит тот же fd, потому что он все еще принадлежит s.

используйте s вместо s.fileno()

Если вы используете selector.register(s ..) вместо selector.register(s.fileno()..), глобальный selector будет содержать ссылку на ваш локальный s, цепочка ссылок:

selector --> s

Хотя два анонимных объекта исчезли, ваши get('/foo', 1))::s и get('/bar', 2))::s по-прежнему удерживаются глобальным selector. Так что не волнуйтесь, два fd не столкнутся.

циклическая ссылка?

Ответ: Нет. Ваша ситуация не имеет ничего общего с циклической ссылкой.

gc.собрать?

Ну замените его на time.sleep(0.02) и вы увидите то же самое явление. Это может быть вызвано:

  1. сокеты приходят и уходят, управляемые другими процессами вашей системы.
  2. Поток Python GC может занять некоторое время, пока он не обнаружит, что s должен быть собран, или поток собирается.
person allen He    schedule 07.11.2017

@allen Хе, большое спасибо за ответ.

  1. Я исследовал дальше, вывод таков: проблема не вызвана gc.

«Сборщик мусора», упомянутый в gc, используется только для разрешения циклических ссылок. В Python (по крайней мере, в основной реализации C, CPython) основным методом управления памятью является подсчет ссылок. В моем коде результат Task() не имеет ссылок, поэтому всегда будет немедленно удален. Невозможно предотвратить это, независимо от того, используете ли вы gc.disable() или что-то еще.

Эти квоты от @Daniel Roseman см. здесь:

как предотвратить сборку мусора Python для анонимных объектов?

  1. И еще, Python GC — это не поток. Вместо этого синхронно

См.: Почему у python нет потока сборщика мусора?

  1. Итак, окончательный ответ на мой заголовок вопроса:

Нет, socket.socket() не вернет занятый файловый дескриптор. Если бы он вернул «оккупированный» fd, это означало бы, что предыдущий был освобожден.

person Charles    schedule 07.11.2017