python
进程和线程
进程:进程是系统中正在运行的一个程序,程序一旦运行就是进程,一个进程可以拥有多个线程,每个线程使用其所属进程的栈空间。
线程:线程是进程的一个实体,是进程的一条执行路径。 多线程也并不是没有坏处,占用了更多的CPU执行时间,导致其他程序无法获得足够的CPU执行时间
并发:程序同时执行多个任务也就是常说的并发
Python既支持多进程又支持多线程,因此使用Python实现并发编程主要有3种方式:多进程、多线程、多进程+多线程。
Python中的多进程
Unix和Linux操作系统上提供了fork()系统调用来创建进程,调用fork()函数的是父进程,创建出的是子进程,子进程是父进程的一个拷贝,但是子进程拥有自己的PID。fork()函数非常特殊它会返回两次,父进程中可以通过fork()函数的返回值得到子进程的PID,而子进程中的返回值永远都是0。Python的os模块提供了fork()函数。由于Windows系统没有fork()调用,因此要实现跨平台的多进程编程,可以使用multiprocessing模块的Process类来创建子进程
不使用多进程
from random import randint
from time import time, sleep
def download_task(filename):
print('开始下载%s...' % filename)
time_to_download = randint(5, 10)
sleep(time_to_download)
print('%s下载完成! 耗费了%d秒' % (filename, time_to_download))
def main():
start = time()
download_task('Python从入门到放弃.pdf')
download_task('Tokyo Hot.avi')
end = time()
print('总共耗费了%.2f秒.' % (end - start))
''''''
开始下载Python从入门到放弃.pdf...
Python从入门到放弃.pdf下载完成! 耗费了5秒
开始下载Tokyo Hot.avi...
Tokyo Hot.avi下载完成! 耗费了5秒
总共耗费了10.01秒.
''''''
if __name__ == '__main__':
main()
从上面的例子可以看出,如果程序中的代码只能按顺序一点点的往下执行,那么即使执行两个毫不相关的下载任务,也需要先等待一个文件下载完成后才能开始下一个下载任务,很显然这并不合理也没有效率。接下来我们使用多进程的方式将两个下载任务放到不同的进程中
多进程
from multiprocessing import Process
from os import getpid
from random import randint
from time import time, sleep
def download_task(filename):
print('启动下载进程,进程号[%d].' % getpid())
print('开始下载%s...' % filename)
time_to_download = randint(5, 10)
sleep(time_to_download)
print('%s下载完成! 耗费了%d秒' % (filename, time_to_download))
def main():
start = time()
p1 = Process(target=download_task, args=('Python从入门到放弃.pdf', )) #创建一个Process实例
p1.start() #start()方法启动
p2 = Process(target=download_task, args=('Peking Hot.avi', ))
p2.start()
p1.join() #等待子进程结束后再继续往下运行,在当前位置阻塞主进程,待执行join()的进程结束后再继续执行主进程
p2.join()
end = time()
print('总共耗费了%.2f秒.' % (end - start))
''''''
启动下载进程,进程号[10].
开始下载Tokyo Hot.avi...
Tokyo Hot.avi下载完成! 耗费了6秒
启动下载进程,进程号[9].
开始下载Python从入门到放弃.pdf...
Python从入门到放弃.pdf下载完成! 耗费了9秒
总共耗费了9.05秒.
''''''
if __name__ == '__main__':
main()
通过Process类创建了进程对象,通过target参数我们传入一个函数来表示进程启动后要执行的代码,后面的args是一个元组,它代表了传递给函数的参数。Process对象的start方法用来启动进程,而join方法表示等待进程执行结束。运行上面的代码可以明显发现两个下载任务“同时”启动了,而且程序的执行时间将大大缩短,不再是两个任务的时间总和。
Python中的多线程
目前的多线程开发通常使用threading模块,该模块对多线程编程提供了更好的面向对象的封装
from random import randint
from threading import Thread
from time import time, sleep
def download(filename):
print('开始下载%s...' % filename)
time_to_download = randint(5, 10)
sleep(time_to_download)
print('%s下载完成! 耗费了%d秒' % (filename, time_to_download))
def main():
start = time()
t1 = Thread(target=download, args=('Python从入门到放弃.pdf',))
t1.start()
t2 = Thread(target=download, args=('Tokyo Hot.avi',))
t2.start()
t1.join()
t2.join()
end = time()
print('总共耗费了%.3f秒' % (end - start))
''''''
开始下载Python从入门到放弃.pdf...
开始下载Tokyo Hot.avi...
Python从入门到放弃.pdf下载完成! 耗费了10秒
Tokyo Hot.avi下载完成! 耗费了10秒
总共耗费了10.010秒
''''''
if __name__ == '__main__':
main()
我们可以直接使用threading模块的Thread类来创建线程,但是我们之前讲过一个非常重要的概念叫“继承”,我们可以从已有的类创建新类,因此也可以通过继承Thread类的方式来创建自定义的线程类,然后再创建线程对象并启动线程。
from random import randint
from threading import Thread
from time import time, sleep
class DownloadTask(Thread):
def __init__(self, filename):
super().__init__() #函数是用于调用父类(超类)的一个方法。
self._filename = filename
def run(self):
print('开始下载%s...' % self._filename)
time_to_download = randint(5, 10)
sleep(time_to_download)
print('%s下载完成! 耗费了%d秒' % (self._filename, time_to_download))
def main():
start = time()
t1 = DownloadTask('Python从入门到放弃.pdf')
t1.start()
t2 = DownloadTask('Tokyo Hot.avi')
t2.start()
t1.join()
t2.join()
end = time()
print('总共耗费了%.2f秒.' % (end - start))
''''''
开始下载Python从入门到放弃.pdf...
开始下载Tokyo Hot.avi...
Python从入门到放弃.pdf下载完成! 耗费了5秒
Tokyo Hot.avi下载完成! 耗费了9秒
总共耗费了9.01秒.
''''''
if __name__ == '__main__':
main()
因为多个线程可以共享进程的内存空间,因此要实现多个线程间的通信相对简单,大家能想到的最直接的办法就是设置一个全局变量,多个线程共享这个全局变量即可。但是当多个线程共享同一个变量(我们通常称之为“资源”)的时候,很有可能产生不可控的结果从而导致程序失效甚至崩溃。如果一个资源被多个线程竞争使用,那么我们通常称之为“临界资源”,对“临界资源”的访问需要加上保护,否则资源会处于“混乱”的状态。下面的例子演示了100个线程向同一个银行账户转账(转入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.01秒的时间
sleep(0.01)
# 修改账户余额
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(100):
t = AddMoneyThread(account, 1)
threads.append(t)
t.start()
# 等所有存款的线程都执行完毕
for t in threads:
t.join()
print('账户余额为: ¥%d元' % account.balance) #账户余额为: ¥1元
if __name__ == '__main__':
main()
100个线程分别向账户中转入1元钱,结果居然远远小于100元。之所以出现这种情况是因为我们没有对银行账户这个“临界资源”加以保护,多个线程同时向账户中存钱时,会一起执行到new_balance = self._balance + money这行代码,多个线程得到的账户余额都是初始状态下的0,所以都是0上面做了+1的操作,因此得到了错误的结果。在这种情况下,“锁”就可以派上用场了。我们可以通过“锁”来保护“临界资源”,只有获得“锁”的线程才能访问“临界资源”,而其他没有得到“锁”的线程只能被阻塞起来,直到获得“锁”的线程释放了“锁”,其他线程才有机会获得“锁”,进而访问被保护的“临界资源”。下面的代码演示了如何使用“锁”来保护对银行账户的操作,从而获得正确的结果。
from time import sleep
from threading import Thread, Lock #引入Lock模块
class Account(object):
def __init__(self):
self._balance = 0
self._lock = Lock() #实例化锁
def deposit(self, money):
# 先获取锁才能执行后续的代码
self._lock.acquire() #.acquire()获得锁定
try:
new_balance = self._balance + money
sleep(0.01)
self._balance = new_balance
finally:
# 在finally中执行释放锁的操作保证正常异常锁都能释放
self._lock.release() #.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(100):
t = AddMoneyThread(account, 1)
threads.append(t)
t.start()
for t in threads:
t.join()
print('账户余额为: ¥%d元' % account.balance) #账户余额为: ¥100元
if __name__ == '__main__':
main()
Python的多线程并不能发挥CPU的多核特性,只要启动几个执行死循环的线程就可以得到证实了。是因为Python的解释器有一个“全局解释器锁”(GIL)的东西,任何线程执行前必须先获得GIL锁,然后每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行,但是即便如此,使用多线程在提升执行效率和改善用户体验方面仍然是有积极意义的。