多线程C调用python api的陷阱

来源:互联网 发布:傲剑绿色版紫霞数据 编辑:程序博客网 时间:2024/05/29 06:53

众所周知,用脚本语言编写的服务(wsgi接口)都需要一个server容器,常见的如php的php-fpm, lightd等。python中一般是用的uwsgi,uwsgi是在wsgi的基础上的一种新的协议,可以用来部署python等脚本程序的运行。然而在不熟悉uwsgi的代码架构和c调用python的api情况下进行开发可能会遇到一些意想不到的问题。


我们先看一段代码,下面这段代码是用的Flask框架,每次请求的时候会把COUNT的值先减一再加一,最后再乘二。如果请求50次,其最终的结果应该是2的50次幂。


from flask import Flask, requestCOUNT = 1 app = Flask(__name__)@app.route('/test_uwsgi')def index():    global COUNT    COUNT=COUNT-1    COUNT=COUNT+1    COUNT=COUNT*2    print COUNT    return 'OK'


17179869184343597383686871947673613743895347227487790694454975581388810995116277762199023255552439804651110487960930222081759218604441635184372088832703687441776641407374883553282814749767106565629499534213121125899906842624


这是直接执行50次index函数得到的最后几行的结果,可得结果为2的50次幂。

 536870912 10737418242147483648429496729685899345921717986918434359738368 687194767361374389534722748779069445497558138881099511627776 2199023255552 4398046511104879609302220817592186044416 3518437208883270368744177664 1407374883553282814749767106565629499534213121125899906842624

这是通过ab测试,用多个并发访问/test_uwsgi接口50次得到的最后几行的结果。可以看出最终的结果肯定是个异常数字。为什么程序在uwsgi中运行时的运行结果会出现异常呢?

其实大家通过阅读这个简单的例子就可以发现,这种例子一般都是用来演示多线程共享数据同步问题的时候,如果不加锁会暴露问题的例子。下面的代码我们就在修改共享资源COUNT的时候加上互斥锁,看看有没有什么变化。

from flask import Flask, requestimport threadingmutex = threading.Lock()COUNT = 1 app = Flask(__name__)@app.route('/test_uwsgi')def index():    global COUNT    global mutex    mutex.acquire()    COUNT=COUNT-1    COUNT=COUNT+1    COUNT=COUNT*2    print COUNT    mutex.release()    return 'OK'

上面的代码也是放到uwsgi的容器里面运行,通过http接口多个并发访问50次,得到的结果是正确的。但是这是为什么呢?在我们原来的python代码中并没有写任何涉及多进程的操作,虽然uwsgi在配置文件中开启了多个线程可以并发的处理请求,但是按笔者原来的理解,不是应该每个线程执行自己独立的Python解释器吗?每个线程在运行python脚本的时候的数据不应该是隔离的吗?

为了弄明白上面的问题,我们不得不研究研究uwsgi及其server架构中的结构和设计。

UWSGI是在python中广泛使用的一个服务器应用容器,类似于php上常见的wsgi协议的服务器应用容器,如mod-php、php-fpm、lightd等。uwsgi协议是在原有的wsgi协议之上新增了一套uwsgi的协议。


通过研读uwsgi的源码(core/uwsgi.c core/loop.c core/init.c core/master_util.c core/util.c),可以知道uwsgi的server设计,采用的是UNX书中介绍归纳的服务器程序设计范式8,暨TCP预先创建线程服务器程序,每个线程各自accept。

