Django 多线程问题是怎么回事?

作者:Pegasus Wang
链接:https://www.zhihu.com/question/56472691/answer/292510026
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

简单的说就是服务端监听 socket 每次 accept 一个新的请求后,开一个线程处理 这个 socket 客户连接。如果你对底层实现原理感兴趣,可以继续看下去,从 socket 编程的角度来解释多线程 wsgi server。最后附上一个异步框架工作过程的视频讲解。这里我们自己撸一个简单的多线程 wsgi server 来看下原理,还是需要深入源码和 socket 编程你才能真正理解。 我们从 python 自带的一个 wsgi server 看下如何实现多线程处理请求。首先你需要熟悉下 wsgi。 看一个最简单的 wsgi app:

#!/usr/bin/env python
# -*- coding:utf-8 -*-

def application(environ, start_response):
    status = '200 OK'
    headers = [('Content-Type', 'text/html; charset=utf8')]

    start_response(status, headers)
    return [b"<h1>Hello</h1>"]


if __name__ == '__main__':
    from wsgiref.simple_server import make_server
    httpd = make_server('127.0.0.1', 8000, application)
    httpd.serve_forever()

然后用你的开发工具跟进去 make_server 这个函数,看下它的定义:

# lib/python2.7/wsgiref/simple_server.py

def make_server(
    host, port, app, server_class=WSGIServer, handler_class=WSGIRequestHandler
):
    """Create a new WSGI server listening on `host` and `port` for `app`"""
    server = server_class((host, port), handler_class)
    server.set_app(app)
    return server

class WSGIServer(HTTPServer):

    """BaseHTTPServer that implements the Python WSGI protocol"""

    application = None

    def server_bind(self):
        """Override server_bind to store the server name."""
        HTTPServer.server_bind(self)
        self.setup_environ()

    def setup_environ(self):
        # Set up base environment
        env = self.base_environ = {}
        env['SERVER_NAME'] = self.server_name
        env['GATEWAY_INTERFACE'] = 'CGI/1.1'
        env['SERVER_PORT'] = str(self.server_port)
        env['REMOTE_HOST']=''
        env['CONTENT_LENGTH']=''
        env['SCRIPT_NAME'] = ''

    def get_app(self):
        return self.application

    def set_app(self,application):
        self.application = application

看到这个 WSGIServer 定义了吗,继承了一个 HttpServer。我们再继续追一下其定义:

# lib/python2.7/BaseHTTPServer.py

class HTTPServer(SocketServer.TCPServer):

    allow_reuse_address = 1    # Seems to make sense in testing environment

    def server_bind(self):
        """Override server_bind to store the server name."""
        SocketServer.TCPServer.server_bind(self)
        host, port = self.socket.getsockname()[:2]
        self.server_name = socket.getfqdn(host)
        self.server_port = port

到这里,我们继续追,看到 TcpServer 定义:

# lib/python2.7/SocketServer.py

class TCPServer(BaseServer):
    """这里我省略了定义"""

你还可以发现一个 ThreadingTCPServer 类:我们看下它的定义

	class ThreadingTCPServer(ThreadingMixIn, TCPServer): 
		pass

好了,怎么多线程处理请求呢?看下这个 ThreadingMixIn 类是如何实现的:

class ThreadingMixIn:
    """Mix-in class to handle each request in a new thread."""

    # Decides how threads will act upon termination of the
    # main process
    daemon_threads = False

    def process_request_thread(self, request, client_address):
        """Same as in BaseServer but as a thread.

        In addition, exception handling is done here.

        """
        try:
            self.finish_request(request, client_address)
            self.shutdown_request(request)
        except:
            self.handle_error(request, client_address)
            self.shutdown_request(request)

    def process_request(self, request, client_address):
        """Start a new thread to process the request."""
        t = threading.Thread(target = self.process_request_thread,
                             args = (request, client_address))
        t.daemon = self.daemon_threads
        t.start()

看到吗,其实就是对于每个新请求,会启动一个新的线程来处理 socket 请求。假如让我们自己实现一个多线程 socket server 应该怎么实现呢?先来写一个简单的单线程 socker echo server:

from socket import *  # 偷懒这么写
s = socket(AF_INET, SOCK_STREAM)
s.bind(("", 9000))
s.listen(5)
while True:
    c, a = s.accept()
    print "Received connection from", a
    c.send("Hello %s\\n" % a[0])
    c.close()

你可以用telnet之类的工具连上该端口看效果。 这样一次只能处理一个请求,如果想每次来一个请求用一个线程处理呢?我们可以这样做:

import threading
from socket import *

def handle_client(c):
    # 处理 client 请求
    c.send("Hello\n")
    c.close()
    return

s = socket(AF_INET, SOCK_STREAM)
s.bind(("", 9000))
s.listen(5)
while True:
    c, a = s.accept()
    t = threading.Thread(target=handle_client,
                         args=(c,))

