多线程C调用python api的陷阱

本文通过一个Flask框架的示例,探讨了在UWSGI多线程环境中,C调用Python API时可能遇到的共享数据同步问题。在不加锁的情况下,多线程并发访问导致结果异常。通过加锁解决并发问题后,运行结果正确。文章分析了UWSGI的服务器设计,并指出Python的GIL限制了多线程的并行性,建议在需要高性能时考虑C多线程结合Python C API的方式,但要注意代码维护性。最后,作者反思了团队沟通和问题解决过程中的不足。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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


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


from flask import Flask, request

COUNT = 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'


17179869184
34359738368
68719476736
137438953472
274877906944
549755813888
1099511627776
2199023255552
4398046511104
8796093022208
17592186044416
35184372088832
70368744177664
140737488355328
281474976710656
562949953421312
1125899906842624


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

 536870912
 1073741824
2147483648
4294967296
8589934592
17179869184
34359738368
 68719476736
137438953472
274877906944
549755813888
1099511627776
 2199023255552
 4398046511104
8796093022208
17592186044416
 35184372088832
70368744177664
 140737488355328
281474976710656
5629499534213121125899906842624

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

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

from flask import Flask, request
import threading

mutex = 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 sharedareas
	uwsgi_sharedareas_init();

	// setup queue
	if (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 available
	if (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 segments
	uwsgi_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 INITIALIZATION
	return 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 it
		uwsgi.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 worker
	uwsgi_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 here
	exit(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_loop
		if (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中的错误,反而是一直在怀疑被调用方的接口性能问题。其实这个团队在交流上确实也存在一些问题,争论基本上靠喊,靠抢话,不是就是论事而是经常人身攻击。每当有工程师的方案确实在理,确实证明是可用的时候,反对的人也宁死不妥协。。。这大概就是像新浪这样臃肿老旧缺少活力的大公司的通病吧(一点吐槽,熟人请无视)。

<think>好的,我现在需要回答用户关于“算法线程是使用调用外部函数”的问题。首先,我需要明确用户的问题具体指的是什么。用户可能是在问算法线程如何通过调用外部函数来执行任务,或者是在探讨多线程编程中算法线程与外部函数的关系。接下来,我需要确定“算法线程”的具体定义,因为不同的编程语境下可能有不同的解释。 首先,我应该从基本概念入手,解释什么是线程。线程是操作系统能够进行运算调度的最小单位,被包含在进程之中,是进程中的实际运作单位。一个进程可以有多个线程,共享进程的资源。而“算法线程”可能指的是执行特定算法的线程,或者是处理计算密集型任务的线程。 然后,用户提到“调用外部函数”。这里的外部函数可能指的是线程在执行过程中调用的其他模块、库函数或系统API。比如,在C++中,线程可能调用标准库函数,或者第三方库中的函数;在Java中,线程可能调用其他类的方法或外部JAR包中的函数。 接下来,我需要考虑多线程编程中调用外部函数的常见场景和注意事项。例如,线程安全是一个重要的问题。如果多个线程同时调用同一个外部函数,而该函数不是线程安全的(比如使用了共享的静态变量),可能会导致数据竞争或不可预知的结果。因此,在调用外部函数时,需要确保函数的线程安全性,或者通过同步机制(如互斥锁、信号量)来管理对共享资源的访问。 此外,性能也是一个需要考虑的因素。频繁调用外部函数,尤其是涉及I/O操作或复杂计算的函数,可能会影响线程的执行效率。优化方法可能包括减少阻塞调用、使用异步I/O或合理分配任务给不同的线程。 我还需要举一些具体的例子来说明。例如,在Python中,使用threading模块创建线程,线程可以调用外部定义的函数;在C++中,使用std::thread创建线程,并将函数指针或可调用对象作为参数传递。每个例子都需要说明如何正确调用外部函数,并注意线程间的同步与通信。 同时,可能需要解释一些相关的概念,如回调函数、动态链接库(DLL)的使用、以及在不同编程语言中处理外部函数调用的差异。比如,在Java中调用JNI(Java Native Interface)来执行本地方法,这涉及到与C/C++代码的交互,此时线程管理需要特别注意本地代码的线程安全性。 最后,我应该总结调用外部函数在算法线程中的重要性,以及在实际开发中需要注意的问题,如资源管理、错误处理和调试技巧。确保用户不仅了解如何实现,还能避免常见的陷阱。</think>以下是对算法线程调用外部函数的介绍,按步骤梳理关键知识点: ### 一、基本概念 1. **线程**:操作系统调度的最小执行单元,共享进程资源(内存、文件等) 2. **算法线程**:特指执行特定算法逻辑的线程,常见于: - 数值计算(如矩阵运算) - 数据处理(如排序/搜索) - 机器学习推理 ### 二、调用外部函数的典型场景 #### 场景1:库函数调用 $$ \text{线程任务} \rightarrow \boxed{\text{调用数学库}} \rightarrow \boxed{\text{BLAS/LAPACK}} $$ 例:C++线程调用Eigen库进行矩阵分解 #### 场景2:系统API调用 ```python # Python示例 import threading import os def file_operation(): with open("data.txt", "w") as f: # 调用系统级文件操作 f.write("Hello Thread") t = threading.Thread(target=file_operation) t.start() ``` #### 场景3:跨语言调用 Java线程通过JNI调用C函数: ```java public class NativeThread extends Thread { public native void nativeProcess(); // 声明本地方法 public void run() { nativeProcess(); // 调用外部C/C++函数 } static { System.loadLibrary("nativeLib"); // 加载动态库 } } ``` ### 三、关键技术要点 1. **线程安全**(需重点关注) - 可重入函数:无静态变量/全局变量依赖 - 不可重入函数:需加锁保护(如互斥锁) 2. **参数传递规范** - 值传递 vs 引用传递 - 跨语言调用时的类型映射(如JNI的jint对应C的int32_t) 3. **性能优化手段** - 减少系统调用次数 - 批量处理数据(如缓冲区聚合) - 异步I/O(如Linux的io_uring) ### 四、实践建议 1. **调试技巧**: - 使用线程检查工具(Valgrind/Helgrind) - 打印线程ID辅助诊断: $$ \text{ThreadID} = \text{pthread_self()} \quad (\text{in C}) $$ $$ \text{current_thread().ident} \quad (\text{in Python}) $$ 2. **错误处理**: - 捕获外部函数返回值 - 设置线程专属错误码(避免全局变量污染) 3. **资源管理**: - 文件描述符需线程自行关闭 - 动态内存分配遵循"谁申请谁释放"原则 ### 五、典型问题解决方案 | 问题现象 | 根源分析 | 解决方法 | |---------|----------|----------| | 数据竞争 | 多线程同时修改共享资源 | 使用互斥锁/原子操作 | | 死锁 | 锁获取顺序不一致 | 统一锁获取顺序 | | 内存泄漏 | 未释放外部函数分配的内存 | 使用RAII机制 | 通过合理设计外部函数调用策略,算法线程既能保持高效运算,又能安全整合系统资源与第三方库能力。实际开发中建议优先选择线程安全的库函数,必要时通过封装创建线程安全接口。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值