热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

python多线程机制深入理解

今天要跟大家一起来学习一下Python的多线程机制。有两个原因,其一是自己在学习中经常会使用到多线程,其二当然是自己对Python中的多线程并不是很了解

今天要跟大家一起来学习一下Python的多线程机制。有两个原因,其一是自己在学习中经常会使用到多线程,其二当然是自己对Python中的多线程并不是很了解。那么,今天和大家一起了解下~

Python多线程机制

开发多线程的应用系统,是在日常开发中经常会遇到的需求。同时,Python也为多线程系统的开发提供了很好的支持。
大家应该都知道,Python多线程机制是在GIL(Global Interpreter Lock)全局解释锁的基础上建立的。

那么Python为什么需要全局解释锁?

为什么需要全局解释锁?

我们知道,要支持多线程的话,一个基本的要求就是不同线程对共享资源访问的互斥,所以Python中引入了GIL,当然这是第一个原因。

Python中的GIL是一个非常霸道的互斥实现,在一个线程拥有了解释器的访问权之后,其它的所有线程都必须等待它释放解释器的访问权,即使这些线程的下一条指令并不会互相影响。

这样的说法也就意味着,无论如何,在同一时间,只能有一个线程能访问Python提供的API。因为单处理器的本质是不可能并行的,这里的同一时间确实对于单处理器是毫无意义的,但是对于多处理器,同一时间,确实可以有多个时间独立运行。然而正是由于GIL限制了这样的情形,使得多处理器最终退化为单处理器,性能大打折扣。那么,为什么还要使用GIL呢?这里就要提到第二个原因。

当然,Python社区也早都认识到了这个问题,并且在不断探索,Greg Stein和Mark Hammond两位老兄曾经创建过一份去除GIL的branch,但是很不幸,这个分支在很多的基准测试中,尤其是在单线程的测试上,效率只有使用GIL的一半左右。

使用GIL时,保护机制的粒度比较大,也就是我们似乎只需要将可能被多个线程共享的资源保护起来即可,对于不会被多个线程共享的资源,完全可以不用保护。但是,如果使用更细粒度的锁机制进行保护,那么,会导致大量的加锁和解锁功能,加锁和解锁对于操作系统来说,是一个比较重量级的动作,同时,没有GIL的保护,编写Python的扩展模块的难度也大大增加。

所以,目前为止,GIL仍然是多线程机制的基石。

对于Python而言,字节码解释器是Python的核心所在,所以Python通过GIL来互斥不同线程对解释器的使用。这里举个例子进行说明:

假设,现在有三个线程A、B和C,它们都需要解释器来执行字节码,进行对应的计算,那么在这之前,它们必须获得GIL。那么现在假设线程A获得了GIL,其它线程只能等A释放GIL之后,才能获得。

对!是这样没错,于是,有两个问题:

1. 线程A何时释放GIL呢(如果A使用完解释器之后才释放GIL,那么,并行的计算退化为串行,多线程的意义何在?)

2. 线程B和C谁将在A释放GIL之后获得GIL呢?

所以毫无疑问的,Python拥有其自己的一套线程调度机制。

关于线程调度

和操作系统的进程调度一样,线程调度机制主要解决两个问题:

1. 在何时挂起当前线程,选择处于等待状态的下一个线程?

2. 在众多处于等待状态的线程中,应该选择激活哪个线程?

对于何时进行线程调度的问题,是由Python自身决定的。我们可以联想操作系统进行进程切换的问题,当一个进程执行了一段时间之后,发生了时钟中断,于是操作系统响应时钟中断,并在这时开始进程的调度。

与此类似,Python中通过软件模拟了这样的中断,来激活线程的调度。Python的字节码解释器是按照指令的顺序一条一条的顺序执行从而工作的,Python内部维护着这样一个数值,作为Python内部的时钟,假设这个值为N,那么Python将在执行了N条指令之后立刻启动线程调度机制。

也就是说,当一个线程获得GIL后,Python内部的监测机制就开始启动,当这个线程执行了N条指令后,Python解释器将强制挂起当前线程,开始切换到下一个处于等待状态的线程。

在Python中,可以这样获得这个数值(N):

那么,下一个问题,Python会在众多等待的线程中选择哪一个呢?

答案是,不知道。因为这个问题是交给了底层的操作系统来解决的,Python借用了底层操作系统所提供的线程调度机制来决定下一个获得GIL进入解释器的线程是谁。

