使用Python中的线程模块,能够同时运行程序的不同部分,并简化设计。如果你已经入门Python,并且想用线程来提升程序运行速度的话,希望这篇教程会对你有所帮助。
通过阅读本文,你将了解到:什么是线程和进程?
Python的threading模块?
python如何创建、执行线程?
资源共享问题?
全局解释器锁(GIL)?
Lock & RLock?
线程与进程
什么是进程
进程是系统进行资源分配和调度的一个独立单位 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。
什么是线程
CPU调度和分派的基本单位 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。
进程与线程的关系图
线程与进程的区别: 地址空间和其他资源:进程间相互独立,同一进程的各线程间共享。某线程内的想爱你城咋其他进程不可见。 通信:进程间通信IPC,线程间可以直接读写进程数据段来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。 调度和切换:线程上下文切换比进程上下文切换要快得多。 在多线程操作系统中,进程不是一个可执行的实体。
Python的threading模块
Python 供了几个用于多线程编程的模块,包括 thread, threading 和 Queue 等。thread 和 threading 模块允许程序员创建和管理线程。thread 模块 供了基本的线程和锁的支持,而 threading 供了更高级别,功能更强的线程管理的功能。Queue 模块允许用户创建一个可以用于多个线程之间 共享数据的队列数据结构。
但是我们一般避免使用thread模块
threading模块的对象
| 对象 | 描述 | | :---- | :---- | | Thread | 线程对象 | | Lock | 互斥锁 | | Condition | 条件变量 | | Event | 事件,该事件发生后所有等待该事件的线程将激活 | | Semaphore | 信号量(计数器) | | Timer | 定时器,运行前会等待一段时间 | | Barrier | 创建一个障碍,必须达到指定数量线程才开始运行 |
threading模块的Thread类
| 名称 | 描述 | | :---- | :---- | | name | 线程名(属性) | | ident | 线程标识符(属性) | | daemon | 线程是否是守护线程(属性) | | isAlive() | 返回线程是否在运行。正在运行指启动后、终止前| | start() | 开启线程 | | run() | 定义线程功能的方法(通常在子类中被应用开发者重写) | | join() | 阻塞当前上下文环境的线程,直到调用此方法的线程终止或到达指定的timeout(可选参数) |
python创建和执行线程
创建线程代码创建方法一:
import os
import time
from threading import Thread,current_thread
def task1():
for i in range(5):
print('{}洗衣服:'.format(current_thread().name), i, os.getpid(), os.getppid())
time.sleep(0.5)
def task2(n):
for i in range(n):
print('{}劳动最光荣,扫地中...'.format(current_thread().name), i, os.getpid(), os.getppid())
time.sleep(0.5)
if __name__ == '__main__':
print('main:', os.getpid())
# 创建线程对象
t1 = Thread(target=task1,name='警察')
t2 = Thread(target=task2,name='小偷', args=(6,))
# 启动线程
t1.start()
t2.start()创建方法二:
import time
from threading import Thread
# 自定义线程类
class MyThread(Thread):
def __init__(self, name):
Thread.__init__(self)
self.name = name
def run(self):
for i in range(5):
print('{}正在打印:{}'.format(self.name, i))
time.sleep(0.1)
if __name__ == '__main__':
# 创建三个线程,给线程起名字
t1 = MyThread('小明')
t2 = MyThread('小花')
t3 = MyThread('ergou')
# 启动线程
t1.start()
t2.start()
t3.start()
守护线程
无论是进程还是线程,都遵循:守护xx会等待主xx运行完毕后被销毁。需要强调的是:运行完毕并非终止运行1.对主进程来说,运行完毕指的是主进程代码运行完毕 2.对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕
守护线程代码
from threading import Thread
import time
def sayhi(name):
time.sleep(1)
print('{0}say hello'.format(name))
if __name__ == '__main__':
t=Thread(target=sayhi,args=('ergou',))
t.setDaemon(True) #必须在t.start()之前设置
t.start()
print('主线程')
print(t.is_alive())
'''主线程True'''
资源共享问题
进程和线程都是实现多任务的一种方式,例如:在同一台计算机上能同时运行多个QQ(进程),一个QQ可以打开多个聊天窗口(线程)。 资源共享:进程不能共享资源,而线程共享所在进程的地址空间和其他资源,同时,线程有自己的栈和栈指针。 所以在一个进程内的所有线程共享全局变量,但多线程对全局变量的更改会导致变量值得混乱。
代码演示:
from threading import Thread
import time
g_num=1000
def work1():
global g_num
g_num+=3
print("work1----num:",g_num)
def work2():
global g_num
print("work2---num:",g_num)
if __name__ == '__main__':
print("start---num:",g_num)
t1=Thread(target=work1)
t1.start()
#故意停顿一秒,以保证线程1执行完成
time.sleep(1)
t2=Thread(target=work2)
t2.start()
得到的结果是:start---num: 1000 work1----num: 1003 work2---num: 1003
全局解释器锁(GIL)
首先需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。就好比C++是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码。同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行(其中的JPython就没有GIL)。
那么CPython实现中的GIL又是什么呢?GIL全称Global Interpreter Lock为了避免误导,我们还是来看一下官方给出的解释:In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)
主要意思为:GIL是一个互斥锁,它防止多个线程同时执行Python字节码。这个锁是必要的,主要是因为CPython的内存管理不是线程安全的 尽管Python完全支持多线程编程, 但是解释器的C语言实现部分在完全并行执行时并不是线程安全的。
因此,解释器实际上被一个全局解释器锁保护着,它确保任何时候都只有一个Python线程执行。 在多线程环境中,Python 虚拟机按以下方式执行:设置GIL
切换到一个线程去执行
运行指定数量的字节码指令
线程主动让出控制(可以调用time.sleep(0))把线程设置完睡眠状态
解锁GIL
再次重复以上步骤对所有面向 I/O 的(会调用内建的操作系统 C 代码的)程序来说,GIL 会在这个 I/O 调用之 前被释放,以允许其它的线程在这个线程等待 I/O 的时候运行。如果某线程并未使用很多 I/O 操作, 它会在自己的时间片内一直占用处理器(和 GIL)。也就是说,I/O 密集型的 Python 程序比计算密集型的程序更能充分利用多线程环境的好处。
GIL对多线程Python程序的影响
程序的性能受到计算密集型(CPU)的程序限制和I/O密集型的程序限制影响,那什么是计算密集型和I/O密集型程序呢?计算密集型(CPU)
高度使用CPU的程序,例如: 进行数学计算,矩阵运算,搜索,图像处理等.
I/O密集型
I/0(Input/Output)程序是进行数据传输,例如: 文件操作,数据库,网络数据等
Python解释器有哪些CPython: 官方默认版本,使用C语言开发,是Python使用最广泛的解释器,有GIL.
IPython: IPython是基于CPython之上的交互式解释器,其它方面和CPython相同.
PyPy: PyPy采用JIT(Just In Time)也就是即时编译编译器,对Python代码执行动态编译,目的是加快执行速度,有GIL.
Jython: 运行在Java平台上的解释器,把Python代码编译为Java字节码执行,没有GIL.
IronPython: IronPython和Jython类似,只不过IronPython是运行在微软.Net平台上的Python解释器,可以直接把Python代码编译成.Net的字节码,没有GIL.
Lock&RLock
常用的资源共享锁机制:Lock
RLock
Semphore
Condition
本案例只是针对Lock和RLock进行讲解,线程二中介绍后面的两个
LockLock 不能连续acquire锁,不然会死锁,Lock 资源竞争可能会导致死锁。
Lock 会降低性能。
from threading import Thread, Lock
lock = Lock()
total = 0
# 两个线程共用一把锁,其中通过acquire申请获取锁对象,通过release释放锁资源
# 进行加法
def add():
global total
global lock
for i in range(1000000):
lock.acquire()
total += 1
lock.release()
# 进行减法
def sub():
global total
global lock
for i in range(1000000):
lock.acquire()
total -= 1
lock.release()
# 创建线程对象
thread1 = Thread(target=add)
thread2 = Thread(target=sub)
# 将Thread1和2设置为守护线程,主线程完成时,子线程也一起结束
# thread1.setDaemon(True)
# thread1.setDaemon(True)
# 启动线程
thread1.start()
thread2.start()
# 阻塞,等待线程1和2完成,如果不使用join,那么主线程完成后,子线程也会自动关闭。
thread1.join()
thread2.join()
特点就是执行速度慢,但是保证了数据的安全性
RLockRLock 可以连续acquire锁,但是需要相应数量的release释放锁
因可以连续获取锁,所以实现了函数内部调用带锁的函数
from threading import Thread, Lock, RLock
lock = RLock()
total = 0
def add():
global lock
global total
# RLock实现连续获取锁,但是需要相应数量的release来释放资源
for i in range(1000000):
# 可以连续获取锁
lock.acquire()
lock.acquire()
total += 1
# 要有对象的release
lock.release()
lock.release()
def sub():
global lock
global total
for i in range(1000000):
lock.acquire()
total -= 1
lock.release()
thread1 = Thread(target=add)
thread2 = Thread(target=sub)
thread1.start()
thread2.start()
# 阻塞,等待线程1和2完成,如果不使用join,那么主线程完成后,子线程也会自动关闭。
thread1.join()
thread2.join()
多线程的内容很多,请大家继续关注:python多线程之进阶篇