并发编程
概述
1、非并发编程
程序由单个步骤序列构成,包含独立子任务的程序执行性能低。
2、并发编程
异步、高效,分解子任务、简化流程与逻辑。
3、进程 process
一个程序的执行实例,每个进程有自己的地址空间、内存、数据栈及辅助数据。
4、线程 thread
同一进程内可被并行激活的控制流,共享相同上下文(空间地址、数据结构)。
特点:
(1)便于信息共享和通讯;
(2)线程访问顺序差异会导致结果不一致。
5、python GIL 全局解释器锁
python代码由虚拟机(解释器主循环)控制,主循环同时只能有一个控制线程执行。
多线程
_thread模块
python内置模块,提供了基本的线程和互斥锁支持,目前已经不建议使用,这里利用该模块理解多线程与单线程的区别。
特点:
1、没有控制进程结束机制;
2、只有一个控制原语(锁);
3、功能少于threading模块。
单线程实例
执行两次worker()函数,第一次耗时4s,第二次耗时2s,总耗时6s。
import time
def worker(n):
print(f'函数执行开始于:{time.ctime()}')
time.sleep(n)
print(f'函数执行结束于:{time.ctime()}')
def main():
print(f'【主函数执行开始于:{time.ctime()}】')
worker(4)
worker(2)
print(f'【主函数执行结束于:{time.ctime()}】')
if __name__ == '__main__':
main()
#运行结果
【主函数执行开始于:Fri Feb 21 16:08:20 2020】
函数执行开始于:Fri Feb 21 16:08:20 2020
函数执行结束于:Fri Feb 21 16:08:24 2020
函数执行开始于:Fri Feb 21 16:08:24 2020
函数执行结束于:Fri Feb 21 16:08:26 2020
【主函数执行结束于:Fri Feb 21 16:08:26 2020】
多线程实例
同样执行两次worker()函数,第一次耗时4s,第二次耗时2s,总耗时小于6s。
import time
import _thread
def worker(n):
print(f'函数执行开始于:{time.ctime()}')
time.sleep(n)
print(f'函数执行结束于:{time.ctime()}')
def main():
print(f'【主函数执行开始于:{time.ctime()}】')
_thread.start_new_thread(worker, (4,))
_thread.start_new_thread(worker, (2,))
time.sleep(5)
print(f'【主函数执行结束于:{time.ctime()}】')
if __name__ == '__main__':
main()
#运行结果
【主函数执行开始于:Fri Feb 21 16:21:26 2020】
函数执行开始于:Fri Feb 21 16:21:26 2020
函数执行开始于:Fri Feb 21 16:21:26 2020
函数执行结束于:Fri Feb 21 16:21:28 2020
函数执行结束于:Fri Feb 21 16:21:30 2020
【主函数执行结束于:Fri Feb 21 16:21:31 2020】
threading模块
该模块在较低级的模块 _thread 基础上建立较高级的线程接口。
1、主要功能
(1)获取当前线程:.current_thread()
(2)构造: .Tread(target=目标函数,args=(参数,))
(3)启动线程:.start()
(4)要求主线程等待:.join()
(5)线程名称:.name
2、构造方法
(1)Thread类方法
import time
import threading
def worker(n):
print(f'{threading.current_thread().name}函数执行开始于:{time.ctime()}')
time.sleep(n)
print(f'{threading.current_thread().name}函数执行结束于:{time.ctime()}')
def main():
print(f'【主函数执行开始于:{time.ctime()}】')
threads = []
t1 = threading.Thread(target=worker, args=(4,))
threads.append(t1)
t2 = threading.Thread(target=worker, args=(2,))
threads.append(t2)
for t in threads:
t.start()
for t in threads:
t.join()
print(f'【主函数执行结束于:{time.ctime()}】')
if __name__ == '__main__':
main()
#运行结果
【主函数执行开始于:Fri Feb 21 17:02:15 2020】
Thread-1函数执行开始于:Fri Feb 21 17:02:15 2020
Thread-2函数执行开始于:Fri Feb 21 17:02:15 2020
Thread-2函数执行结束于:Fri Feb 21 17:02:17 2020
Thread-1函数执行结束于:Fri Feb 21 17:02:19 2020
【主函数执行结束于:Fri Feb 21 17:02:19 2020】
(2)自定义Thread派生类
import time
import threading
def worker(n):
print(f'{threading.current_thread().name}函数执行开始于:{time.ctime()}')
time.sleep(n)
print(f'{threading.current_thread().name}函数执行结束于:{time.ctime()}')
class MyThread(threading.Thread):
def __init__(self,func,args):
threading.Thread.__init__(self)
self.func = func
self.args = args
def run(self):
self.func(*self.args)
def main():
print(f'【主函数执行开始于:{time.ctime()}】')
threads = []
t1 = MyThread(worker, (4,))
threads.append(t1)
t2 = MyThread(worker, (2,))
threads.append(t2)
for t in threads:
t.start()
for t in threads:
t.join()
print(f'【主函数执行结束于:{time.ctime()}】')
if __name__ == '__main__':
main()
#运行结果
【主函数执行开始于:Fri Feb 21 17:14:04 2020】
Thread-1函数执行开始于:Fri Feb 21 17:14:04 2020
Thread-2函数执行开始于:Fri Feb 21 17:14:04 2020
Thread-2函数执行结束于:Fri Feb 21 17:14:06 2020
Thread-1函数执行结束于:Fri Feb 21 17:14:08 2020
【主函数执行结束于:Fri Feb 21 17:14:08 2020】
同步原语:锁
多线程代码中,如果两个线程运行的顺序发生变化,可能造成代码的执行轨迹或者行为不相同,或者产生不一致的数据。为了避免这种现象,引入锁这一概念。
当多线程争夺锁时,允许第一个获得锁的线程进入临界区,并执行代码;所有之后到达的线程都将被阻塞,直到第一个线程执行结束并释放锁。此时其他的线程可以获得锁并进入临界区。注意:那些被阻塞的线程是没有顺序的(并不是先到先得),意味着下一个获得锁的线程的顺序并不是确定的。
1、获得:.acquire()
2、释放:.release()
3、支持上下文操作:with lock:
例:将1—5依次放入数组中,三个相同的任务同时进行,未使用锁时
import time
import threading
import random
eggs = []
def put_egg(n, lst):
for i in range(1, n+1):
time.sleep(random.randint(0,1))
lst.append(i)
def main():
threads = []
for i in range(3):
t = threading.Thread(target=put_egg,args=(5,eggs))
threads.append(t)
for t in threads:
t.start()
for t in threads:
t.join()
print(eggs)
if __name__ == '__main__':
main()
#运行结果
[1, 2, 1, 3, 4, 5, 2, 3, 4, 1, 5, 2, 3, 4, 5]
使用锁时
import time
import threading
import random
eggs = []
lock = threading.Lock()
def put_egg(n, lst):
lock.acquire()
for i in range(1, n + 1):
time.sleep(random.randint(0,1))
lst.append(i)
lock.release()
def main():
threads = []
for i in range(3):
t = threading.Thread(target=put_egg,args=(5,eggs))
threads.append(t)
for t in threads:
t.start()
for t in threads:
t.join()
print(eggs)
if __name__ == '__main__':
main()
#运行结果
[1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5]
也可以使用上下文操作:with
def put_egg(n, lst):
with lock:
for i in range(1, n + 1):
time.sleep(random.randint(0,1))
lst.append(i)
队列 queue模块
queue是python的标准库,俗称队列。队列可以完美解决线程间的数据交换,保证线程间数据的安全性和一致性。
1、队列种类
(1)Queue FIFO:先进先出
(2)LifoQueue LIFO:先进后出
(3)PriorityQueue :优先队列
2、使用方法
(1)构造实例:.Queue(maxsize=0)
(2)放入数据项:.put(item)
(3)获取数据项:.get(block, timeout)
(4)声明当前队列处理完毕:.task_done()
(5)队列所有项处理完毕前阻塞:.join()
例:使用先进先出序列,单线程产生数据放入队列,双线程从队列中取出数据
import time
import threading
import queue
import random
def producer(data_queue):
for i in range(5):
time.sleep(0.5)
item = random.randint(1,100)
data_queue.put(item)
print(f'{threading.current_thread().name}在队列中放入数据项{item}')
def consumer(data_queue):
while True:
try:
item = data_queue.get(timeout=3)
print(f'{threading.current_thread().name}从队列中移除了{item}')
except queue.Empty:
break
else:
data_queue.task_done()
def main():
q = queue.Queue()
threads = []
p = threading.Thread(target=producer,args=(q,))
p.start()
for i in range(2):
c = threading.Thread(target=consumer,args=(q,))
threads.append(c)
for t in threads:
t.start()
for t in threads:
t.join()
q.join()
if __name__ == '__main__':
main()
#运行结果
Thread-1在队列中放入数据项28
Thread-2从队列中移除了28
Thread-1在队列中放入数据项60
Thread-3从队列中移除了60
Thread-1在队列中放入数据项42
Thread-2从队列中移除了42
Thread-1在队列中放入数据项79
Thread-3从队列中移除了79
Thread-1在队列中放入数据项92
Thread-2从队列中移除了92
multiprocessing模块
该模块类似于上文的多线程,此外可以充分运用多核、多CPU的计算能力,适用于计算密集型任务。
import time
import multiprocessing
def func(n):
print(f'{multiprocessing.current_process().name}执行开始于{time.ctime()}')
time.sleep(n)
print(f'{multiprocessing.current_process().name}执行结束于{time.ctime()}')
def main():
print(f'主函数运行于:{time.ctime()}')
processes = []
p1 = multiprocessing.Process(target=func,args=(4,))
processes.append(p1)
p2 = multiprocessing.Process(target=func, args=(2,))
processes.append(p2)
for p in processes:
p.start()
for p in processes:
p.join()
print(f'主函数结束于:{time.ctime()}')
if __name__ == '__main__':
main()
#运行结果
主函数运行于:Fri Feb 21 19:16:08 2020
Process-1执行开始于Fri Feb 21 19:16:09 2020
Process-2执行开始于Fri Feb 21 19:16:09 2020
Process-2执行结束于Fri Feb 21 19:16:11 2020
Process-1执行结束于Fri Feb 21 19:16:13 2020
主函数结束于:Fri Feb 21 19:16:13 2020
concurrent.futures模块
concurrent.futures模块提供了使用工作线程或进程池运行任务的接口,线程和进程池API都是一样,所以应用只做最小的修改就可以在线程和进程之间地切换。
1、ThreadPoolExecutor 线程池执行器
2、ProcessPoolExecutor 进程池执行器
例:定义一个函数,分别使用顺序执行、线程池执行及进程池执行
#定义函数
import time
import concurrent.futures
numbers = list(range(1,11))
def count(n):
for i in range(10000000):
i += i
return i*n
def worker(x):
result = count(x)
print(f'数字{x}的计算结果是:{result}')
顺序执行
def sequential_execution():
start_time = time.clock()
for i in numbers:
worker(i)
print(f'顺序执行花费时间{time.clock()-start_time}秒')
if __name__ == '__main__':
sequential_execution()
#运行结果
数字1的计算结果是:19999998
数字2的计算结果是:39999996
数字3的计算结果是:59999994
数字4的计算结果是:79999992
数字5的计算结果是:99999990
数字6的计算结果是:119999988
数字7的计算结果是:139999986
数字8的计算结果是:159999984
数字9的计算结果是:179999982
数字10的计算结果是:199999980
顺序执行花费时间4.1379074000000005秒
线程池执行
def threading_execution():
start_time = time.clock()
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
for i in numbers:
executor.submit(worker,i)
print(f'线程池执行花费时间{time.clock() - start_time}秒')
if __name__ == '__main__':
threading_execution()
#运行结果
数字1的计算结果是:19999998
数字6的计算结果是:119999988
数字2的计算结果是:39999996
数字7的计算结果是:139999986
数字5的计算结果是:99999990
数字4的计算结果是:79999992
数字3的计算结果是:59999994
数字9的计算结果是:179999982
数字8的计算结果是:159999984
数字10的计算结果是:199999980
线程池执行花费时间4.4011857999999995秒
进程池执行
def process_execution():
start_time = time.clock()
with concurrent.futures.ProcessPoolExecutor(max_workers=5) as executor:
for i in numbers:
executor.submit(worker,i)
print(f'进程池执行花费时间{time.clock() - start_time}秒')
if __name__ == '__main__':
process_execution()
#运行结果
数字1的计算结果是:19999998
数字3的计算结果是:59999994
数字4的计算结果是:79999992
数字5的计算结果是:99999990
数字2的计算结果是:39999996
数字6的计算结果是:119999988
数字8的计算结果是:159999984
数字9的计算结果是:179999982
数字10的计算结果是:199999980
数字7的计算结果是:139999986
进程池执行花费时间1.4596897秒
未完待续!