Как вы думаете, этот сценарий печатает ?:
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 .
Эта архитектура, на мой взгляд, имеет ошибку. Вот реализация _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 .
Итак, теперь мы ясно понимаем, почему локальные элементы потока не удаляются сразу после его смерти:
- Рабочий поток хранит Weeper в local.weeper . _ldict () создает новый __dict__ для этого потока и сохраняет его как значение в PyThreadState-> dict , а также сохраняет его в local-> dict . Таким образом, есть две ссылки на __dict__ этого потока : один из PyThreadState , другой из local.
- Рабочий поток умирает, а интерпретатор удаляет его PyThreadState . Теперь есть одна ссылка на __dict__ : local- > dict мертвого потока .
- Наконец, мы выполняем getattr ( local , ‘ what ‘ , None ) из основного потока. В _ldict () , сам -> ДИКТ ! = Ldict , так self-> ДИКТ разыменовывается и заменял основной поток __dict__ . Теперь __dict__ мертвой нити окончательно разыменована, а Weeper удален.
Ошибка в том, что _ldict () возвращает локальный __dict__ для текущего потока и сохраняет ссылку на него. Вот почему __dict__ не удаляется, как только его поток умирает: есть бесполезная, но постоянная ссылка на __dict__, пока не появится другой поток и не очистит его.
Локальные нити в новом Python
В Python 2.7 и 3.x архитектура немного сложнее. Каждый элемент PyThreadState содержит фиктивный элемент для каждого локального элемента , а каждый локальный элемент содержит формат, указывающий слабые ссылки на макеты для каждого потока __dict__ .
Когда поток умирает и его 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 — нет.