您所在的位置:读书频道 > 设计开发 > 其它开发 > 15.4.2 阻塞调度

15.4.2 阻塞调度

2008-07-23 16:46 陈儒著 电子工业出版社 我要评论(0) 字号:T | T
一键收藏,随时查看,分享好友!

《Python源码剖析--深度探索动态语言核心技术》第15章主要讲述的是Python 多线程机制,本小节为大家介绍的是 阻塞调度。

AD:

15.4.2 阻塞调度

标准调度是在Python 执行完了可执行的指令条数之后才发生的,但是在实际中,Python 需要支持另一种触发线程调度的情形,我们称之为阻塞调度。其基本思想是:在线程A 通过某种操作,比如说等待输入,将自身阻塞后,Python 应该将等待GIL 的线程B唤醒。

考虑我们的thread1.py,在主线程和子线程中都有time.sleep(1)的调用。假如子线程调用了time.sleep(1),那么子线程将释放GIL,挂起自身,Python 唤醒主线程。同样,在主线程中调用time.sleep(1),也将使Python 唤醒子线程。这展示了一种情形,即程序有时候希望将自己挂起。

除了这种线程主动放弃GIL 的情况之外,还有另一种情形,即程序不得不挂起。还是考虑我们的thread1.py,假如在主线程的thread.start_new_thread(threadProc, ())之后我们调用raw_input(),那么主线程必须等待用户的输入,这时,主线程也不得不释放GIL,将自身挂起。

我们通过研究time.sleep()来剖析Python 是如何实现这种阻塞调度的。在Python中,time module 在timemodule.c 中实现,而其中的sleep 操作由time_sleep 函数,进而由floatsleep 函数实现。

[timemodule.c]
static int floatsleep(double secs)
{
double millisecs = secs * 1000.0;
unsigned long ul_millis;
Py_BEGIN_ALLOW_THREADS
/* Allow sleep(0) to maintain win32
semantics, and as decreed
* by Guido, only the main thread can be interrupted.
*/
ul_millis = (unsigned long)millisecs;
if (ul_millis == 0 || main_thread !=
PyThread_get_thread_ident())
Sleep(ul_millis);
Py_END_ALLOW_THREADS
……
return 0;
}

实际上,Sleep 机制也是平台相关的,这里我们只展示了Win32 平台下的sleep 实现。同时,由于Win32 平台下的sleep 实现也比较复杂,我们关注的焦点并不是timemodule的实现,而是阻塞调度机制是如何实现的,所以我们只列出了子线程调用sleep 时涉及的相关代码。我们看到,Python 实际上是通过调用Win32 的系统API:sleep 来实现了阻塞的机制。那么在调用sleep 之前,子线程肯定需要将GIL 释放。在floatsleep 中,我们注意到在调用Sleep 之前,有一个Py_BEGIN_ALLOW_THREADS,与之对应的,在调用Sleep之后,还有一个Py_END_ALLOW_THREADS,正是这两个宏完成了触发Python 进行线程调度的工作。
[ceval.h]
#define Py_BEGIN_ALLOW_THREADS { \
PyThreadState *_save; \
_save = PyEval_SaveThread();
#define Py_END_ALLOW_THREADS
PyEval_RestoreThread(_save); \
}
[ceval.c]
PyThreadState* PyEval_SaveThread(void)
{
PyThreadState *tstate = PyThreadState_Swap(NULL);
if (interpreter_lock)
PyThread_release_lock(interpreter_lock);
return tstate;
}
void PyEval_RestoreThread(PyThreadState *tstate)
{
if (interpreter_lock) {
int err = errno;
PyThread_acquire_lock(interpreter_lock, 1);
errno = err;
}
PyThreadState_Swap(tstate);
}


在Py_BEGIN_ALLOW_THREADS 这个宏定义的代码中,子线程释放了GIL,这将唤醒等待GIL 的主线程;而在Py_END_ALLOW_THREADS 宏所定义的代码中,子线程重新申请GIL。注意,在子线程调用了Py_BEGIN_ALLOW_THREAD 之后,它就不再受GIL 的约束。从这时开始,Python 的两个线程都可能被操作系统的线程调度机制选中,直到子线程通过Py_END_ALLOW_THREADS 申请GIL 为止,Python 又恢复为只能有一个线程被操作系统的线程调度机制选中。

这意味着Python 的线程在某种情况下可以脱离GIL 的控制,然而我们看到,在Py_BEGIN_ALLOW_THREAD 和Py_END_ALLOW_THREADS 之间,子线程并没有调用任何Python 的C API,只是调用了操作系统的API,这不会导致共享资源的访问冲突,所以依然是线程安全的。开始的时候我们就说过,在理论上,Python 并不是一定要GIL 这样的解释器级的互斥线程的机制,只要能保护共享资源即可,而当前Python 采用的GIL 只是多种线程互斥机制中的一种而已。

同样,对于raw_input 而言,其最终将由PyOS_Readline 实现,我们最终也会在PyOS_Readline 中发现Py_BEGIN_ALLOW_THREAD 和Py_END_ALLOW_THREADS 联袂的身影。Python 正是通过这两个宏实现了阻塞调度机制。

有趣的是,在线程通过阻塞调度切换时,Python 内部的那个_Py_Ticker 依然会被保持,并不会被重置为100,只有标准调度才会重置这个Python 的模拟时钟。在图15-10 中,清晰地显示了这一结果。注意,这时需要将thread1.py 中的两条time.sleep 语句打开,以便激发阻塞调度。

  
图15-10 阻塞调度与标准调度结合下的线程切换

从图15-10 中我们看到阻塞调度确实是独立于标准调度另一种线程调度机制。而阻塞调度确实没有重置_Py_Ticker,否则Python 显示出来的值决不会是137 这样小的值了。需要注意的,图15-10 和图15-9 中的输出从120 多增长到了130 多,是因为图15-9 对应的例子中我们只使用了标准调度,图15-10 中则混合使用了标准调度和阻塞调度,这多出来的10 几条正是Python 执行time.sleep()时所消耗的指令数。

【责任编辑:夏书 TEL:(010)68476606】

回书目   上一节   下一节

网友评论TOP5

查看所有评论(

提交评论

  1. Linux服务器配置全程实录
  2. 揭秘--优秀PPT这样制作

热点专题

更多>>

读书

Linux环境下C编程指南
本书系统地介绍了在Linux平台下用C语言进行程序开发的过程,通过列举大量的程序实例,使读者很快掌握在Linux平台下进行C程序开发

51CTO旗下网站

领先的IT技术网站 51CTO 领先的中文存储媒体 WatchStor 中国首个CIO网站 CIOage 中国首家数字医疗网站 HC3i 移动互联网生活门户 灵客风LinkPhone