如何正确区分Python线程

开发 后端
怎么区分Python线程是属于哪个状态对象呢,幸好我们还有线程id呢,ID存储的正是各个线程的id,根据这个有效ID,可以区分不同的Python线程。

在Python语言中Python线程可以从这里开始与主线程对GIL的竞争,在t_bootstrap中,申请完了GIL,也就是说子线程也就获得了GIL,使其始终保存着活动线程的状态对象。

当PyEval_AcquireThread结束之后,子线程也就获得了GIL,并且做好了一切执行的准备。接下来子线程通过PyEval_ CallObjectWithKeywords,将最终调用我们已经非常熟悉的PyEval_EvalFrameEx。

也就是Python的字节码执行引擎。传递进PyEval_CallObjectWithKeywords的boot->func是一PyFunctionObject对象,正是therad1.py中定义的threadProc编译后的结果。在PyEval_CallObjectWithKeywords结束之后,子线程将释放GIL,并完成销毁线程的所有扫尾工作,到了这里,子线程就结束了。

从t_bootstrap的代码看上去,似乎子线程会一直执行,直到子线程的所有计算都完成,才会通过PyThreadState_DeleteCurrent释放GIL。如此一来,那主线程岂非一直都会处于等待GIL的状态?如果真是这样,那Python线程显然就不可能支持多线程机制了。

实际上在PyEval_EvalFrameEx中,图15-2中显示的Python内部维护的那个模拟时钟中断会不断地激活线程的调度机制,在子线程和主线程之间不断地进行切换。从而真正实现多线程机制,当然,这一点我们将在后面详细剖析。现在我们感兴趣的是子线程在PyEval_AcquireThreade中到底做了什么。