所以说,Python中的线程实际上就是操作系统所支持的原生线程。

那么,接下来,我们一起揭开Python中GIL的真实面目。

关于GIL

应该知道,Python中多线程常用的两个模块:Thread和在其之上的threading。其中Thread是使用C实现的,而Threading是用python实现。

我们可以通过Thread模块进行分析(以Python2.7.13为例)。

创建线程
首先从创建线程说起,在threadmodule.c中,thread_PyThread_start_new_thread()函数通过三个主要的动作完成一个线程的创建:

//创建bootstate结构
boot = PyMem_NEW(struct bootstate, 1);
if (boot == NULL)
return PyErr_NoMemory();
boot->interp = PyThreadState_GET()->interp;
boot->func = func;
boot->args = args;
boot->keyw = keyw;
boot->tstate = _PyThreadState_Prealloc(boot->interp);
if (boot->tstate == NULL) {
PyMem_DEL(boot);
return PyErr_NoMemory();
}
Py_INCREF(func);
Py_INCREF(args);
Py_XINCREF(keyw);
// 初始化多线程环境
PyEval_InitThreads(); 
//创建线程
ident = PyThread_start_new_thread(t_bootstrap, (void*) boot);
if (ident == -1) {
PyErr_SetString(ThreadError, "can't start new thread");
Py_DECREF(func);
Py_DECREF(args);
Py_XDECREF(keyw);
PyThreadState_Clear(boot->tstate);
PyMem_DEL(boot);
return NULL;
}
return PyInt_FromLong(ident);

1. 创建并初始化bootstate结构boot,在boot中,将保存关于Python的一切信息(线程过程,线程过程参数等)。

2. 初始化Python的多线程环境。

3. 以boot为参数,创建操作系统的原生线程。

从以上代码可以看出,Python在刚启动时,并不支持多线程,也就是说,Python中支持多线程的数据结构以及GIL都是没有创建的。当然这是因为大多数的Python程序都不需要Python的支持。

在Python虚拟机启动时,多线程机制并没有被激活,它只支持单线程,一旦用户调用thread.start_new_thread,明确的告诉Python虚拟机需要创建新的线程,这时Python意识到用户需要多线程的支持,这个时候,Python虚拟机会自动建立多线程需要的数据结构、环境以及GIL。

建立多线程环境

建立多线程环境,主要就是创建GIL。那么GIL是如何实现的呢?
打开"python/ceval.c":

static PyThread_type_lock interpreter_lock = 0; /* This is the GIL */
static PyThread_type_lock pending_lock = 0; /* for pending calls */
static long main_thread = 0;

int
PyEval_ThreadsInitialized(void)
{
return interpreter_lock != 0;
}

void
PyEval_InitThreads(void)
{
if (interpreter_lock)
return;
interpreter_lock = PyThread_allocate_lock();
PyThread_acquire_lock(interpreter_lock, 1);
main_thread = PyThread_get_thread_ident();
}

在这段代码中,iterpreter_lock就是GIL。

无论创建多少个线程,Python建立多线程环境的动作只会执行一次。在创建GIL之前,Python会检查GIL是否已经被创建,如果是,则不再进行任何动作,否则,就会去创建这个GIL。

在上述代码中,我们可以看到,创建GIL使用的是Pythread_allocate_lock完成的,下面看看该函数的内部实现:

PyThread_type_lock
PyThread_allocate_lock(void)
{
PNRMUTEX aLock;

dprintf(("PyThread_allocate_lock called\n"));
if (!initialized)
PyThread_init_thread();

aLock = AllocNonRecursiveMutex() ;

dprintf(("%ld: PyThread_allocate_lock() -> %p\n", PyThread_get_thread_ident(), aLock));

return (PyThread_type_lock) aLock;
}

可以看到该函数返回了alock,alock是结构体PNRMUTEX,实际上就是我们需要创建的那个interperter_lock(GIL)。这么说来,GIL就是结构体PNRMUTEX呀,于是我们找来它的真身:

typedef struct NRMUTEX {
LONG owned ;
DWORD thread_id ;
HANDLE hevent ;
} NRMUTEX, *PNRMUTEX ;

这里又三个变量,owned、thread_id和hevent。这里的hevent是windows平台下的Event这个内核对象,也就是通过Event来实现线程之间的互斥。thread_id将记录任一时刻获得GIL的线程的id。

