[Python][Django][asyncio]关于多线程、协程以及异步在Django中的应用

在开发环境中,Django默认用runserver方式启动,请求默认是支持多线程的,可以通过加上

--nothreading

参数,禁止多线程运行;
但是在生产环境中,都会用uWSGI作为前置启动方式,它是默认采用多进程方式工作,每个进程是单线程运行,可以通过加上–threads设置单个进程运行的线程数;
Important!!!
重要的事情说三遍,单进程单线程的工作模式会导致工程中使用threading启动新的线程并不会一直工作,只在网络有请求到来时,主线程运行的那段时间内,新线程才会同步运行!!!
重要的事情说三遍,单进程单线程的工作模式会导致工程中使用threading启动新的线程并不会一直工作,只在网络有请求到来时,主线程运行的那段时间内,新线程才会同步运行!!!
重要的事情说三遍,单进程单线程的工作模式会导致工程中使用threading启动新的线程并不会一直工作,只在网络有请求到来时,主线程运行的那段时间内,新线程才会同步运行!!!
Gunicorn也是一种高性能的Python WSGI HTTP Server,默认工作方式是sync,也是多进程,每个进程默认单线程的工作方式,可以在配置文件中加入threads参数设置线程的数量(假定在gthread模式下生效!也就是会用gthread替代sync模式);
因为Python的全局解释器锁(GIL)的缘故,采用多线程进行上下文切换效率很低,不如异步框架(https://blog.csdn.net/yiliumu/article/details/77983393)。
下面针对gevent模式来说明:
1、gevent是基于协程(coroutine -based)的Python网络库
2、Event Loop是其核心概念
3、代码执行的同步、异步机制研究(http://sdiehl.github.io/gevent-tutorial/
4、使用siege进行压力测试,sync在每个请求执行时间很短的情况下,是优于gevent模式,因为串行一次比切换一次loop消耗开销更小(0.01s);但是在单个业务超过3s以上的时候,串行阻塞时间会叠加、直到前一个业务返回,导致平均响应时间线性上升,如果client并发超过server的最大并发数量,所有请求将会严重等待。。。而基于协程的并行模式,只要不超过服务器的最大连接数,每个业务都只消耗该任务的网络I/O的等待时间,对于网络异步IO密集的任务很适用。。。例如:APNs推送。。。
5、Gunicorn下threading会正常工作,并不会停止运行!!!!

总结一下:多线程是线程调度,每个线程运行网络请求需要while循环等待网络数据,多个请求是竞争状态;协程是在Socket进入等待的时候,运行状态会挂起,进入下一个消息循环运行别的代码、直到收到信号数据准备好了,再切回来运行,尽量让工作都不闲着。。。因为是单线程,所以在本线程的cpu时间内运行的飞起。。。直到server的连接数满上限,不能接受多余的任务为止。。。

附上一张Gunicorn-Gevent初始化运行的调试堆栈和代码:

 
class GeventWorker(AsyncWorker):
 
    server_class = None
    wsgi_handler = None
 
    def patch(self):
        from gevent import monkey
        monkey.noisy = False
 
        # if the new version is used make sure to patch subprocess
        if gevent.version_info[0] == 0:
            monkey.patch_all()
        else:
            monkey.patch_all(subprocess=True)
 
        # monkey patch sendfile to make it none blocking
        patch_sendfile()
 
        # patch sockets
        sockets = []
        for s in self.sockets:
            if sys.version_info[0] == 3:
                sockets.append(socket(s.FAMILY, _socket.SOCK_STREAM,
                    fileno=s.sock.fileno()))
            else:
                sockets.append(socket(s.FAMILY, _socket.SOCK_STREAM,
                    _sock=s))
        self.sockets = sockets
 
    def notify(self):
        super(GeventWorker, self).notify()
        if self.ppid != os.getppid():
            self.log.info("Parent changed, shutting down: %s", self)
            sys.exit(0)
 
    def timeout_ctx(self):
        return gevent.Timeout(self.cfg.keepalive, False)
 
    def run(self):
        servers = []
        ssl_args = {}
 
        if self.cfg.is_ssl:
            ssl_args = dict(server_side=True, **self.cfg.ssl_options)
 
        for s in self.sockets:
            s.setblocking(1)
            pool = Pool(self.worker_connections)
            if self.server_class is not None:
                environ = base_environ(self.cfg)
                environ.update({
                    "wsgi.multithread": True,
                    "SERVER_SOFTWARE": VERSION,
                })
                server = self.server_class(
                    s, application=self.wsgi, spawn=pool, log=self.log,
                    handler_class=self.wsgi_handler, environ=environ,
                    **ssl_args)
            else:
                hfun = partial(self.handle, s)
                server = StreamServer(s, handle=hfun, spawn=pool, **ssl_args)
 
            server.start()
            servers.append(server)
 
        while self.alive:
            self.notify()
            gevent.sleep(1.0)
 
        try:
            # Stop accepting requests
            for server in servers:
                if hasattr(server, 'close'):  # gevent 1.0
                    server.close()
                if hasattr(server, 'kill'):  # gevent < 1.0
                    server.kill()
 
            # Handle current requests until graceful_timeout
            ts = time.time()
            while time.time() - ts <= self.cfg.graceful_timeout:
                accepting = 0
                for server in servers:
                    if server.pool.free_count() != server.pool.size:
                        accepting += 1
 
                # if no server is accepting a connection, we can exit
                if not accepting:
                    return
 
                self.notify()
                gevent.sleep(1.0)
 
            # Force kill all active the handlers
            self.log.warning("Worker graceful timeout (pid:%s)" % self.pid)
            [server.stop(timeout=1) for server in servers]
        except:
            pass
 
    def handle_request(self, *args):
        try:
            super(GeventWorker, self).handle_request(*args)
        except gevent.GreenletExit:
            pass
        except SystemExit:
            pass
 
    def handle_quit(self, sig, frame):
        # Move this out of the signal handler so we can use
        # blocking calls. See #1126
        gevent.spawn(super(GeventWorker, self).handle_quit, sig, frame)
 
    if gevent.version_info[0] == 0:
 
        def init_process(self):
            # monkey patch here
            self.patch()
 
            # reinit the hub
            import gevent.core
            gevent.core.reinit()
 
            #gevent 0.13 and older doesn't reinitialize dns for us after forking
            #here's the workaround
            gevent.core.dns_shutdown(fail_requests=1)
            gevent.core.dns_init()
            super(GeventWorker, self).init_process()
 
    else:
 
        def init_process(self):
            # monkey patch here
            self.patch()
 
            # reinit the hub
            from gevent import hub
            hub.reinit()
 
            # then initialize the process
            super(GeventWorker, self).init_process()

从运行过程可以看到,Gunicorn启动Django的时候,默认已经打上了全部的monkey补丁。。。同时也更改了wsgi.multithread=True。。。

[Python]关于ctypes使用char*指针与bytes相互转换的问题

最近研究人脸识别,需要用python调用so动态库,涉及到c/c++中的指针字符串转Python的bytes对象的问题。
按照ctypes的文档,直观方式是先创建对应的类型数组,再将指针取地址一一赋值:

from ctypes import *
 
 
p=(c_char * 10)()
for i in range(10):
    p[i] = i
 
b=bytes(bytearray(p))
print(b)

搜寻了各种资料,都未能找到更好的。。。直到ctypes.string_at

_string_at = PYFUNCTYPE(py_object, c_void_p, c_int)(_string_at_addr)
def string_at(ptr, size=-1):
    """string_at(addr[, size]) -> string
 
    Return the string at addr."""
    return _string_at(ptr, size)

于是char*转bytes可以直接用string_at方法,传入指针地址,以及字符串长度即可。

同样的问题,bytes对象需要传给c/c++代码。。。
直观方式同样是创建char数组array,拷贝bytes之后,再用cast强制转换成c_char_p

from ctypes import *
 
 
p=(c_char * 10)()
for i in range(10):
    p[i] = i
 
m=cast(p, c_char_p)
print(m)

比较奇葩的是cast得到的对象,如果我们直接用bytes对象cast。。。

from ctypes import *
 
 
b=b'0123456789'
m=cast(p, c_char_p)
print(m)

吼吼,奇迹出现了,bytes对象cast成了char*指针。。。用string_at转换看看

string_at(m)

总结一下:
1、bytes基于Buffer Protocol,查看其c实现https://hg.python.org/cpython/file/3.4/Objects/bytesobject.c
2、string_as的c代码https://hg.python.org/cpython/file/3717b1481d1b/Modules/_ctypes/_ctypes.c

static PyObject *
string_at(const char *ptr, int size)
{
	if (size == -1)
		return PyString_FromString(ptr);
	return PyString_FromStringAndSize(ptr, size);
}

3、cast的c代码同样在_ctypes.c(https://hg.python.org/cpython/file/3717b1481d1b/Modules/_ctypes/_ctypes.c)

static PyObject *
cast(void *ptr, PyObject *src, PyObject *ctype)
{
	CDataObject *result;
	if (0 == cast_check_pointertype(ctype))
		return NULL;
	result = (CDataObject *)PyObject_CallFunctionObjArgs(ctype, NULL);
	if (result == NULL)
		return NULL;
 
	/*
	  The casted objects '_objects' member:
 
	  It must certainly contain the source objects one.
	  It must contain the source object itself.
	 */
	if (CDataObject_Check(src)) {
		CDataObject *obj = (CDataObject *)src;
		/* CData_GetContainer will initialize src.b_objects, we need
		   this so it can be shared */
		CData_GetContainer(obj);
		/* But we need a dictionary! */
		if (obj->b_objects == Py_None) {
			Py_DECREF(Py_None);
			obj->b_objects = PyDict_New();
			if (obj->b_objects == NULL)
				goto failed;
		}
		Py_XINCREF(obj->b_objects);
		result->b_objects = obj->b_objects;
		if (result->b_objects && PyDict_Check(result->b_objects)) {
			PyObject *index;
			int rc;
			index = PyLong_FromVoidPtr((void *)src);
			if (index == NULL)
				goto failed;
			rc = PyDict_SetItem(result->b_objects, index, src);
			Py_DECREF(index);
			if (rc == -1)
				goto failed;
		}
	}
	/* Should we assert that result is a pointer type? */
	memcpy(result->b_ptr, &ptr, sizeof(void *));
	return (PyObject *)result;
 
  failed:
	Py_DECREF(result);
	return NULL;
}

关于js创建sign的进一步思路

关于js创建sign的进一步思路
对于代理使用js创建sign,大家已经熟悉了,现在我们能不能把这种js内部函数调用,变成外部可以访问的http请求呢?抱着这个目的,我开始了对js中webscoekt的使用研究——因为websocket是允许在html中跨域访问的。
1、js请求websocket
ws的请求地址是这样的:
ws://echo.websocket.org/
它的回调event是这样的:

        websocket = new WebSocket(wsUri);
        websocket.onopen = function(evt) {
            onOpen(evt)
        };
        websocket.onclose = function(evt) {
            onClose(evt)
        };
        websocket.onmessage = function(evt) {
            onMessage(evt)
        };
        websocket.onerror = function(evt) {
            onError(evt)
        };

websocket使用的是TCP长链接,即能收message,也能发送message,这样就只需要起一个websocket的server,让server给js发送请求sign的指令即可,应答由server异步接收。
2、WebSocket Server
我打算用另一个websocket client去链接server,与js端的websocket client通讯,server把命令指令进行中转即可;也可以让server兼做http server,省略websocket server转发client请求。
请求过程如下:
websocket client ==>> websocket server ==>> websocket – js client
数据返回是倒着来:
websocket client <<== websocket server <<== websocket – js client
于是在client中,提交sign请求给server,server转发请求给js client;
js client计算sign,异步得到结果后,传回给server;
server再将js client返还的数据包,转发给client,完成一次Request&Response
3、Http Server
虽然websocket client已经够用了,但是需要使用websocket对于html页面也是一种负担和技术门槛,而且数据协议需要定义好,才能在websocket server中转发请求与应答。加上一层http server是为了简化html使用,还能形成API,加一层Ngnix后,可以扩大并发,瓶颈目前在js client,毕竟手机或者模拟器跑app上的js有点慢。融合http server到websocket client中即可,这个就不多讲了,目前还在研究中。

附录:
1、websocket server使用autobahn
2、http server使用aiohttp

Python[Defining Python Source Code Encodings]文件编码详解

Python文件如果没指定文件编码格式时,默认使用ASCII,所以源文件中使用了中文字符时,需要在第一行或者第二行这样指定编码格式:

# coding=<encoding name>

或者采用大多数编辑器认可的格式:

#!/usr/bin/python
# -*- coding: <encoding name> -*-

又或者:

#!/usr/bin/python
# vim: set fileencoding=<encoding name> :

准确来说,第一行或者第二行需要符合以下正则表达式:

^[ \t\v]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)

一般情况下,我们使用如下代码放在第一行即可:

# coding: utf-8

参考文档:https://www.python.org/dev/peps/pep-0263/

Python[Mac OS X]环境搭建之Virtualenv

上篇已简单介绍如何安装python以及依赖的各种库,实际上应用中会遇到很多问题,例如:
pip install mysqlclient
安装mysql时经常会遇到mysql_config找不到,或者安装完成后,在import mysql时抛出异常,提示so中 Symbol not found: _mysql_affected_rows

再例如不同的工程需要依赖的特定库版本不同,又或者python2切换到python3…

于是我们今天学习使用Virtualenv来创建纯粹的单个python依赖环境,解决上面的这些问题。

1、安装

$ pip install virtualenv

2、Usage:

$ virtualenv ENV

在当前目录下创建了ENV文件夹,存放我们准备配置的python环境
3、Activate

$ source bin/activate

在ENV/bin目录下执行activate, 切换到该工作环境
4、Deactivate

$ deactivate

退出该工作环境

附录:PyCharm在Project Interpreter中也可以快速新建一个工程适用的virtualenv,打开Python Console可以看到python是用得哪个环境下的

==============分割线===================
后记:为了找到mysqlclient安装失败的原因,尝试了N次,以及修改其setup.py才明白原因。
1、mysql_config是mysql安装时产生的可执行文件(这就需要从mysql官网上下载适合系统的版本,我的机器上安装了多个版本的mysql,任选一个都可以)
2、setup.py中使用了mysql_config命令执行,所以需要将mysql_config文件所在的bin目录,放到PATH环境变量中,如:PATH=”/usr/local/mysql-5.7.10-osx10.9-x86_64/bin:${PATH}”
3、site.cfg中“mysql_config = /usr/local/bin/mysql_config”需要替换为你的该文件路径,如果你需要从zip解压文件安装的话,或者你没有设置PATH
4、卸载错误的版本 pip3 uninstall mysqlclient
5、重新安装 pip3 install mysqlclient

Python[Mac OS X]搭建服务器后台运行环境

1、Python2.7
默认Mac应该会安装了python2.7版本,如果需要更换版本,可以在官方网站上下载:
https://www.python.org/downloads/
2、Setuptools 7.0
按照官网的提示:Install pip, setuptools, and wheel
从官网下载的Python 2 >=2.7.9 or Python 3 >=3.4 已经包含了pip以及setuptools
可以更新到最新版本:
pip install -U pip setuptools
实际更新的时候会遇到坑……

Collecting setuptools
  Downloading setuptools-36.2.6-py2.py3-none-any.whl (477kB)
    100% |████████████████████████████████| 481kB 187kB/s 
Installing collected packages: setuptools
  Found existing installation: setuptools 18.5
    Uninstalling setuptools-18.5:
Exception:
Traceback (most recent call last):
  File "/Library/Python/2.7/site-packages/pip/basecommand.py", line 215, in main
    status = self.run(options, args)
  File "/Library/Python/2.7/site-packages/pip/commands/install.py", line 342, in run
    prefix=options.prefix_path,
  File "/Library/Python/2.7/site-packages/pip/req/req_set.py", line 778, in install
    requirement.uninstall(auto_confirm=True)
  File "/Library/Python/2.7/site-packages/pip/req/req_install.py", line 754, in uninstall
    paths_to_remove.remove(auto_confirm)
  File "/Library/Python/2.7/site-packages/pip/req/req_uninstall.py", line 115, in remove
    renames(path, new_path)
  File "/Library/Python/2.7/site-packages/pip/utils/__init__.py", line 267, in renames
    shutil.move(old, new)
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/shutil.py", line 299, in move
    copytree(src, real_dst, symlinks=True)
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/shutil.py", line 208, in copytree
    raise Error, errors
Error: [('/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python/_markerlib/__init__.py', '/tmp/pip-PlzVRR-uninstall/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python/_markerlib/__init__.py', "[Errno 1] Operation not permitted: '/tmp/pip-PlzVRR-uninstall/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python/_markerlib/__init__.py'"), ('/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python/_markerlib/__init__.pyc', '/tmp/pip-PlzVRR-uninstall/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python/_markerlib/__init__.pyc', "[Errno 1] Operation not permitted: '/tmp/pip-PlzVRR-uninstall/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python/_markerlib/__init__.pyc'"), ('/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python/_markerlib/markers.py', '/tmp/pip-PlzVRR-uninstall/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python/_markerlib/markers.py', "[Errno 1] Operation not permitted: '/tmp/pip-PlzVRR-uninstall/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python/_markerlib/markers.py'"), ('/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python/_markerlib/markers.pyc', '/tmp/pip-PlzVRR-uninstall/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python/_markerlib/markers.pyc', "[Errno 1] Operation not permitted: '/tmp/pip-PlzVRR-uninstall/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python/_markerlib/markers.pyc'"), ('/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python/_markerlib', '/tmp/pip-PlzVRR-uninstall/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python/_markerlib', "[Errno 1] Operation not permitted: '/tmp/pip-PlzVRR-uninstall/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python/_markerlib'")]

果断用zip文件安装成功,下载页面:
https://pypi.python.org/pypi/setuptools/7.0#downloads
解压 setuptools-7.0.zip
执行 sudo python setup.py install
3、Django
官方网站:https://www.djangoproject.com/download/
安装简单,直接运行:
pip install Django==1.11.3
提示我“OSError: [Errno 13] Permission denied: ‘/Library/Python/2.7/site-packages/django’”
于是加上sudo:
sudo -H pip install Django==1.11.3

关于Python3.x的问题,因为已经存在Python2.7版本了,想同时使用3.x的时候,需要使用命令python3来区别,另外pip也对应有pip3来执行,对应上面的两条命令如下:
pip3 install -U pip setuptools
pip3 install Django==1.11.3
验证安装是否正确:
执行 python3
命令行输入:
import django
print (django.get_version()) 或者django.VERSION
显示结果:

Python 3.6.2 (v3.6.2:5fd33b5926, Jul 16 2017, 20:11:06) 
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import django
>>> print (django.get_version())
1.11.3
>>> django.VERSION
(1, 11, 3, 'final', 0)