深刻理解Python中的ThreadLocal變量(下)

上篇咱們看到了 ThreadLocal 變量的簡單使用,中篇對python中 ThreadLocal 的實現進行了分析,但故事尚未結束。本篇咱們一塊兒來看下Werkzeug中ThreadLocal的設計。html

Werkzeug 做爲一個 WSGI 工具庫,因爲一些方面的考慮,並無直接使用python內置的ThreadLocal類,而是本身實現了一系列Local類。包括簡單的Local,以及在此基礎上實現的LocalStack,LocalManager 和 LocalProxy。接下來咱們一塊兒來看看這些類的使用方式,設計的初衷,以及具體的實現技巧。python

Local 類的設計

Werkzeug 的設計者認爲python自帶的ThreadLocal並不能知足需求,主要由於下面兩個緣由:git

  1. Werkzeug 主要用「ThreadLocal」來知足併發的要求,python 自帶的ThreadLocal只能實現基於線程的併發。而python中還有其餘許多併發方式,好比常見的協程(greenlet),所以須要實現一種可以支持協程的Local對象。github

  2. WSGI不保證每次都會產生一個新的線程來處理請求,也就是說線程是能夠複用的(能夠維護一個線程池來處理請求)。這樣若是werkzeug 使用python自帶的ThreadLocal,一個「不乾淨(存有以前處理過的請求的相關數據)」的線程會被用來處理新的請求。flask

爲了解決這兩個問題,werkzeug 中實現了Local類。Local對象能夠作到線程和協程之間數據的隔離,此外,還要支持清理某個線程或者協程下的數據(這樣就能夠在處理一個請求以後,清理相應的數據,而後等待下一個請求的到來)。併發

具體怎麼實現的呢,思想其實特別簡單,咱們在深刻理解Python中的ThreadLocal變量(上) 一文的最後有提起過,就是建立一個全局字典,而後將線程(或者協程)標識符做爲key,相應線程(或協程)的局部數據做爲 value。這裏 werkzeug 就是按照上面思路進行實現,不過利用了python的一些黑魔法,最後提供給用戶一個清晰、簡單的接口。app

具體實現

Local 類的實如今 werkzeug.local 中,以 8a84b62 版本的代碼進行分析。經過前兩篇對ThreadLocal的瞭解,咱們已經知道了Local對象的特色和使用方法。因此這裏再也不給出Local對象的使用例子,咱們直接看代碼。ide

class Local(object):
    __slots__ = ('__storage__', '__ident_func__')
    
    def __init__(self):
        object.__setattr__(self, '__storage__', {})
        object.__setattr__(self, '__ident_func__', get_ident)
    ...

因爲可能有大量的Local對象,爲了節省Local對象佔用的空間,這裏使用 __slots__ 寫死了Local能夠擁有的屬性:函數

  • __storage__: 值爲一個字典,用來保存實際的數據,初始化爲空;工具

  • __ident_func__:值爲一個函數,用來找到當前線程或者協程的標誌符。

因爲Local對象實際的數據保存在__storage__中,因此對Local屬性的操做實際上是對__storage__的操做。對於獲取屬性而言,這裏用魔術方法__getattr__攔截__storage__ 和 __ident_func__之外的屬性獲取,將其導向__storage__存儲的當前線程或協程的數據。而對於屬性值的set或者del,則分別用__setattr__和__setattr__來實現(這些魔術方法的介紹見屬性控制)。關鍵代碼以下所示:

def __getattr__(self, name):
    try:
        return self.__storage__[self.__ident_func__()][name]
    except KeyError:
        raise AttributeError(name)

def __setattr__(self, name, value):
    ident = self.__ident_func__()
    storage = self.__storage__
    try:
        storage[ident][name] = value
    except KeyError:
        storage[ident] = {name: value}

def __delattr__(self, name):
    try:
        del self.__storage__[self.__ident_func__()][name]
    except KeyError:
        raise AttributeError(name)

假設咱們有ID爲1,2, ... , N 的N個線程或者協程,每一個都用Local對象保存有本身的一些局部數據,那麼Local對象的內容以下圖所示:

此外,Local類還提供了__release_local__方法,用來釋放當前線程或者協程保存的數據。

Local 擴展接口

Werkzeug 在 Local 的基礎上實現了 LocalStack 和 LocalManager,用來提供更加友好的接口支持。

LocalStack

LocalStack經過封裝Local從而實現了一個線程(或者協程)獨立的棧結構,註釋裏面有具體的使用方法,一個簡單的使用例子以下:

ls = LocalStack()
ls.push(12)
print ls.top    # 12
print ls._local.__storage__
# {140735190843392: {'stack': [12]}}

LocalStack 的實現比較有意思,它將一個Local對象做爲本身的屬性_local,而後定義接口push, pop 和 top 方法進行相應的棧操做。這裏用 _local.__storage__._local.__ident_func__() 這個list來模擬棧結構。在接口push, pop和top中,經過操做這個list來模擬棧的操做,須要注意的是在接口函數內部獲取這個list時,不用像上面黑體那麼複雜,能夠直接用_local的getattr()方法便可。以 push 函數爲例,實現以下:

def push(self, obj):
    """Pushes a new item to the stack"""
    rv = getattr(self._local, 'stack', None)
    if rv is None:
        self._local.stack = rv = []
    rv.append(obj)
    return rv

pop 和 top 的實現和通常棧相似,都是對 stack = getattr(self._local, 'stack', None) 這個列表進行相應的操做。此外,LocalStack還容許咱們自定義__ident_func__,這裏用 內置函數 property 生成了描述器,封裝了__ident_func__的get和set操做,提供了一個屬性值__ident_func__做爲接口,具體代碼以下:

def _get__ident_func__(self):
    return self._local.__ident_func__

def _set__ident_func__(self, value):
    object.__setattr__(self._local, '__ident_func__', value)
__ident_func__ = property(_get__ident_func__, _set__ident_func__)
del _get__ident_func__, _set__ident_func__

LocalManager

Local 和 LocalStack 都是線程或者協程獨立的單個對象,不少時候咱們須要一個線程或者協程獨立的容器,來組織多個Local或者LocalStack對象(就像咱們用一個list來組織多個int或者string類型同樣)。

Werkzeug實現了LocalManager,它經過一個list類型的屬性locals來存儲所管理的Local或者LocalStack對象,還提供cleanup方法來釋放全部的Local對象。Werkzeug中LocalManager最主要的接口就是裝飾器方法make_middleware,代碼以下:

def make_middleware(self, app):
    """Wrap a WSGI application so that cleaning up happens after
    request end.
    """
    def application(environ, start_response):
        return ClosingIterator(app(environ, start_response), self.cleanup)
    return application

這個裝飾器註冊了回調函數cleanup,當一個線程(或者協程)處理完請求以後,就會調用cleanup清理它所管理的Local或者LocalStack 對象(ClosingIterator 的實如今 werkzeug.wsgi中)。下面是一個使用 LocalManager 的簡單例子:

from werkzeug.local import Local, LocalManager

local = Local()
local_2 = Local()
local_manager = LocalManager([local, local2])

def application(environ, start_response):
    local.request = request = Request(environ)
    ...

# application 處理完畢後,會自動清理local_manager 的內容
application = local_manager.make_middleware(application)

經過LocalManager的make_middleware咱們能夠在某個線程(協程)處理完一個請求後,清空全部的Local或者LocalStack對象,這樣這個線程又能夠處理另外一個請求了。至此,文章開始時提到的第二個問題就能夠解決了。Werkzeug.local 裏面還實現了一個 LocalProxy 用來做爲Local對象的代理,也很是值得去學習。

經過這三篇文章,相信對 ThreadLocal 有了一個初步的瞭解。Python標準庫和Werkzeug在實現中都用到了不少python的黑魔法,不過最終提供給用戶的都是很是友好的接口。Werkzeug做爲WSGI 工具集,爲了解決Web開發中的特定使用問題,提供了一個改進版本,而且進行了一系列封裝,便於使用。不得不說,werkzeug的代碼可讀性很是好,註釋也是寫的很是棒,建議去閱讀源碼。

本文由selfboot 發表於我的博客,採用署名-非商業性使用-相同方式共享 3.0 中國大陸許可協議。
非商業轉載請註明做者及出處。商業轉載請聯繫做者本人
本文標題爲:深刻理解Python中的ThreadLocal變量(下)
本文連接爲:http://selfboot.cn/2016/11/03...

更多閱讀

Context Locals
Private Variables and Class-local References
How does the @property decorator work?
How do the Proxy, Decorator, Adapter, and Bridge Patterns differ?

Flask源碼剖析
Werkzeug.locals 模塊解讀
Charming Python: 從Flask的request提及
How to remove a key from a python dictionary?
werkzeug源碼分析(local.py)

相關文章
相關標籤/搜索