那么owned是什么呢?

GIL中的owned是指示GIL是否可用的变量,它的值被初始化为-1,Python会检查这个值是否为1,如果是,则意味着GIL可用,必须将其置为0,当owned为0后,表示该GIL已经被一个线程占用,不可再用;同时,当一个线程开始等待GIL时,其owned就会被增加1;当一个线程最终释放GIL时,一定会将GIL的owned减1,这样,当所有需要GIL的线程都最终释放了GIL之后,owned将再次变为-1,意味着GIL再次变为可用。

关于Python中的多线程,今天我们就学到这里。


推荐阅读
  • 本文介绍了如何在C#中启动一个应用程序,并通过枚举窗口来获取其主窗口句柄。当使用Process类启动程序时,我们通常只能获得进程的句柄,而主窗口句柄可能为0。因此,我们需要使用API函数和回调机制来准确获取主窗口句柄。 ... [详细]
  • 本文将介绍如何编写一些有趣的VBScript脚本,这些脚本可以在朋友之间进行无害的恶作剧。通过简单的代码示例,帮助您了解VBScript的基本语法和功能。 ... [详细]
  • 1:有如下一段程序:packagea.b.c;publicclassTest{privatestaticinti0;publicintgetNext(){return ... [详细]
  • 使用 Azure Service Principal 和 Microsoft Graph API 获取 AAD 用户列表
    本文介绍了一段通用代码示例,该代码不仅能够操作 Azure Active Directory (AAD),还可以通过 Azure Service Principal 的授权访问和管理 Azure 订阅资源。Azure 的架构可以分为两个层级:AAD 和 Subscription。 ... [详细]
  • 本文探讨了如何优化和正确配置Kafka Streams应用程序以确保准确的状态存储查询。通过调整配置参数和代码逻辑,可以有效解决数据不一致的问题。 ... [详细]
  • 从 .NET 转 Java 的自学之路:IO 流基础篇
    本文详细介绍了 Java 中的 IO 流,包括字节流和字符流的基本概念及其操作方式。探讨了如何处理不同类型的文件数据,并结合编码机制确保字符数据的正确读写。同时,文中还涵盖了装饰设计模式的应用,以及多种常见的 IO 操作实例。 ... [详细]
  • 本文详细介绍了Java中的访问器(getter)和修改器(setter),探讨了它们在保护数据完整性、增强代码可维护性方面的重要作用。通过具体示例,展示了如何正确使用这些方法来控制类属性的访问和更新。 ... [详细]
  • Explore a common issue encountered when implementing an OAuth 1.0a API, specifically the inability to encode null objects and how to resolve it. ... [详细]
  • 本文介绍如何使用Objective-C结合dispatch库进行并发编程,以提高素数计数任务的效率。通过对比纯C代码与引入并发机制后的代码,展示dispatch库的强大功能。 ... [详细]
  • 本文详细介绍如何使用Python进行配置文件的读写操作,涵盖常见的配置文件格式(如INI、JSON、TOML和YAML),并提供具体的代码示例。 ... [详细]
  • Java 类成员初始化顺序与数组创建
    本文探讨了Java中类成员的初始化顺序、静态引入、可变参数以及finalize方法的应用。通过具体的代码示例,详细解释了这些概念及其在实际编程中的使用。 ... [详细]
  • 本文介绍了Java并发库中的阻塞队列(BlockingQueue)及其典型应用场景。通过具体实例,展示了如何利用LinkedBlockingQueue实现线程间高效、安全的数据传递,并结合线程池和原子类优化性能。 ... [详细]
  • 主要用了2个类来实现的,话不多说,直接看运行结果,然后在奉上源代码1.Index.javaimportjava.awt.Color;im ... [详细]
  • 题目描述:给定n个半开区间[a, b),要求使用两个互不重叠的记录器,求最多可以记录多少个区间。解决方案采用贪心算法,通过排序和遍历实现最优解。 ... [详细]
  • 本文详细介绍了Java中org.eclipse.ui.forms.widgets.ExpandableComposite类的addExpansionListener()方法,并提供了多个实际代码示例,帮助开发者更好地理解和使用该方法。这些示例来源于多个知名开源项目,具有很高的参考价值。 ... [详细]
author-avatar
物之美者,招摇之桂
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有