是不是很简单,这其实就是多线程工作的原理,每次 accept 得到一个新的客户端请求以后开一个线程去处理。当然 socket 编程还是偏底层,我们刚才看到了 python 提供了 SocketServer 模块来简化 socket 编程。我们使用 SocketServer 模块来发送数据:

import SocketServer
import time

class TimeHandler(SocketServer.BaseRequestHandler):
    def handle(self):
        # self.request 是一个 client socket 对象
        self.request.sendall(time.ctime() + "\n")

serv = SocketServer.TCPServer(("", 8889), TimeHandler)
serv.serve_forever()

它的执行原理是这样的:server 循环等待请求到来对每个 socket 连接,server 创建一个新的 handler 类实例handle() 方法调用处理 client socket 对象,比如发送数据handle() 方法返回后,连接关闭,同时 handler 实例对象销毁但是这个 server 的处理能力很差,一次只能处理一个请求,我们看下这个模块提供了几个类:

  • TCPServer: 同步的 tcp server
  • ForkingTCPServer: 多进程 tcp server
  • ThreadingTCPServer: 多线程

tcp server怎么实现一个多线程 tcp server 呢?很简单:

# 改成 ThreadingTCPServer 就行了,代码其他部分不动
serv = SocketServer.ThreadingTCPServer(("",8000),TimeHandler)
serv.serve_forever()

这样一来,新的请求就能被新的线程去处理了。我们就通过线程来增强了并发能力,当然线程开销比较大,不如用协程(抽空会写个用协程实现异步的socker server)。
如果你浏览该模块,还能看到两个 Mixin:

  • ForkingMixin
  • ThreadingMixIn

我们只要继承它们就很容易实现一个多进程或者多线程的 server,比如实现一个多线程的 HTTPServer

from BaseHTTPServer import HTTPServer
from SimpleHTTPServer import SimpleHTTPRequestHandler
from SocketServer import ThreadingMixIn

class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
    pass

serv = ThreadedHTTPServer(("", 8080), SimpleHTTPRequestHandler)

好了,看了这么多让我们改造下 Python 自带的 wsgi server 为多线程的:

import time
import SocketServer
import socket
from SimpleHTTPServer import SimpleHTTPRequestHandler
from SocketServer import ThreadingMixIn


class HTTPServer(SocketServer.TCPServer):

    allow_reuse_address = 1    # Seems to make sense in testing environment

    def server_bind(self):
        """Override server_bind to store the server name."""
        SocketServer.TCPServer.server_bind(self)
        host, port = self.socket.getsockname()[:2]
        self.server_name = socket.getfqdn(host)
        self.server_port = port


class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
    pass


class ThreadWSGIServer(ThreadedHTTPServer):

    """BaseHTTPServer that implements the Python WSGI protocol"""

    application = None

    def server_bind(self):
        """Override server_bind to store the server name."""
        HTTPServer.server_bind(self)
        self.setup_environ()

    def setup_environ(self):
        # Set up base environment
        env = self.base_environ = {}
        env['SERVER_NAME'] = self.server_name
        env['GATEWAY_INTERFACE'] = 'CGI/1.1'
        env['SERVER_PORT'] = str(self.server_port)
        env['REMOTE_HOST'] = ''
        env['CONTENT_LENGTH'] = ''
        env['SCRIPT_NAME'] = ''

    def get_app(self):
        return self.application

    def set_app(self, application):
        self.application = application


def application(environ, start_response):
    time.sleep(10)    # 注意这里的 sleep
    status = '200 OK'
    headers = [('Content-Type', 'text/html; charset=utf8')]

    start_response(status, headers)
    return [b"<h1>Hello</h1>"]


if __name__ == '__main__':
    from wsgiref.simple_server import make_server
    httpd = make_server('127.0.0.1', 8000, application, server_class=ThreadWSGIServer)
    httpd.serve_forever()

对了,我们怎么证明这个真是多线程的 wsgi server 了呢,很简单。你看我加了个 sleep(10)。 如果你在之前的单线程 wsgi server 跑,你可以开俩个终端去 curl,curl 完一个赶紧切到另一个 curl 。 单线程你会发现几乎是 10 + 10 秒,但是下边这个多线程 wsgi server 你会发现大概只用10秒,两个请求是并发的(当然我这种测试很 low,你可以随便用一个网络测试工具)。虽然我没看过 django wsgi server 的实现,但是它自己实现的开发服务器原理应该是类似的。如果你能坚持看到这里,是更明白了呢还是更懵了呢?你可以看下PegasusWang/notebooks 这个是手撸 web 框架的教程,看完你就对 wsgi,如何自己写 框架以及 gunicorn 部署有点概念了。最近在学习异步框架的工作原理,感兴趣可以看看,之后还会出个协程版本的。

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值