python多线程编程笔记

多线程编程

多线程编程对于具有如下特点的编程任务而言是非常理想的:本质上是异步的;需要多个并发活动;每个活动的处理顺序可能是不确定的,或者说随机的、不可预测的。这种编程任务可以被组织或划分成多个执行流,其中每个执行流都有一个指定要完成的任务。根据应用的不同,这些子任务可能需要计算出中间结果,然后合并为最终的输出结果。
计算密集型的任务可以比较容易地划分成多个子任务,然后按顺序执行或按照多线程方式执行。而那种使用单线程处理多个外部输入源的任务就不那么简单了。如果不使用多线程,要实现这种编程任务就需要为串行程序使用一个或多个计时器,并实现一个多路复用方案。

进程:进程(有时称为重量级进程)则是一个执行中的程序。每个进程都拥有自己的地址空间、内存、数据栈以及其他用于跟踪执行的辅助数据。操作系统管理其上所有进程的执行,并为这些进程合理地分配时间。进程也可以通过派生(fork 或 spawn)新的进程来执行其他任务,不过因为每个新进程也都拥有自己的内存和数据栈等,所以只能采用进程间通信(IPC)的方式共享信息。

线程:线程(有时候称为轻量级进程)与进程类似,不过它们是在同一个进程下执行的,并共享相同的上下文 线程包括开始、执行顺序和结束三部分。它有一个指令指针,用于记录当前运行的上下文。当其他线程运行时,它可以被抢占(中断)和临时挂起(也称为睡眠) ——这种做法叫做让步(yielding)。
一个进程中的各个线程与主线程共享同一片数据空间,因此相比于独立的进程而言,线
程间的信息共享和通信更加容易。线程一般是以并发方式执行的,正是由于这种并行和数据共享机制,使得多任务间的协作成为可能。当然,在单核 CPU 系统中,因为真正的并发是不可能的,所以线程的执行实际上是这样规划的:每个线程运行一小会儿,然后让步给其他线程(再次排队等待更多的 CPU 时间)。在整个进程的执行过程中,每个线程执行它自己特定的任务,在必要时和其他线程进行结果通信。

全局解释器锁
Python 代码的执行是由 Python 虚拟机(又名解释器主循环)进行控制的。 Python 在
设计时是这样考虑的,在主循环中同时只能有一个控制线程在执行,就像单核 CPU 系统中的多进程一样。内存中可以有许多程序,但是在任意给定时刻只能有一个程序在运行。同理,尽管 Python 解释器中可以运行多个线程,但是在任意给定时刻只有一个线程会被解释器执行。
对 Python 虚拟机的访问是由全局解释器锁(GIL)控制的。这个锁就是用来保证同时只能有一个线程运行的。在多线程环境中, Python 虚拟机将按照下面所述的方式执行。
1.设置 GIL。
2.切换进一个线程去运行。
3.执行下面操作之一。
a.指定数量的字节码指令。
b.线程主动让出控制权(可以调用 time.sleep(0)来完成)。
4.把线程设置回睡眠状态(切换出线程)。
5.解锁 GIL。
6.重复上述步骤。
例如,对于任意面向 I/O 的 Python 例程(调用了内置的操作系统 C 代码的那种),
GIL 会在 I/O 调用前被释放,以允许其他线程在 I/O 执行的时候运行。而对于那些没有太多 I/O 操作的代码而言,更倾向于在该线程整个时间片内始终占有处理器(和 GIL)。换句话说就是, I/O 密集型的 Python 程序要比计算密集型的代码能够更好地利用多线程环境。

