目录
3.1 Python multiprocessing模块下的Queue类
1. 简介
简单了解关于进程和线程的基本原理。如果想更深入的学习可以去慕课app学习电子科技大学蒲晓蓉老师的《计算机操作系统》,会使你从计算机系统的原理掌握一些关于系统的知识。
进程(process),是指计算机中已经运行的程序。进程曾是分时系统的基本运作单位。在早期进程是程序的基本执行实体;而现如今面向线程设计的系统,进程是线程的容器。
线程(thread),是操作系统能够进行运算的调度的最小单位。多数情况下,被包含在进程中,是进程的实际运作单位。一个线程指的是进程中一个单一顺序的控制流,一个进程可以并发多个线程,每条线程并行执行不同的任务,线程具有许多传统进程所具有的特征,又被称为轻型进程(Light—Weight Process)或进程元;把进程称为重型进程(Heavy—Weight Process)。
进程和线程的区别。进程是面向操作系统的基本单位,由操作系统资源分配的;而线程是面向任务调度和执行的基本单位。不同的进程间都有独立的代码和数据空间,程序之间切换存在较大的开销,作为轻量级的进程(线程),一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。线程之间是相互影响的,一个线程崩溃整改进程都会死掉,进程崩溃后在保护模式下不会对其他进程产生影响,多进程要比多线程冗余性更高。进程是独立,程序运行的入口、顺序执行序列和程序出口对于每个进程来说都是独立的。但是线程是不能独立执行的,必须依存在应用程序中,有应用程序控制线程执行。
上面说进程是独立的所以进程间要数据共享就需要通过进程通信机制(IPC,Inter-Process Communication),具体的方式包含管道、信号、套接字、共享内存区等。
当然对于线程间的通信要更容易,线程处于同一个进程内,可以共享相同的上下文。在单核的CPU系统中,多线程是不可能的,在某个时刻能够获得CPU资源的线程是唯一的,多个线程通过时间差共享了CPU的资源。使用多线程实现并发编程能较大的提升程序的性能和时间,在操作系统中使用系统自带的监控管理工具查看系统使用的软件的线程数。
Python既支持多进程又支持多线程,因此使用Python实现并发编程主要有3种方式:多进程、多线程、多进程+多线程。
2. Python多进程编程
在UnixOS和LinuxOS中提供了 fork()函数来创建进程,调用fork()函数的是父进程,然后创建出子进程,子进程类似父进程的克隆版,但是子进程仍然拥有自己的PID。fork()函数会返回两次,父进程可以通过fork()函数的返回值得到子进程的PID,而子进程中的返回值永远都是0。Python的os模块提供了fork()函数,但是在windows系统中没有fork()可以调用,因此要实现跨平台的多进程编程,就要使用multiprocessing模块的process类来创建子进程,该模块还提供了更高级的封装,例如批量启动进程的进程池(pool)、用于进程间通信的队列(Queue)和管道(Pipe)等。
例:
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下载完成!耗时%s' % (filename, time_to_download))
def main():
start = time()
download_task('test.python')
download_task('中国话.zh_cn')
end = time()
print('花费时间%.2f秒。' % (end - start))
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()
Process1 = Process(target=download_task, args=('test.py', ))
Process1.start()
Process2 = Process(target=download_task, args=('中国话.zh_cn', ))
Process2.start()
Process1.join()
Process2.join()
end = time()
print('文件下载完成,累计耗时%.2f秒.' % (end - start))
if __name__ == "__main__":
main()
运行结果截图:
使用对进程执行任务时,能明显的缩短程序运行的时间通过Process类创建了进程对象,通过target参数传入一个函数来表示进程启动后要执行的代码,后面的args是一个元组,它代表了传递给函数的参数。Process对象的start方法来启动进程,join方法来表示等待进程结束。运行上面的代码可以明显的发现两个下载任务“同时”启动了,程序的执行时间也会大大的缩短。
3. 进程间通信
Python提供了多种实现进程间通信的机制,主要有以下两种:
3.1 Python multiprocessing模块下的Queue类
Queue
是构造方法,函数签名是Queue(maxsize=0)
,其中maxsize
设置队列的大小。
put(item, block=True, timeout=None): 往队列里放数据。如果满了的话,blocking = False 直接报 Full异常。如果blocking = True,就是等一会,timeout必须为 0 或正数。None为一直等下去,0为不等,正数n为等待n秒还不能存入,报Full异常。
get(item, block=True, timeout=None): 从队列里取数据。如果为空的话,blocking = False 直接报 empty异常。如果blocking = True,就是等一会,timeout必须为 0 或正数。None为一直等下去,0为不等,正数n为等待n秒还不能读取,报empty异常。
简单的介绍直接上代码吧,我感觉这个东西我越看越晕。
from multiprocessing import Process, Queue
from time import sleep
def sub_task(string, q):
number = q.get()
while number:
print(number, string)
sleep(0.01)
number = q.get()
def main():
q = Queue(10)
for number in range(1,11):
q.put(number)
p1 = Process(target=sub_task, args=('ping', q)).start()
p2 = Process(target=sub_task, args=('192.168.0.1', q)).start()
if __name__ == '__main__':
main()
代码运行结果:
3.2 Pipe,又称为“管道”
Pipe直译过来就是“管道”,用它来实现了多进程通信的编程方式,具体的理解方式等同于“管”类似的事物。一根管子管道有两个口,Pipe也常用来实现2个进程之间的通信,在管道的两端分别是两个进程,一端用来传输数据,一端用来接收数据。
使用Pipe实现进程通信,首先需要调用multiprocessing.Pip()函数来创建一个管道,语法格式如下:
conn1, conn2 = multiprocessing.Pipe( [duplex=True] )
conn1 和 conn2 分别用来接收Pipe函数返回的两个端口,duplex参数默认为True,表示该管道是双向的,就是两个端口既可以发数据,亦可以接收数据。如果duplex为False,表示管道是单向的,conn1 只能接收数据,而 conn2 只能发送数据。conn1 和 conn2 都属于 PipeConnection 对象,它们还可以调用下表所示的这些方法。
方法名 | 功能 |
---|---|
send(obj) | 发送一个 obj 给管道的另一端,另一端使用 recv() 方法接收。需要说明的是,该 obj 必须是可序列化的,如果该对象序列化之后超过 32MB,则很可能会引发 ValueError 异常。 |
recv() | 接收另一端通过 send() 方法发送过来的数据。 |
close() | 关闭连接。 |
poll([timeout]) | 返回连接中是否还有数据可以读取。 |
send_bytes(buffer[, offset[, size]]) | 发送字节数据。如果没有指定 offset、size 参数,则默认发送 buffer 字节串的全部数据;如果指定了 offset 和 size 参数,则只发送 buffer 字节串中从 offset 开始、长度为 size 的字节数据。通过该方法发送的数据,应该使用 recv_bytes() 或 recv_bytes_into 方法接收。 |
recv_bytes([maxlength]) | 接收通过 send_bytes() 方法发送的数据,maxlength 指定最多接收的字节数。该方法返回接收到的字节数据。 |
recv_bytes_into(buffer[, offset]) | 功能与 recv_bytes() 方法类似,只是该方法将接收到的数据放在 buffer 中。 |
代码展示如下,自己研究了很久感觉这些玩意儿不是那么容易,subprocess模块中的类和函数来创建和启动子进程,但是我弄了半天也没成功,我也想装*啊,可是实力不允许。
import multiprocessing
import random
import re
import subprocess
def proc_send(conn, ipaddr):
ipInfo = subprocess.Popen(["ping.exe", ipaddr], stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE, shell = True)
out = ipInfo.stdout.read().decode('gbk')
conn.send(out)
print(multiprocessing.current_process().pid,"进程发送数据:",ipaddr)
def proc_recv(conn):
judge = re.search(r'已接收 = \d', conn.recv())
counter = int(judge.group()[6:])
if counter > 0:
print(multiprocessing.current_process().pid,"接收数据:It's OK")
else:
print('%d不通' % conn.recv())
if __name__ == '__main__':
pipe = multiprocessing.Pipe()
p1 = multiprocessing.Process(target=proc_send,args=(pipe[0],['192.168.101.1']))
p2 = multiprocessing.Process(target=proc_recv,args=(pipe[1],))
p1.start()
p2.start()
p1.join()
p2.terminate()
代码执行结果:
4. 多线程
Python早期的版本中使用thread模块(现在命名为_thread)来实现多线程编程。目前使用较多的是threading模块,该模块对多线程编程提供了更好的面对对象的封装。如何进行多线程编程呢?狗!狗!狗!
from random import randint
from threading import Thread
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()
task1 = Thread(target=download_task, args=('test.tar',))
task1.start()
task2 = Thread(target=download_task, args=('filename.txt',))
task2.start()
task1.join()
task2.join()
end = time()
print('累计用时%.2f秒.' % (end - start))
if __name__ == '__main__':
main()
代码运行结果截图:
4.1 创建自定义的线程类
在Python中使用threading模块的Thread类来创建线程,在面向对象一节中本人学习了一个相关概念叫“继承”,可以在已有的类基础上创建新类,因此也可以通过继承Thread类的方式创建自定义的线程类,并通过创建线程对象来启动线程。
from threading import Thread
from random import randint
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()
Thread1 = DownloadTask('test.txt')
Thread1.start()
Thread2 = DownloadTask('filename.py')
Thread2.start()
Thread1.join()
Thread2.join()
end = time()
print('累计用时%.2f' % (end - start))
if __name__ == '__main__':
main()
代码运行结果截图:
多线程可以共享进程的内存空间,所以在线程间实现通信要简单很多。在多个线程共享一个全局变量的时候,很有可能产生不可控的结果导致程序崩溃。所以在多个线程竞争同一个资源的时候(通尝称之为"临界资源"),对"临界资源"的访问需要加上保护,否则就会出现“混乱”的状态。
例,在例子中启用100个线程同时向一个账户进行转账,演示结果如下:
"""
100个线程同时向一个账户转账一元钱的场景,银行账户作为一个临界资源
"""
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
sleep(0.01)
self._balance = new_balance
@property
def balance(self):
return self._balance
class AddMonryThread(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 = AddMonryThread(account, 1)
threads.append(t)
t.start()
for t in threads:
t.join()
print('账户余额:¥%d元' % account.balance)
if __name__ == '__main__':
main()
执行结果:
通过执行代码发现100个线程分别向同一个账户转入一元钱,结果远远没有达到理想的值。出现这样的情况是因为银行账户“临界资源”未受到保护,多个线程同时向账户中存钱时,会同时执行new_balance = self._balance + money,类似于百米赛跑无论多少个人跑,但每个人都是从0跑到100,但是多线程的目的是计算100个人跑了多少。线程在初始账户为0的情况下把1元存进余额因此得到了错误的结果。这里就将出现一个新的概念,也是早闻其名不见其人的“锁”,通过“锁”来保护“临界资源”,只有获得“锁”的线程才能访问“临界资源”,而其他没有得到“锁”的线程只能被阻塞,直到获得“锁”的线程释放了“锁”,其他线程才有机会获取“锁”,才能访问“临界资源”。简单的理解就是通过“锁”将本身属于同一时刻的不同跑道上的人让他变得有序,这样就能计算在时间段内每个人跑了多少距离。
from threading import Thread, Lock
from time import sleep
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()
print("初始金额%d" % account.balance)
threads = []
for _ in range(100):
t = AddMoneyThread(account, 1)
threads.append(t)
# print(threads)
t.start()
for t in threads:
t.join()
print('账户余额为%d' % account.balance)
if __name__ == '__main__':
main()
5. 练习
练习1 :将耗时的任务放到线程中以换取更好的用户体验
在实例中采用sleep()函数来模拟下载任务花费的时间,在不使用"多线程"的情况下,开始下载后整个程序的其他部分会被这个耗时的任务阻塞而导致异常。
import time
import tkinter
import tkinter.messagebox
def download():
# Time delay is used to simulate download time
time.sleep(10)
tkinter.messagebox.showinfo(title='提示', message='下载完成')
def show_about():
tkinter.messagebox.showinfo(title='Author', message='jiejie')
def main():
tp = tkinter.Tk()
tp.title('单线程')
tp.geometry('200x200')
tp.wm_attributes('-topmost', True)
panel = tkinter.Frame(tp)
button1 = tkinter.Button(panel, text='下载', command=download)
button1.pack(side='left')
button2 = tkinter.Button(panel, text='Authon', command=show_about)
button2.pack(side='right')
panel.pack(side='bottom')
tkinter.mainloop()
if __name__ == '__main__':
main()
使用多线程将耗时的任务放到一个独立的线程里去执行,这样耗时的任务就不会阻塞主线程运行,与上面的代码执行对比会发现,上面的代码执行时会发生卡死,即需要等待下载任务执行完毕才能进行下一步操作;而下面的代码执行时在开始下载任务的同时也能进行其他操作,比如点击Author按钮或者直接关闭弹窗。
import time
import tkinter
import tkinter.messagebox
from threading import Thread
def main():
class DownloadTaskHandler(Thread):
def run(self):
time.sleep(10)
tkinter.messagebox.showinfo(title='Tips', message='下载完成咯')
# Enable download button
button1.config(state=tkinter.NORMAL)
def download():
# Disable download button
button1.config(state=tkinter.DISABLED)
# The thread is set as a guard thread throught the daemon parameter
# When the main thread exits,it is no longer retained.
# Using threads to handle time-consuming download tasks
DownloadTaskHandler(daemon=True).start()
def show_about():
tkinter.messagebox.showinfo(title='Author', message='jiejie')
tp = tkinter.Tk()
tp.title('单线程')
tp.geometry('200x200')
tp.wm_attributes('-topmost', 1)
panel = tkinter.Frame(tp)
button1 = tkinter.Button(panel, text='下载', command=download)
button1.pack(side='left')
button2 = tkinter.Button(panel, text='Author', command=show_about)
button2.pack(side='right')
panel.pack(side='bottom')
tkinter.mainloop()
if __name__ == '__main__':
main()
练习2 :使用多进程对复杂任务进行同时管理
实例为求1~100000000的和,该任务为计算密集型任务,常规做法如下:
from time import time
def main():
total = 0
number_list = [x for x in range(100000000)]
start = time()
for number in number_list:
total += number
print(total)
end = time()
print('耗时%.3f秒' % (end - start))
if __name__ == '__main__':
main()
在上面的代码中,创建一个列表作为容器装入1~100000000个数,然后再进行计算。那如果采用分解式解决方案呢?代码如下:
from multiprocessing import Process, Queue
from random import randint
from time import time
def task_handler(curr_list, result_queue):
total = 0
for number in curr_list:
total += number
result_queue.put(total)
def main():
processes = []
number_list = [x for x in range(1, 100000001)]
result_queue = Queue()
index = 0
# Start 8 processes to slice the data for calculation
for _ in range(8):
p = Process(target=task_handler,
args=(number_list[index:index + 12500000], result_queue))
index += 12500000
processes.append(p)
p.start()
# Start recording the time spent by all processes after the calculation is completed
start = time()
for p in processes:
p.join()
# Merge results
total = 0
while not result_queue.empty():
total += result_queue.get()
print(total)
end = time()
print('耗时:', (end - start), 's', sep='')
if __name__ == '__main__':
main()
在执行这两段代码后,比较结果。发现使用多进程能极大的节省计算的时间,多进程获得了更多的计算机资源充分的利用了计算机多核CPU的特性。当然了在实际的执行过程中感觉没有太大的区别,这部分原因让我在查查资料。