目录
- 十一、Python中线程
- 11.1 线程的定义
- 11.2 多线程
- 11.3 线程池
十一、Python中线程
11.1 线程的定义
线程(Thread):一个进程还可以拥有多个并发的执行线索,简单的说就是拥有多个可以获得CPU调度的执行单元,这就是所谓的线程。由于线程在同一个进程下,它们可以共享相同的上下文,因此相对于进程而言,线程间的信息共享和通信更加容易。
11.2 多线程
在Python早期的版本中就引入了thread模块(现在名为_thread)来实现多线程编程,然而该模块过于底层,而且很多功能都没有提供,因此目前的多线程开发我们推荐使用threading模块,该模块对多线程编程提供了更好的面向对象的封装。
函数原型:threading.Thread(target=function, name=“Thread”, args=参数)
注意: 设定执行的参数,设定格式一定要Tuple(变量,)
进程Thread
类的常用方法如下 -
方法 | 说明 |
run | 子线程执行的目标方法 |
start | 创建子线程的实例对象,并执行run方法 |
join | 主线程阻塞等待子线程结束,可以设置截至时间 time |
isAlive | 判断子线程是否存在 |
getName | 获取线程名 |
setName | 设置线程名 |
setDaemon | 子线程是否随主线程退出而终止 |
我们依旧通过下载文件的例子用多线程的方式来实现一遍。
from threading import Thread
from os import getpid
from time import time, sleep
def download_task(filename, downtime):
"""
资源下载
@param filename 资源名
@param downtime 下载耗时
"""
print('启动下载进程,进程号[%d].' % getpid())
print('开始下载%s...' % filename)
sleep(downtime)
print('%s下载完成! 耗费了%d秒' % (filename, downtime))
def main():
start = time()
t1 = Thread(target=download_task, args=('Python从入门到精通.pdf', 5, ))
t1.start()
t2 = Thread(target=download_task, args=('Python的编程核心.pdf', 8, ))
t2.start()
t1.join()
t2.join()
end = time()
print('总共耗费了%.2f秒.' % (end - start))
if __name__ == '__main__':
main()
上面代码的执行结果 -
启动下载进程,进程号[3848].
开始下载Python从入门到精通.pdf...
启动下载进程,进程号[3848].
开始下载Python的编程核心.pdf...
Python从入门到精通.pdf下载完成! 耗费了5秒
Python的编程核心.pdf下载完成! 耗费了8秒
总共耗费了8.01秒.
**说明:**这里可以看到同一个进程,线程下载速度优先于进程。
前面我们学习了继承,这列我们通过自定义类来继承
Thread
类,然后再创建线程对象并启动线程。代码如下所示。
from threading import Thread
from time import time, sleep
class DownloadTask(Thread):
def __init__(self, filename, downtime):
super().__init__()
self._filename = filename
self._downtime = downtime
def run(self):
print('开始下载%s...' % self._filename)
sleep(self._downtime)
print('%s下载完成! 耗费了%d秒' % (self._filename, self._downtime))
def main():
start = time()
t1 = DownloadTask('Python从入门到精通.pdf', 5)
t1.start()
t2 = DownloadTask('Python的编程核心.pdf', 8)
t2.start()
t1.join()
t2.join()
end = time()
print('总共耗费了%.2f秒.' % (end - start))
if __name__ == '__main__':
main()
上面代码的执行结果 -
开始下载Python从入门到精通.pdf...
开始下载Python的编程核心.pdf...
Python从入门到精通.pdf下载完成! 耗费了5秒
Python的编程核心.pdf下载完成! 耗费了8秒
总共耗费了8.01秒.
因为线程之间可以共享进程的内存空间,因此要实现多个线程间的通信相对简单,最直接的办法就是设置一个全局变量,多个线程共享这个全局变量即可。但是当多个线程共享同一个变量的时候,很有可能产生不可控的结果从而导致程序失效甚至崩溃。举个例子50个线程分别向账户中转入1元钱,代码如下,代码中有坑哈,大家可以试试。
from time import sleep
from threading import Thread
class Account(object):
"""
账户管理
"""
def __init__(self):
self._balance = 0
def deposit(self, money):
# 计算存款后的余额
new_balance = self._balance + money
# 模拟受理存款业务需要0.02秒的时间
sleep(0.02)
# 修改账户余额
self._balance = new_balance
@property
def balance(self):
return self._balance
class AddMoneyThread(Thread):
"""
存钱的线程
"""
def __init__(self, account, money):
super().__init__()
self._account = account
self._money = money
def run(self):
self._account.deposit(self._money)
def main():
account = Account()
threads = []
# 创建100个存款的线程向同一个账户中存钱
for _ in range(50):
t = AddMoneyThread(account, 1)
threads.append(t)
t.start()
# 等所有存款的线程都执行完毕
for t in threads:
t.join()
print('账户余额为: ¥%d元' % account.balance)
if __name__ == '__main__':
main()
上面脚本的执行结果 -
账户余额为: ¥1元
运行上面的程序,结果是不是让人大跌眼镜,为什么会出现这种情况是因为我们没有对银行账户这个“临界资源”加以保护,多个线程同时向账户中存钱时,会一起执行到new_balance = self._balance + money
这行代码,多个线程得到的账户余额都是初始状态下的0
,所以都是0
上面做了+1的操作,因此得到了错误的结果。
解决办法:加“锁”,通过“锁”来保护“临界资源”,只有获得“锁”的线程才能访问“临界资源”,而其他没有得到“锁”的线程只能被阻塞起来,直到获得“锁”的线程释放了“锁”,其他线程才有机会获得“锁”,进而访问被保护的“临界资源”。下面的代码演示了如何使用“锁”来保护对银行账户的操作,从而获得正确的结果。
from time import sleep
from threading import Thread, Lock
class Account(object):
"""
账户管理
"""
def __init__(self):
self._balance = 0
self._lock = Lock()
def deposit(self, money):
# 先获取锁才能执行后续的代码
self._lock.acquire()
try:
new_balance = self._balance + money
sleep(0.01)
self._balance = new_balance
finally:
# 在finally中执行释放锁的操作保证正常异常锁都能释放
self._lock.release()
@property
def balance(self):
return self._balance
class AddMoneyThread(Thread):
"""
存钱的线程
"""
def __init__(self, account, money):
super().__init__()
self._account = account
self._money = money
def run(self):
self._account.deposit(self._money)
def main():
account = Account()
threads = []
for _ in range(50):
t = AddMoneyThread(account, 1)
threads.append(t)
t.start()
for t in threads:
t.join()
print('账户余额为: ¥%d元' % account.balance)
if __name__ == '__main__':
main()
11.3 线程池
ThreadPoolExecutor
线程池:可以指定线程的数量,供用户调用,便于管理线程池。主要用于执行的目标很多,而手动限制线程数量又太繁琐的情况
方法 | 说明 |
submit | 函数格式:submit(fn, *args, **kwargs):将 fn 函数提交给线程池。*args 代表传给 fn 函数的参数,*kwargs 代表以关键字参数的形式为 fn 函数传入参数。 |
map | 函数格式:map(func, *iterables, timeout=None, chunksize=1):该函数类似于全局函数 map(func, *iterables),只是该函数将会启动多个线程,以异步方式立即对 iterables 执行 map 处理。 |
shutdown | 函数格式:shutdown(wait=True):关闭线程池。加了pool.shutdown(),则会等待所有线程都运行结束后,再运行后面语句。 |
其中
submit
方法会返回一个 Future 对象,Future 类主要用于获取线程任务函数的返回值。
方法 | 说明 |
cancel | 取消该 Future 代表的线程任务。如果该任务正在执行,不可取消,则该方法返回 False;否则,程序会取消该任务,并返回 True。 |
cancelled | 返回 Future 代表的线程任务是否被成功取消。 |
running | 如果该 Future 代表的线程任务正在执行、不可被取消,该方法返回 True。 |
done | 如果该 Funture 代表的线程任务被成功取消或执行完成,则该方法返回 True。 |
result | 获取该 Future 代表的线程任务最后返回的结果。如果 Future 代表的线程任务还未完成,该方法将会阻塞当前线程,其中 timeout 参数指定最多阻塞多少秒。 |
exception | 获取该 Future 代表的线程任务所引发的异常。如果该任务成功完成,没有异常,则该方法返回 None。 |
add_done_callback | 为该 Future 代表的线程任务注册一个“回调函数”,当该任务成功完成时,程序会自动触发该 fn 函数。 |
from concurrent.futures import ThreadPoolExecutor
from time import time, sleep
def download_task(filename, downtime):
print ('开始下载%s...' % filename)
sleep(downtime)
return '%s下载完成! 耗费了%d秒' % (filename, downtime)
def get_result(future):
print(future.result())
def main():
start = time()
with ThreadPoolExecutor(max_workers=2) as pool:
future1 = pool.submit(download_task, *('Python从入门到精通.pdf', 5))
future2 = pool.submit(download_task, *('Python的编程核心.pdf', 8))
# 为future1添加线程完成的回调函数
future1.add_done_callback(get_result)
# 为future2添加线程完成的回调函数
future2.add_done_callback(get_result)
end = time()
print('总共耗费了%.2f秒.' % (end - start))
if __name__ == '__main__':
main()
上面代码执行结果 -
开始下载Python从入门到精通.pdf...
开始下载Python的编程核心.pdf...
Python从入门到精通.pdf下载完成! 耗费了5秒
Python的编程核心.pdf下载完成! 耗费了8秒
总共耗费了8.00秒.