Python的threaing模块
Python 提供了多个模块来支持多线程编程,包括 thread、 threading 和 Queue 模块等。程序是可以使用 thread 和 threading 模块来创建与管理线程。 thread 模块提供了基本的线程和锁定支持;而 threading 模块提供了更高级别、功能更全面的线程管理。使用 Queue 模块,用户可以创建一个队列数据结构,用于在多线程之间进行共享。
避免使用 thread 模块:最明显的一个原因是在主线程退出之后,所有其他线程都会在没有清理的情况下直接退出(不支持守护线程)。低级别的 thread 模块拥有的同步原语很少(实际上只有一个),而另一个模块 threading 会确保在所有“重要的”子线程退出前,保持整个进程的存活。 只建议那些想访问线程的更底层级别的专家使用 thread 模块。为了强调这一点,在 Python3 中该模块被重命名为_thread。
在这里插入图片描述
如果主线程准备退出时,不需要等待某些子线程完成,就可以为这些子线程设置守护
线程标记。该标记值为真时,表示该线程是不重要的,或者说该线程只是用来等待客户端
请求而不做任何其他事情。
要将一个线程设置为守护线程,需要在启动线程之前执行如下赋值语句:thread.daemon = True(调用 thread.setDaemon(True)的旧方法已经弃用了)。同样,要检查线程的守护状态,也只需要检查这个值即可(对比过去调用 thread.isDaemon()的方法)。一个新的子线程会继承父线程的守护标记。
Threading有三种用法:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
使用 thread 模块时实现的锁没有了,取而代之的是一组 Thread 对象。当实例化每个 Thread 对象时,把函数(target)和参数(args)传进去,然后得到返回的 Thread 实例。实例化 Thread(调用 Thread())和调用 thread.start_new_thread()的最大区别是新线程不会立即开始执行。这是一个非常有用的同步功能,尤其是当你并不希望线程立即开始执行时。
当所有线程都分配完成之后,通过调用每个线程的 start()方法让它们开始执行,而不是在这之前就会执行。相比于管理一组锁(分配、获取、释放、检查锁状态等)而言,这里只需要为每个线程调用 join()方法即可。 join()方法将等待线程结束,或者在提供了超时时间的情况下,达到超时时间。使用 join()方法要比等待锁释放的无限循环更加清晰(这也是这种锁
又称为自旋锁的原因)。
在这里插入图片描述
在这里插入图片描述
主要是添加了 ThreadFunc 类,并在实例化 Thread 对象时做了一点小改动,同时实例化了可调用类 ThreadFunc。实际上,这里完成了两个实例化。让我们先仔细看看 ThreadFunc 类吧。我们希望这个类更加通用,而不是局限于 loop()函数,因此添加了一些新的东西,比如让这个类保存了函数的参数、函数自身以及函数名的字符串。而构造函数__init__()用于设定上述这些值。当创建新线程时, Thread 类的代码将调用 ThreadFunc 对象,此时会调用__call__()这个特殊方法。由于我们已经有了要用到的参数,这里就不需要再将其传递给 Thread()的构造函数了,直接调用即可。
在这里插入图片描述

同步原语
前面对同步进行过一些介绍,所以这里就使用其中两种类型的同步原语演示几个示例程
序:锁/互斥,以及信号量。锁是所有机制中最简单、最低级的机制,而信号量用于多线程竞争有限资源的情况。
锁有两种状态:锁定和未锁定。而且它也只支持两个函数:获得锁和释放锁。当多线程争夺锁时,允许第一个获得锁的线程进入临界区,并执行代码。所有之后到达的线程将被阻塞,直到第一个线程执行结束,退出临界区,并释放锁。此时,其他等待的线程可以获得锁并进入临界区。不过请记住,那些被阻塞的线程是没有顺序的(即不是先到先执行),胜出线程的选择是不确定的,而且还会根据 Python 实现的不同而有所区别。

使用上下文管理:
使用 Python 2.5 或更新版本,还有一种方案可以不再调用锁的 acquire()和 release()
方法,从而更进一步简化代码。这就是使用 with 语句,此时每个对象的上下文管理器负责在进入该套件之前调用 acquire()并在完成执行之后调用 release()。
在这里插入图片描述在这里插入图片描述

信号量
信号量是最古老的同步原语之一。它是一个计数器,当资源消耗时递减,当资源释放时递增。你可以认为信号量代表它们的资源可用或不可用。消耗资源使计数器递减的操作
习惯上称为 P() (来源于荷兰单词 probeer/proberen),也称为 wait、try、acquire、pend 或 procure。相对地,当一个线程对一个资源完成操作时,该资源需要返回资源池中。这个操作一般称为 V()(来源于荷兰单词 verhogen/verhoog),也称为 signal、 increment、 release、 post、 vacate。Python 简化了所有的命名,使用和锁的函数/方法一样的名字: acquire 和 release。信号量比锁更加灵活,因为可以有多个线程,每个线程拥有有限资源的一个实例 。

生产者-消费者问题和Queue/queue模块

多线程常用的第三方库
在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章