目录
在学习python多线程过程,学习一些很不错的文章和教程,这里进行摘抄并总结归纳一下,便于加深自己的理解
1.GIL
GIL的全称是全局解释器锁(Global Interpreter Lock),GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。就好比C++,可以用不同的编译器来编译成可执行代码。有名的编译器例如GCC,INTEL C++,Visual C++等。Python也一样,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。像其中的JPython就没有GIL。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷。所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL。
1.1 为什么要有GIL
相比于使用更细粒度的锁,GIL具有以下优点:
- 在单线程任务中更快;
- 在多线程任务中,对于I/O密集型程序运行更快;
- 在多线程任务中,对于用C语言包来实现CPU密集型任务的程序运行更快;
- 在写C扩展的时候更加容易,因为除非你在扩展中允许,否则Python解释器不会切换线程;
- 在打包C库时更加容易。我们不用担心线程安全性。,因为如果该库不是线程安全的,则只需在调用GIL时将其锁定即可。
1.2 GIL的运作方式
- 某个线程拿到GIL
- 该线程执行代码,直到达到了check_interval*
- 解释器让当前线程释放GIL
- 所有的线程开始竞争GIL
- 竞争到GIL锁的线程又从第1步开始执行
1.3 GIL带来的问题
因为有GIL的存在,由CPython做解释器(虚拟机)的多线程Python程序只能利用多核处理器的一个核来运行。
例如,我们将一个8线程的JAVA程序运行在4核的处理器上,那么每个核会运行1个线程,然后利用时间片轮转,轮流运行每一个线程。但是,我们将一个8线程的Python程序(由CPython作解释器)运行在一个4核处理器上,那么总共只会有1个核在工作,8个线程都要在这一个核上面时间片轮转。
所以说,用Python做多线程的最佳场景是处理I/O密集型应用,例如网络爬虫,因为I/O的速度比CPU的速度慢非常多,这样不同线程的上下文切换(context switch)的时间就可以忽略不计了。如果做CPU密集型任务,则使用多进程可以更好地利用CPU的多核性能。另外,使用gevent协程可以避免多线程的上下文切换的问题
在python中使用都是操作系统级别的线程,linux中使用的pthread,window使用的是其原生线程
2.多线线程
多线程的好处自不必赘述,通过一个简单的例子来看一下,参考关于播放音乐和视频
先看看单线程的
#coding=utf-8
import threading
from time import ctime,sleep
def music(func):
for i in range(2):
print("I was listening to %s. %s" %(func,ctime()))
sleep(1)
def move(func):
for i in range(2):
print("I was at the %s! %s" %(func,ctime()))
sleep(5)
if __name__ == '__main__':
print("all start %s" %ctime())
music(u'Music')
move(u'Vedio')
print("all over %s" %ctime())
输出如下:
但实际上诗就像MV一样,这两个是一般是同时开展的
#coding=utf-8
import threading
from time import ctime,sleep
def music(func):
for i in range(2):
print("I was listening to %s. %s" %(func,ctime()))
sleep(1)
def move(func):
for i in range(2):
print("I was at the %s! %s" %(func,ctime()))
sleep(5)
threads = []
t1 = threading.Thread(target=music,args=(u'Music',))
threads.append(t1)
t2 = threading.Thread(target=move,args=(u'Vedio',))
threads.append(t2)
if __name__ == '__main__':
print("all start %s" % ctime())
for t in threads:
t.setDaemon(True)
t.start()
t.join()
print("all over %s" %ctime())
1.join()方法,用于等待线程终止。join()的作用是,在子线程完成运行之前,这个子线程的父线程将一直被阻塞等待子线程结束
2.程序中一般有主线程和子线程,两种线程都执行完毕,认为是程序执行结束。setDaemon(True)将线程声明为守护线程,此类线程的特点是,当程序中主线程及所有非守护线程执行结束时,未执行完毕的守护线程也会随之消亡(进行死亡状态),程序将结束运行。Python 解释器的垃圾回收机制就是守护线程的典型代表,当程序中所有主线程及非守护线程执行完毕后,垃圾回收机制也就没有再继续执行的必要了。需要注意的一点是,线程对象调用 daemon 属性必须在调用 start() 方法之前,否则 Python 解释器将报 RuntimeError 错误
输入结果如下:
2.1 线程的调度和启动
python 中一个线程对应c语言中一个线程。GIL使得同一个时刻只有一个线程运行在cpu上执行字节码,无法将多个线程映射到多个cpu上执行,GIL会根据执行的字节码行数以及时间释放GIL,在遇到IO操作时会主动释放。python提供了_thread 和threading 两个模块来支持多线程,其中_thread提供低级别的,原始的线程支持,以及一个简单的锁,但是一般不建议使用_thread模块;而threading模块则提供了功能丰富的多线程支持。
通过一个对一个全局变量进行增减操作,来解释上述蓝色字体描述
total = 0
def add(name):
global total
for i in range(10000):
total += 1
print('当前是%s线程,i的值为%s' % (name,total))
def desc(name):
global total
for i in range(10000):
total -= 1
print('当前是%s线程,i的值为%s' % (name,total))
import threading
thread1 = threading.Thread(target=add,args=('thread1',))
thread2 = threading.Thread(target=desc,args=('thread2',))
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(total)
输入结果如下所示:
从输出结果来看,并不是等待其中一个thread跑完了再去跑另一个线程任务
3.线程构造与使用
python 主要提供两种方式来创建线程:
- 使用threading模块的Thread类的构造器创建线程
- 继承 threading模块的Thread类创建线程类
3.1调用Thread类构造器创建线程
调用Thread类的构造器创建线程,直接调用threading.Thread类的如下构造器创建线程
__init__(self, group= None, target=None, name= None, args=(), kwargs= None, *, daemon= None)
target :指定该线程要调度的目标方法。只传函数名,不传函数,即不加()
args :指定一个元组,以位置参数的形式为target指定的函数传入参数。元组的第一个参数传给target的第一个,以此类推。
kwargs:指定一个字典,以关键字参数的形式为target指定的函数传入参数
daemon: 指定所构建的线程是否为后台线程。
调用Thread类的构造器创建并启动多线程的步骤:
-
调用Thread类的构造器创建线程对象。在创建线程对象时。target参数指定的函数将作为线程的执行实体
-
调用线程对象的start()方法启动线程。
import threading
#定义一个普通action方法,该方法准备作为线程的执行实体
def action(max):
for i in range(max):
#调用threading模块的current_thread()函数获取当前线程
#调用线程对象的getName()方法获取单前线程的名字
print(threading.current_thread().getName() + " " +str(i))
#下面是主程序
for i in range(100):
#调用threading模块的current_thread()函数获取当前线程
print(threading.current_thread().getName() + " " +str(i))
if i == 20:
t1 = threading.Thread(target=action, args=(100,))
t1.start()
t2 = threading.Thread(target =action, args=(100,))
t2.start()
print("主线程执行完成!")
执行结果如下:
可以看到在循环变量达到20时,创建并且启动了两个新线程。所以此时一共有三个线程,但是三个线程之间的执行没有没有先后顺序,他们的执行方式为Thread-1执行一段时间Thread-2或MainThread获得CPU执行一段时间。
- threading.current_thread():它是threading模块的函数,该函数总是返回当前正在执行的线程对象
- getName() 它是Thread类的实例方法,该方法返回调用它的线程名字
-setName() 方法可以为线程设置名字,getName和setName 两个方法可通过name属性来代替。
3.2继承Thread类创建线程类
-
定义Thread类的子类,并重写该类的run()方法。run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
-
创建Thread子类的实例,即创建线程对象
-
调用线程对象的start()方法来启动线程
import threading
import time
class GetDetailHtml(threading.Thread):
def __init__(self, name):
super().__init__(name=name)
def run(self):
print("get detail html started")
time.sleep(2)
print("get detail html end")
class GetDetailUrl(threading.Thread):
def __init__(self, name):
super().__init__(name=name)
def run(self):
print("get detail url started")
time.sleep(2)
print("get detail url end")
if __name__ == "__main__":
thread1 = GetDetailHtml("get_detail_html")
thread2 = GetDetailUrl("get_detail_url")
start_time = time.time()
thread1.start()
thread2.start()
"""join进行线程阻塞,只有两个线程都执行完成后才执行后边的"""
thread1.join()
thread2.join()
print("last time :{}".format(time.time() - start_time))
输出结果如下:
参考 python 多线程
参考 python并发编程