到这里,了解了PyEval_AcquireThread,似乎创建线程的机制都清晰了。但实际上,有一个非常重要的机制——线程状态保护机制——隐藏在了一个毫不起眼的地方:PyThreadState_New。

  1. [threadmodule.c]  
  2.  
  3. static PyObject* thread_PyThread_start_new_thread(PyObject *self, PyObject  
  4.  
  5.   *fargs)  
  6.  
  7. {  
  8.  
  9.     PyObject *func, *args, *keyw = NULL;  
  10.  
  11.     struct bootstate *boot;  
  12.  
  13.     long ident;  
  14.  
  15.     PyArg_UnpackTuple(fargs, "start_new_thread", 2, 3, &func, &args, &keyw);  
  16.  
  17.     //[1]:创建bootstate结构  
  18.  
  19.     boot = PyMem_NEW(struct bootstate, 1);  
  20.  
  21.     boot->interp = PyThreadState_GET()->interp;  
  22.  
  23.     boot->funcfunc = func;  
  24.  
  25.     boot->argsargs = args;  
  26.  
  27.     boot->keywkeyw = keyw;  
  28.  
  29.     //[2]:初始化多线程环境  
  30.  
  31.     PyEval_InitThreads(); /* Start the interpreter's thread-awareness */  
  32.  
  33.     //[3]:创建线程  
  34.  
  35.     ident = PyThread_start_new_thread(t_bootstrap, (void*) boot);  
  36.  
  37.     return PyInt_FromLong(ident);  
  38.  
  39. [thread.c]  
  40.  
  41. /* Support for runtime thread stack size tuning.  
  42.  
  43.    A value of 0 means using the platform's default stack size  
  44.  
  45.    or the size specified by the THREAD_STACK_SIZE macro. */  
  46.  
  47. static size_t _pythread_stacksize = 0;  
  48.  
  49. [thread_nt.h]  
  50.  
  51. long PyThread_start_new_thread(void (*func)(void *), void *arg)  
  52.  
  53. {  
  54.  
  55.     unsigned long rv;  
  56.  
  57.     callobj obj;  
  58.  
  59.     obj.id = -1;    /* guilty until proved innocent */  
  60.  
  61.     obj.func = func;  
  62.  
  63.     obj.arg = arg;  
  64.  
  65.     obj.done = CreateSemaphore(NULL, 0, 1, NULL);  
  66.  
  67.     rv = _beginthread(bootstrap, _pythread_stacksize, &obj); /* use default stack size */  
  68.  
  69.     if (rv == (unsigned long)-1) {  
  70.  
  71.         //创建raw thread失败  
  72.  
  73.         obj.id = -1;  
  74.  
  75.     }  
  76.  
  77.     else {  
  78.  
  79.         WaitForSingleObject(obj.done, INFINITE);  
  80.  
  81.     }  
  82.  
  83.     CloseHandle((HANDLE)obj.done);  
  84.  
  85.     return obj.id;  
  86.  
  87. }  

这个机制对于理解Python线程的创建和维护是非常关键的。要剖析线程状态的保护机制,我们首先需要回顾一下线程状态。在Python中,每一个Python线程都会有一个线程状态对象与之关联。

在线程状态对象中,记录了每一个线程所独有的一些信息。实际上,在剖析Python的初始化过程时,我们曾经见过这个对象。每一个线程对应的线程状态对象都保存着这个线程当前的PyFrameObject对象,线程的id这样一些信息。有时候,线程是需要访问这些信息的。

比如考虑一个最简单的情形,在某种情况下,每个线程都需要访问线程状态对象中所保存的thread_id信息,显然,线程A获得的应该是A的thread_id,线程B亦然。倘若线程A获得的是B的thread_id,那就坏菜了。这就意味着Python线程内部必须有一套机制,这套机制与操作系统管理进程的机制非常类似。

我们知道,在操作系统从进程A切换到进程B时,首先会保存进程A的上下文环境,再进行切换;当从进程B切换回进程A时,又会恢复进程A的上下文环境,这样就保证了进程A始终是在属于自己的上下文环境中运行。

这里的线程状态对象就等同于进程的上下文,Python同样会有一套存储、恢复线程状态对象的机制。同时,在Python内部,维护着一个全局变量:PyThreadState * _PyThread- State_Current。

当前活动线程所对应的线程状态对象就保存在这个变量里,当Python调度线程时,会将被激活的线程所对应的线程状态对象赋给_PyThreadState_Current,使其始终保存着活动线程的状态对象。

这就引出了这样的一个问题:Python如何在调度进程时,获得被激活线程对应的状态对象?Python内部会通过一个单向链表来管理所有的Python线程的状态对象,当需要寻找一个线程对应的状态对象时。#t#

就遍历这个链表,搜索其对应的状态对象。在此后的描述中,我们将这个链表称为“状态对象链表”。下面我们来看一看实现这个机制的关键数据结构在Python中,对于这个状态对象链表的访问,不必在GIL的保护下进行。

因为对于这个状态对象链表,Python线程会创建一个独立的锁,专职对状态对象链表进行保护。这个锁的创建是在Python进行初始化的时候完成的。PyThread_create_key将创建一个新的key。注意,这里的key都是一个整数,而且,当PyThread_create_key***次被调用时(在_PyGILState_Init中的调用正是***次调用)。

会通过PyThread_allcate_lock创建一个keymutex。根据我们前面的分析,这个keymutex实际上和GIL一样,都是一个PNRMUTEX结构体,而在这个结构体中,维护着一个Win32下的Event内核对象。这个keymutex的功能就是用来互斥对状态对象链表的访问。

在_PyGILState_Init中,创建的新key被Python维护的全局变量autoTLSkey接收,其中的TLS是Thread Local Store的缩写,这个autoTLSkey将用作Python保存所有线程的状态对象的一个参数。的key值。也就是说,状态对象列表中所有key结构体中的key值都会是autoTLSkey。哎,那位看官说了,你看PyThread_create_key返回的是nkeys的递增后的值啊。

就是说每create一次,得到的结果都是不同的,怎么能说所有的key都是一样的呢?事实上,在整个Python的源码中,PyThread_create_key只在_PyGILState_Init中被调用了,而这个_PyGILState_Init只会在Python运行时环境初始化时调用一次。

责任编辑:chenqingxiang 来源: 比特网
相关推荐

2015-05-19 16:21:05

2010-08-26 09:40:00

2010-08-25 13:13:04

2010-06-07 09:03:33

MySQL大小写

2019-03-13 22:40:15

机器学习假设算法

2023-09-08 12:19:01

线程方法interrupt

2022-02-28 07:01:22

线程中断interrupt

2010-02-22 10:06:17

Python调用

2010-02-03 14:15:18

Python 开发

2010-02-03 17:42:30

2010-02-22 14:13:38

安装Python

2010-02-02 18:20:43

Python编写

2010-02-03 15:40:37

Python函数

2010-02-24 15:27:26

Python数组

2021-03-15 12:23:24

Pythonyield代码

2018-03-30 09:21:30

程序员网络招聘

2010-02-03 14:37:10

Python 开发环境

2010-02-02 14:11:14

Python 进行编程

2020-08-25 07:16:20

Python资源文件文件

2010-02-24 09:47:05

Python 源文件
点赞
收藏

51CTO技术栈公众号