Статьи

Питон 2.6 странность

Как вы думаете, этот сценарий печатает ?:

import thread, threading, sys

class Weeper(object):
    def __del__(self):
        sys.stdout.write('oh cruel world %s\n' % thread.get_ident())

local = threading.local()

def target():
    local.weeper = Weeper()

t = threading.Thread(target=target)
t.start()
t.join()
sys.stdout.write('done %s\n' % thread.get_ident())
getattr(local, 'whatever', None)

 Если вы догадались что-то вроде этого:

oh cruel world 4475731968
done 140735297751392 

… то вы были бы правы, в Python 2.7 и 3.x . В Python 2.6 и старше порядок сообщений меняется на противоположный:

done 140735297751392
oh cruel world 140735297751392

В New Python Weeper удаляется, как только его поток умирает, и __del__ запускается в умирающем потоке. В старом Python Weeper не удаляется до тех пор, пока поток не станет мертвым, а другой поток не получит доступ к локальному __dict__. Таким образом, Weeper удаляется в строке getattr ( local , what , None ) после того, как поток умирает, а Weeper .__ del__ запускается в основном потоке.

Что если мы удалим вызов getattr ? В старом Python это происходит:

done 140735297751392
Exception AttributeError: "'NoneType' object has no attribute 'get_ident'"
    in <bound method Weeper.__del__ of <__main__.Weeper object at 0x104f95590>>
    ignored

 Без getattr Weeper не удаляется до выключения интерпретатора. Последовательность выключения является сложной и трудно прогнозируемой — в этом случае для модуля потока было установлено значение None до удаления Weeper, поэтому Weeper .__ del__ не может выполнить поток . get_ident ( ) .

Локальные темы в старом Python

Чтобы понять, почему в Python 2.6 и более ранних версиях локальные системы действуют именно так, давайте рассмотрим реализацию на языке C. Структура PyThreadState основного интерпретатора имеет атрибут dict , а каждый объект threading.local имеет атрибут ключа, отформатированный как «thread.local. <Память адрес себя> « . Каждый локальный имеет __dict__ атрибутов на поток, сохраненную в PyThreadState «s Dict с ключом местного жителя.

threadmodule.c включает функцию _ldict (localobject * self), которая принимает local и находит его __dict__ для текущего потока. _ldict () находит и возвращает локальный __dict__ для этого потока и сохраняет его в self-> dict .

Поток-локальная архитектура старого Python

Эта архитектура, на мой взгляд, имеет ошибку. Вот реализация _ldict () :

static PyObject * _ldict(localobject *self)
{
    PyObject *tdict = PyThreadState_GetDict(); // get PyThreadState->dict for this thread
    PyObject *ldict = PyDict_GetItem(tdict, self->key);
    if (ldict == NULL) {
        ldict = PyDict_New(); /* we own ldict */
        PyDict_SetItem(tdict, self->key, ldict);
        Py_CLEAR(self->dict);
        Py_INCREF(ldict);
        self->dict = ldict; /* still borrowed */

        if (Py_TYPE(self)->tp_init != PyBaseObject_Type.tp_init) {
            Py_TYPE(self)->tp_init((PyObject*)self, self->args, self->kw);
        }
    }

    /* The call to tp_init above may have caused another thread to run.
       Install our ldict again. */
    if (self->dict != ldict) {
        Py_CLEAR(self->dict);
        Py_INCREF(ldict);
        self->dict = ldict;
    }

    return ldict;
}

 

Я редактировал для краткости. Здесь есть несколько интересных вещей — одна из них — проверка на собственный метод __init__ . Если этот объект является подклассом local, который переопределяет __init__ , тогда __init__ вызывается всякий раз, когда новый поток обращается к атрибутам этого local впервые.

Но самое главное , я покажу вам , это два вызова Py_CLEAR (само-> Dict) , который декрементах self-> ДИКТ «s RefCount. Он вызывается, когда поток обращается к атрибутам этого локального объекта в первый раз, или если этот поток обращается к атрибутам локального объекта после того, как другой поток получил к ним доступ, то есть если self -> dict ! = Ldict .

Итак, теперь мы ясно понимаем, почему локальные элементы потока не удаляются сразу после его смерти:

  1. Рабочий поток хранит Weeper в local.weeper . _ldict () создает новый __dict__ для этого потока и сохраняет его как значение в PyThreadState-> dict , а также сохраняет его в local-> dict . Таким образом, есть две ссылки на __dict__ этого потока : один из PyThreadState , другой из local.
  2. Рабочий поток умирает, а интерпретатор удаляет его PyThreadState . Теперь есть одна ссылка на __dict__ : local- > dict мертвого потока .
  3. Наконец, мы выполняем getattr ( local , what , None ) из основного потока. В _ldict () , сам -> ДИКТ ! = Ldict , так self-> ДИКТ разыменовывается и заменял основной поток __dict__ . Теперь __dict__ мертвой нити окончательно разыменована, а Weeper удален.

Ошибка в том, что _ldict () возвращает локальный __dict__ для текущего потока и сохраняет ссылку на него. Вот почему __dict__ не удаляется, как только его поток умирает: есть бесполезная, но постоянная ссылка на __dict__, пока не появится другой поток и не очистит его.

Локальные нити в новом Python

В Python 2.7 и 3.x архитектура немного сложнее. Каждый элемент PyThreadState содержит фиктивный элемент для каждого локального элемента , а каждый локальный элемент содержит формат, указывающий слабые ссылки на макеты для каждого потока __dict__ .

Локальная архитектура нового Python

Когда поток умирает и его PyThreadState удаляется, слабые обратные вызовы немедленно запускаются в этом потоке, удаляя __dict__ потока для каждого локального. И наоборот, когда локальный удален, он удаляет свою пустышку из PyThreadState-> dict .

_ldict () в New Python действует более разумно, чем в Old Python. Он находит фиктивный файл текущего потока в PyThreadState и получает от этого манекена __dict__ для этого потока. Но в отличие от старого Python, он нигде не хранит дополнительную ссылку на __dict__ . Это просто возвращает это:

static PyObject * _ldict(localobject *self)
{
    PyObject *tdict, *ldict, *dummy;
    tdict = PyThreadState_GetDict();
    dummy = PyDict_GetItem(tdict, self->key);
    if (dummy == NULL) {
        ldict = _local_create_dummy(self);
        if (Py_TYPE(self)->tp_init != PyBaseObject_Type.tp_init) {
            Py_TYPE(self)->tp_init((PyObject*)self, self->args, self->kw);
        }
    } else {
        ldict = ((localdummyobject *) dummy)->localdict;
    }

    return ldict;
}

 

По всей видимости, вся эта техника «от слабых до пустышек» предназначена для решения проблемы циклического сбора мусора, которую я не очень хорошо понимаю. Я считаю, что настоящая причина, по которой Python 2.7 действует так, как ожидалось при выполнении моего скрипта, и почему Python 2.6 ведет себя странно, заключается в том, что 2.6 хранит дополнительную бесполезную ссылку на
__dict__, а 2.7 — нет.