int main(int argc, char *argv[], char *envp[]) {uwsgi_setup(argc, argv, envp);return uwsgi_run();}void uwsgi_setup(int argc, char *argv[], char *envp[]) {int i;struct utsname uuts;.........设置和初始化各种资源,这里就省略了,有兴趣的自己看看......//最主要的是这行uwsgi_start((void *) uwsgi.argv);}int uwsgi_start(void *v_argv) {......简化摘要一些主要的代码......... 题外话,这里是创建一个多线程的共享内存空间,后面uwsgi_setup_workers的时候会用到。因为uwsgi有一个master进程,可以监测各个子进程的状态,所以需要一块匿名共享内存// initialize sharedareasuwsgi_sharedareas_init();// setup queueif (uwsgi.queue_size > 0) {uwsgi_init_queue();}... 这里很重要,uwsgi.p是一个接口,uwsgi中部署的app在这里初始化(在uwsgi中,部署的APP需要所对应语言的插件,如python就用python插件)后面也会看到,实际上uwsgi所执行的python代码,其所有模块的import都在这里执行// initialize request plugin only if workers or master are availableif (uwsgi.sockets || uwsgi.master_process || uwsgi.no_server || uwsgi.command_mode || uwsgi.loop) {for (i = 0; i < 256; i++) {if (uwsgi.p[i]->init) {uwsgi.p[i]->init();}}}// again check for workers/sockets...if (uwsgi.sockets || uwsgi.master_process || uwsgi.no_server || uwsgi.command_mode || uwsgi.loop) {for (i = 0; i < 256; i++) {if (uwsgi.p[i]->post_init) {uwsgi.p[i]->post_init();}}}。。。这里主要是设置各个worker的共享内存空间// initialize workers/master shared memory segmentsuwsgi_setup_workers();// here we spawn the workers...if (!uwsgi.status.is_cheap) {if (uwsgi.cheaper && uwsgi.cheaper_count) {int nproc = uwsgi.cheaper_initial;if (!nproc)nproc = uwsgi.cheaper_count;for (i = 1; i <= uwsgi.numproc; i++) {if (i <= nproc) {if (uwsgi_respawn_worker(i))break;uwsgi.respawn_delta = uwsgi_now();}else {uwsgi.workers[i].cheaped = 1;}}}else {for (i = 2 - uwsgi.master_process; i < uwsgi.numproc + 1; i++) {。。。这里就是根据我们设置的进程数,去fork子进程if (uwsgi_respawn_worker(i))break;uwsgi.respawn_delta = uwsgi_now();}}}// END OF INITIALIZATIONreturn 0;}int uwsgi_respawn_worker(int wid) {。。。主要是这行代码,fork子进程,里面就不跟了pid_t pid = uwsgi_fork(uwsgi.workers[wid].name);if (pid == 0) {signal(SIGWINCH, worker_wakeup);signal(SIGTSTP, worker_wakeup);uwsgi.mywid = wid;uwsgi.mypid = getpid();// pid is updated by the master//uwsgi.workers[uwsgi.mywid].pid = uwsgi.mypid;// OVERENGINEERING (just to be safe)uwsgi.workers[uwsgi.mywid].id = uwsgi.mywid;/*   uwsgi.workers[uwsgi.mywid].harakiri = 0;   uwsgi.workers[uwsgi.mywid].user_harakiri = 0;   uwsgi.workers[uwsgi.mywid].rss_size = 0;   uwsgi.workers[uwsgi.mywid].vsz_size = 0; */// do not reset worker counters on reload !!!//uwsgi.workers[uwsgi.mywid].requests = 0;// ...but maintain a delta counter (yes this is racy in multithread)//uwsgi.workers[uwsgi.mywid].delta_requests = 0;//uwsgi.workers[uwsgi.mywid].failed_requests = 0;//uwsgi.workers[uwsgi.mywid].respawn_count++;//uwsgi.workers[uwsgi.mywid].last_spawn = uwsgi.current_time;}else if (pid < 1) {uwsgi_error("fork()");}else {// the pid is set only in the master, as the worker should never use ituwsgi.workers[wid].pid = pid;if (respawns > 0) {uwsgi_log("Respawned uWSGI worker %d (new pid: %d)\n", wid, (int) pid);}else {uwsgi_log("spawned uWSGI worker %d (pid: %d, cores: %d)\n", wid, pid, uwsgi.cores);}}return 0;}int uwsgi_run() {。。。也是捡重要的摘抄一些如果pid是master,就执行master_loop如果pid是worker,就执行uwsgi_worker_run// !!! from now on, we could be in the master or in a worker !!!if (getpid() == masterpid && uwsgi.master_process == 1) {(void) master_loop(uwsgi.argv, uwsgi.environ);}//from now on the process is a real workeruwsgi_worker_run();// never here_exit(0);}void uwsgi_worker_run() {int i;if (uwsgi.lazy || uwsgi.lazy_apps) {uwsgi_init_all_apps();}uwsgi_ignition();// never hereexit(0);}void uwsgi_ignition() {if (uwsgi.loop) {void (*u_loop) (void) = uwsgi_get_loop(uwsgi.loop);if (!u_loop) {uwsgi_log("unavailable loop engine !!!\n");exit(1);}if (uwsgi.mywid == 1) {uwsgi_log("*** running %s loop engine [addr:%p] ***\n", uwsgi.loop, u_loop);}u_loop();uwsgi_log("your loop engine died. R.I.P.\n");}else {。。。子进程的循环体,一般是用simple_loopif (uwsgi.async < 1) {simple_loop();}else {async_loop();}}// end of the process...end_me(0);}。。。一直到这里,在子进程的loop里面才开始创建接收处理request请求的线程线程的执行函数simple_loop_run也是一个循环,基本上都是常规步奏,accept,receive, response...,后面就不继续追下去了在reciev接到请求的数据后,会通过python_call的方法调用python脚本的wsgi函数,处理这个请求void simple_loop() {uwsgi_loop_cores_run(simple_loop_run);}void uwsgi_loop_cores_run(void *(*func) (void *)) {int i;for (i = 1; i < uwsgi.threads; i++) {long j = i;pthread_create(&uwsgi.workers[uwsgi.mywid].cores[i].thread_id, &uwsgi.threads_attr, func, (void *) j);}long y = 0;func((void *) y);}

简单来说,就是uwsgi中执行python脚本和直接运行python脚本是不同的。uwsgi执行python脚本是通过调用python c api的方法,首先通过调用api载入python脚本中的module,这时候,像最开始的实例代码一样module import中的相关代码会被执行,所有的全局变量在进程中被创建和初始化。然后uwsgi创建线程,开始处理请求调用python api(python_call),执行python脚本中处理请求的函数(wsgi接口),因为module import在线程创建之前已经执行了,所以之前在进程中的共享数据在线程中是可以访问的。这里就是需要我们着重注意的,在访问这些线程间共享数据的时候需要加锁,或者在编写python脚本的时候尽量少用全局变量而多用单例模式,避免不必要的采坑。

其实上面所述只是对我遇到问题的一个简化,为了帮助大家弄明白uwsgi多线程执行python wsgi接口的相关问题。我所遇到的问题是在处理请求的函数中,调用了一个在全局中创建的gearman client,这个client库不是线程安全的,使用中也没有加锁。当请求的并发比较大的时候,gearman client这个库就会报出一些连接的异常。


由于GIL的存在,Python的多线程并不能充分的利用多核的优势。在实际项目中,我们常常使用多进程来取代多线程的方法,来实现一些需要并发的业务。然而,进程的开销毕竟比较大,并且设计进程间的数据同步,进程间通信等操作相对线程来说十分的复杂,也是开发中的一个痛点,我们经常为了性能而放弃使用python转而使用c多线程的方案,但是确实降低了代码的可维护性,增加了代码的成本。

在了解了uwsgi的多线程结构之后,其实我们也可以学习其通过多线程调用python c api的方法,使用c的线程调用python的业务代码,将需要共享的数据放在c线程创建之前进行module import。将c多线程的部分提取出来成为一个框架,工程师只需要书写python的业务代码并在注意在使用共享数据的时候加锁。框架部分交由专业的有经验的c/python工程师进行维护,这样在不牺牲代码生产效率的前提下,提升了程序的性能。



后记:其实这个问题并不是十分的复杂,暴露的问题是对于uwsgi的代码结构,和python c api以及c调用python的方法和相关概念等不是非常的熟练,暴露了自己知识体系中的短板。由于那几天同时在开发几个需求,没有对问题进行详细的测试,没有仔细的分析和查找Trackback中的错误,反而是一直在怀疑被调用方的接口性能问题。其实这个团队在交流上确实也存在一些问题,争论基本上靠喊,靠抢话,不是就是论事而是经常人身攻击。每当有工程师的方案确实在理,确实证明是可用的时候,反对的人也宁死不妥协。。。这大概就是像新浪这样臃肿老旧缺少活力的大公司的通病吧(一点吐槽,熟人请无视)。

0 0
原创粉丝点击