Python多线程
- 模块:Threading
- 概念:
- 线程:CPU执行程序的基本单位
- 父线程:与子线程而言是相对的。调用别的线程的程序(线程叫做父线程)
- 子线程:被别的程序所调用,则是调用者的子线程
- 守护进程:也称之为后台进程,即主进程(前台进程)结束,守护进程也结束
- 主线程:最外层的线程(最初运行的程序)
- 注意:使用Pycharm时,运行多线程程序需要使用Terminal或者Run控制台,而不是Python Console否则得到的将不是原本的多线程结果。(比如守护线程会失效)。参考博客
一下用简单的几个例子来讲解Python多线程的用法。
第一个例子:展示多线程与普通程序的区别
"""
最简单的多线程demo
展示多线程程序与普通程序的区别
"""
import time
from threading import Thread
def demo1(doing):
print(f"I want to {doing}, {time.ctime()}")
time.sleep(2)
print(f"\n{doing} 结束")
def main():
print(f"开始, {time.ctime()}")
t1 = Thread(target=demo1, args=("听歌",))
t2 = Thread(target=demo1, args=("吃饭",))
t1.start()
t2.start()
print(f"结束, {time.ctime()}")
if __name__ == '__main__':
main()
"""
开始, Thu May 6 20:22:43 2021
I want to 听歌, Thu May 6 20:22:43 2021
I want to 吃饭, Thu May 6 20:22:43 2021
结束, Thu May 6 20:22:43 2021
听歌 结束, Thu May 6 20:22:45 2021
吃饭 结束, Thu May 6 20:22:45 2021
"""
输出格式经过了略微的调整,原始输出肯能比较乱,原因之后会讲。
由上可以看出,原本需要4秒才能执行完的程序,上面只用了2秒。即实现了并行,同时"听歌"和"吃饭"。
Python实现多线程使用的是threading库,使用Thread类创建线程对象,Thread对象有如下比较常用的方法:
-
Thread
(target=None, name=None, args=(), daemon=None)
以上为比较常用的参数,不是所有参数,详细参数可参考文末的链接官网教程
target 是指明该线程运行的函数名
name 是我们给该线程取的名称
args 是调用该函数需要传递的参数元组,末尾加逗号
daemon 设置该线程是否为守护线程 -
start()
表示启动线程,让其开始工作。该方法每个对象只能够调用一次,否则抛出RuntimeError
,它安排对象的 run()方法在一个独立的控制进程中调用。 -
run()
运行target
传递过去的函数 -
join
(timeout=None)
这个会阻塞调用该线程的线程,如果子线程调用该方法,主线程就算将自己的程序运行完了也不会结束,而是 会等到这个子线程运行完再结束。 -
setName() 和 getName()
分别是设置名字和获取名字 -
isDaemon() 和 setDaemon()
分别是判断是否是守护进程和设置守护进程
第二个例子:使用join()方法和将线程模块化
import threading
import time
class MyThreading(threading.Thread):
def __init__(self, name):
super(MyThreading, self).__init__()
self.name = name
def run(self):
print(f"开始线程: {self.name}, {time.ctime()}")
time.sleep(2)
print(f"退出线程: {self.name}, {time.ctime()}")
class Test:
def __init__(self):
pass
def main_threading(self):
print(f"主线程开始运行, {time.ctime()}")
Thread1 = MyThreading("Thread1")
Thread2 = MyThreading("Thread2")
Thread1.start()
Thread2.start()
Thread1.join()
Thread2.join()
print(f"主线程结束运行, {time.ctime()}")
if __name__ == '__main__':
test = Test()
test.main_threading()
"""
主线程开始运行, Thu May 6 20:46:30 2021
开始线程: Thread1, Thu May 6 20:46:30 2021
开始线程: Thread2, Thu May 6 20:46:30 2021
退出线程: Thread2, Thu May 6 20:46:32 2021
退出线程: Thread1, Thu May 6 20:46:32 2021
主线程结束运行, Thu May 6 20:46:32 2021
"""
由第一个例子可以知道主线程本应该在子线程结束前结束,但是用了子线程调用join()后会阻塞父线程,致使在子线程结束完后才可结束。
第三个例子:守护线程的使用
以上例子中的子线程结束时间都是根据自身程序的运行时间觉得,而与父线程的是否结束无关,但我们在日常使用时,经常会有父线程结束,子线程就必须结束的一些线程,这种线程我们称之为守护线程,或者后台进程,如我们的垃圾回收功能就是。
我们修改第二个例子中的Test()类
class Test:
def __init__(self):
pass
def main_threading(self):
print(f"主线程开始运行, {time.ctime()}")
Thread1 = MyThreading("Thread1")
Thread2 = MyThreading("Thread2")
Thread1.setDaemon(True)
Thread2.setDaemon(True)
Thread1.start()
Thread2.start()
print(f"主线程结束运行, {time.ctime()}")
"""
主线程开始运行, Thu May 6 20:48:56 2021
开始线程: Thread1, Thu May 6 20:48:56 2021
开始线程: Thread2, Thu May 6 20:48:56 2021
主线程结束运行, Thu May 6 20:48:56 2021
"""
由运行结果可以看出,父线程结束,子线程就无条件中止了。
第四个例子:
现在的CPU都是多核的,但是python的多线程是无法使用多核,而实现并行的方式是线程切换,只要速度足够快,在某一段时间内,我们就会认为是并行的。这就是所谓的并发,而某一时刻多个线程同时工作则称之为并行。我们可以用程序输出来演示线程切换过程。
import time
from threading import Thread, current_thread
def run(n):
for i in range(n):
print(current_thread().name + " " + str(i))
thread = Thread(target=run, args=(20,), name="ChildThread")
# 如果设置子线程为守护进程,可以查看守护进程的结束与主线程的结束时间
# thread.setDaemon(True)
thread.start()
for i in range(10):
print(current_thread().name + " " + str(i))
"""
ChildThread 0
ChildThread 1
ChildThread 2
ChildThread 3
ChildThread 4
MainThread 0
MainThread 1
MainThread 2
MainThread 3
MainThread 4
MainThread 5
MainThread 6
MainThread 7
ChildThread 5
ChildThread 6
ChildThread 7
ChildThread 8
ChildThread 9
ChildThread 10
ChildThread 11
ChildThread 12
ChildThread 13
MainThread 8
MainThread 9
ChildThread 14
ChildThread 15
ChildThread 16
ChildThread 17
ChildThread 18
ChildThread 19
Process finished with exit code 0
"""
从运行结果我们就可以看出线程的切换。
第五个例子:
由于线程是切换的,那么所以某一个线程在执行时可能被打断。加上python多线程使用的是同一块内存,那么资源是共享的,所以当多个线程再某一段时间内,同时访问某一资源,对其进行修改的话可能会导致混乱。
from threading import Thread
var = 0
def change(n):
global var
var += n
var -= n
def run_thread(n):
for i in range(1000000):
change(n)
thread1 = Thread(target=run_thread, args=(5,), name="thread1")
thread2 = Thread(target=run_thread, args=(8,), name="thread2")
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(var)
"""
11
"""
# 结果不一定
"""
理论上应该是0,但是当两个线程交替执行,当循环次数足够多时,结果就不一定是0
为了解决这个问题,采用了锁的办法
"""
第六个例子:就是解决上述问题的办法,使用锁
from threading import Lock, Thread
var = 0
lock = Lock()
def change(n):
global var
var += n
var -= n
def run_thread(n):
for i in range(20000000):
lock.acquire()
try:
change(n)
finally:
lock.release()
thread1 = Thread(target=run_thread, args=(5,), name="thread1")
thread2 = Thread(target=run_thread, args=(8,), name="thread2")
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(var)
"""
这样得到的结果就一定是0
"""
可以这么理解这个锁:人相当于线程,锁相当于门锁,一个人进厕所会锁上门让别人不能进来打扰,只有当他解锁出来后别的人才可以进去。
Threading.Lock
类的主要方法如下:
acquire
(blocking=True, timeout=-1)- 这个方法用于获取锁。获取的方式可以是阻塞方式,也可以是非阻塞方式。
- 差别就是,如果别的线程使用了锁,而当前线程想获取锁时,可以干等着一直等到使用了锁的线程释放了锁(解锁),再获取锁并锁定。这样称之为阻塞方式。
- 而在非阻塞的情况下(也就是blocking设置为False时),调用acquire方法,若是锁可获得,则返回True,并且上锁,若是锁被别的线程占用,则返回False。
timeout
就是设置阻塞等待的时间,若是在timeout
设置的时间内没有获得锁则返回False,否则返回True并获取锁,再上锁。若是timeout=-1
,那么将会无限等待。
release
()- 这个方法就是释放锁
- 若是在未锁定时调用,会返回
RuntimeError
Python线程使用场景
由于Python的多线程受到了GIL的原因,Python的多线程无法实现真正的并行,所以多线程程序是通过频繁的线程切换来实现的。
但是线程切换会带来不小的开销,频繁的线程切换会造成资源的浪费。所以多线程在Python里似乎意味着并没有带来性能的提升,甚至降低了程序的性能。
但是有没有一种操作是正好需要频繁切换线程才能提高性能的呢?
答案是有的,那就是I/O操作,因为I/O操作不需要一直依靠CPU,只需要CPU向磁盘发起通知,然后就可以去执行其他的命令,等到数据准备好了再读取数据就好了,就不需要CPU一直等待I/O操作直至完成。
所以Python多线程的使用场景就很明确了:
- 不适用于CPU计算密集型,而是适用于I/O密集型
以上就是我对python多线程的一些领悟,如有错误请积极指出,谢谢。