为什么要使用多线程?
使用多线程,可以同时进行多项任务,可以使用户界面更友好,还可以后台执行某些用时长的任务,同时具有易于通信的优点。(对于GIL以及Python多线程对于效率的影响讨论可看知乎为什么有人说 Python 的多线程是鸡肋呢? - 知乎 (zhihu.com))
python3中多线程的实现使用了threading模块,它允许同一进程中运行多个线程。
如何创建和执行一个线程
一般我们有两种方法来创建线程,一种是以某个函数来作为起点,另一种是继承Thread类。
方法一
获取一个Thread对象,构造参数中target是起点函数,注意不要加括号
。假如起点函数有参数,则可以通过args输入元组参数或者kwargs输入字典参数。
#! -*-conding=: UTF-8 -*-
# 2023/5/6 15:53
import time
from threading import Thread
def task():
print("另外开始一个子线程做任务啦")
time.sleep(1) # 用time.sleep模拟任务耗时
print("子线程任务结束啦")
if __name__ == '__main__':
print("这里是主线程")
# 创建线程对象
t1 = Thread(target=task)
# 启动
t1.start()
time.sleep(0.3)
print("主线程依然可以干别的事")
输出结果为:
这里是主线程
另外开始一个子线程做任务啦
主线程依然可以干别的事
子线程任务结束啦
方法二
#! -*-conding=: UTF-8 -*-
# 2023/5/6 15:53
import time
from threading import Thread
class NewThread(Thread):
def __init__(self):
Thread.__init__(self) # 必须步骤
def run(self): # 入口是名字为run的方法
print("开始新的线程做一个任务啦")
time.sleep(1) # 用time.sleep模拟任务耗时
print("这个新线程中的任务结束啦")
if __name__ == '__main__':
print("这里是主线程")
# 创建线程对象
t1 = NewThread()
# 启动
t1.start()
time.sleep(0.3) # 这里如果主线程结束,子线程会立刻退出,暂时先用sleep规避
print("主线程依然可以干别的事")
输出结果为:
这里是主线程
开始新的线程做一个任务啦
主线程依然可以干别的事
这个新线程中的任务结束啦
正式介绍threading模块
关于线程信息的函数:
threading.active_count()
:返回当前存活的Thread对象数量。threading.current_thread()
:返回当前线程的Thread对象。threading.enumerate()
:列表形式返回所有存活的Thread对象。threading.main_thread()
:返回主Thread对象。
Thread对象的方法及属性:
Thread.name
:线程的名字,没有语义,可以相同名称。Thread.ident
:线程标识符,非零整数。Thread.Daemon
:是否为守护线程。Thread.is_alive()
:是否存活。Thread.start()
:开始线程活动。若多次调用抛出RuntimeError。Thread.run()
:用来重载的,Thread.join(timeout=None)
:等待直到线程正常或异常结束。尚未开始抛出RuntimeErrorThread(group=None, target=None, name=None, args=(), kwargs={}, *, deamon=None)
:构造函数。
守护线程 Daemon
在Python 3中,守护线程(daemon thread)是一种特殊的线程,它在程序运行时在后台运行,不会阻止程序的退出。当主线程退出时,守护线程也不会自动退出,而不需要等待它执行完毕。
方法一
在创建线程对象时,可以通过设置daemon
属性为True
来创建守护线程,例如:
import threading
import time
def worker():
while True:
print('Worker thread running')
time.sleep(1)
# 创建守护线程
t = threading.Thread(target=worker, daemon=True)
# 启动线程
t.start()
# 主线程执行一些操作
print('Main thread running')
time.sleep(5)
print('Main thread finished')
输出结果为:
Worker thread runningMain thread running
Worker thread running
Worker thread running
Worker thread running
Worker thread running
Main thread finished
在这个示例中,我们创建了一个守护线程worker()
,并将daemon
属性设置为True
。在主线程中,我们执行了一些操作,并休眠5秒钟。由于守护线程的存在,即使主线程已经结束,守护线程仍会在后台运行。
方法二
设置守护线程用Thread.setDaemon(bool)
#! -*-conding=: UTF-8 -*-
# 2023/5/6 16:06
import time
from threading import Thread
def task1():
print("开始子线程1做任务1啦")
time.sleep(1) # 用time.sleep模拟任务耗时
print("子线程1中的任务1结束啦")
def task2():
print("开始子线程2做任务2啦")
for i in range(5):
print("任务2-{}".format(i))
time.sleep(1)
print("子线程2中的任务2结束啦")
if __name__ == '__main__':
print("这里是主线程")
# 创建线程对象
t1 = Thread(target=task1)
t2 = Thread(target=task2)
t2.setDaemon(True) # 设置为守护进程,必须在start之前
# 启动
t1.start()
t2.start()
time.sleep(1)
print("主线程结束了")
输出结果为:
这里是主线程
开始子线程1做任务1啦
开始子线程2做任务2啦
任务2-0
主线程结束了
子线程1中的任务1结束啦任务2-1
守护线程的作用在于,当我们需要在程序运行时执行一些后台任务,但是不想让这些任务阻止程序的正常退出时,可以使用守护线程。
例如,在一个Web应用程序中,我们可能需要启动一个守护线程来定期清理缓存或者执行一些后台任务。
需要注意的是,守护线程无法完全控制其执行过程,因此不能用于一些必须在程序退出之前完成的任务。同时,守护线程不能访问一些主线程资源,例如共享内存或者打开的文件,因为这些资源可能会在主线程结束时被释放。
让主线程等待子线程结束 join
假如要让主线程等子线程结束,那么可以使用Thread.join()方法。
当调用线程对象的join()
方法时,主线程将被阻塞,直到该线程执行完成或者超时。
以下是一个简单的示例:
import threading
import time
def worker():
print('Worker thread started')
time.sleep(2)
print('Worker thread finished')
# 创建线程对象
t = threading.Thread(target=worker)
# 启动线程
t.start()
# 等待线程结束
t.join()
# 主线程继续执行
print('Main thread finished')
输出结果为:
Worker thread started
Worker thread finished
Main thread finished
在这个示例中,我们创建了一个子线程worker()
,并使用start()
方法启动线程。在主线程中,我们调用了线程对象的join()
方法,让主线程等待子线程执行完毕。在子线程执行完毕后,主线程继续执行。
需要注意的是,join()
方法还可以设置超时时间,以避免无限期等待线程的执行。例如:
import threading
import time
def worker():
print('Worker thread started')
time.sleep(2)
print('Worker thread finished')
# 创建线程对象
t = threading.Thread(target=worker)
# 启动线程
t.start()
# 等待线程结束,最多等待3秒钟
t.join(3)
# 主线程继续执行
print('Main thread finished')
输出结果为:
Worker thread started
Worker thread finished
Main thread finished
在这个示例中,我们设置了join()
方法的超时时间为3秒钟,即使子线程没有执行完成,主线程也会在3秒钟后继续执行。
线程共享资源可能引起什么问题?
在线程编程中,多个线程可能同时访问和修改同一个共享资源,例如全局变量、共享内存、文件等。如果没有进行适当的同步操作,就可能会引发以下问题:
竞态条件(Race Condition):当多个线程同时访问和修改同一个共享资源时,就可能会发生竞态条件。这种情况下,由于线程执行顺序的不确定性,可能会导致资源被错误地读取或写入,从而引发程序的错误或崩溃。
死锁(Deadlock):当多个线程都在等待另一个线程释放某个资源时,就可能会发生死锁。这种情况下,程序会永久地阻塞在这个状态下,无法继续执行。
活锁(Livelock):多个线程相互协作,但是由于某些原因无法前进,导致它们不断重试,最终导致系统陷入死循环。活锁是一种比死锁更难以诊断和解决的问题。
为了避免以上问题,我们可以使用线程同步机制来保护共享资源的访问。
例如,可以使用锁(Lock)
、信号量(Semaphore)
、条件变量(Condition)
等机制来限制同时访问共享资源的线程数量,从而避免竞态条件。同时,也可以使用一些算法和策略来避免死锁和活锁等问题的发生。
下面是一些具体的例子,说明在多线程程序中共享资源可能引发的问题:
竞态条件
from threading import Thread
num = 0
def sum_one(