并发编程之多线程
目录
并发编程之多线程
一 同步锁
1 两个注意点
1).线程抢的是GIL锁,GIL锁相当于执行权限,拿到执行权限后才能拿到互斥锁Lock,其他线程也可以抢到GIL,但如果发现Lock仍然没有被释放则阻塞,即便是拿到执行权限GIL也要立刻交出来。
2).join是等待所有,即整体串行,而锁只是锁住修改共享数据的部分,即部分串行,想要保证数据安全的根本原理在于让并发编程串行,join与互斥锁都可以实现,毫无疑问,互斥锁的部分串行效率要更高。
2 验证GIL锁的存在
启动跟自己机器cpu相同的进程和线程数,多进程可以最大化利用cpu,多线程无法有效利用多核cpu。
from threading import Thread
from multiprocessing import Process
def task():
while True:
pass
if __name__ == '__main__':
for i in range(4): # 启动跟自己机器cpu相同的进程和线程数,验证GIL的存在
# t = Thread(target=task) # Pycharm进程CPU占用约35%
t = Process(target=task) # Pycharm进程CPU占用约90%
t.start()
3 GIL与普通互斥锁的区别
3.1 前提:为何要使用锁
锁的目的是保护共享的数据,同一时间只能有一个线程来修改共享的数据。保护不同的数据要使用不同的锁。
3.2 GIL与普通互斥锁的区别
GIL锁保护解释器级别的数据安全(如垃圾回收的数据) ,不保护应用程序级别的数据;
应用程序的数据需要开发者自行加锁保护,如进程、线程内的共享数据。
GIL锁全套
###### GIL锁全套:
# 什么是GIL锁?
GIL锁是全局解释器锁
# 为何要有GIL锁?
为了实现python的垃圾回收机制,才有了GIL锁;
python解释型语言,需要做垃圾回收,数据进程内共享,多线程下有的线程没有操作变量,有的有,为防止垃圾回收出错,所以作者想了一个办法,垃圾回收的时候,变量只在一个线程使用,其他线程都不要使用,然后来决定是否回收。
编译型语言没有这个问题,要先编译,然后再执行,可以监控到变量是否在使用,不存在GIL锁的问题。
# 为何加互斥锁?GIL与普通互斥锁的区别?
# 首先,GIL锁与互斥锁,控制级别不一样
GIL锁有了以后,同一时刻同一进程内,只有一个线程执行。如定义a=0 多个线程对a进行累加,并发执行,数据会乱【为何会乱?线程1拿到a=0,遇到IO或者时间片调度;切换到线程2去使用,拿到了a,进行a+1,遇到IO;线程3拿到了a,进行a+1,遇到IO,切换出来执行,最后还是a+1=0+1,最后都是1】,加互斥锁,是因为GIL锁无法控制用户级别的线程安全。
3.3 代码示例:
不加互斥锁,多个线程同时修改同一个数据,无法保证数据的安全。
from threading import Thread, Lock
import time
mutex = Lock()
money = 100
def task():
global money
# mutex.acquire()
temp = money
time.sleep(1)
money = temp - 1
# mutex.release()
if __name__ == '__main__':
ll = []
for i in range(10):
t = Thread(target=task,)
t.start()
# t.join() # 会怎么样?变成了串行,不能这么做
ll.append(t)
for t in ll:
t.join()
print(money)
4 IO密集型与计算密集型
其它语言中,不允许开启多进程,都是开启多线程完成并发,而使用cpython解释器的python中,由于GIL锁的存在,一个进程同时只有一个线程,所以针对IO密集操作和计算密集操作有不同的方法: # 计算密集型:开多进程 # I/O密集型:开多线程
from threading import Threadfrom multiprocessing import Processimport time# 计算密集型def task(): count = 0 for i in range(100000000): count += 1if __name__ == '__main__': ctime = time.time() ll = [] for i in range(10): # t = Thread(target=task,) # 开线程:55.27706050872803 t = Process(target=task, ) # 开进程:16.265764474868774 t.start() ll.append(t) for i in ll: i.join() print(time.time()-ctime)
# IO密集型def task(): time.sleep(2)if __name__ == '__main__': ctime = time.time() ll = [] for i in range(400): # t = Thread(target=task,) # 开线程:2.076205253601074 t = Process(target=task,) # 开进程:10.390235424041748 t.start() ll.append(t) for t in ll: t.join() print(time.time()-ctime)
二 死锁现象与递归锁
1 什么是死锁现象
指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,他们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
2 如下就是死锁:
# 死锁现象,张三拿到了A锁,等B锁,李四拿到了B锁,等A锁from threading import Thread,Lockimport timemutexA = Lock()mutexB = Lock()def eat_apple(name): mutexA.acquire() print('%s 获得了a锁' % name) mutexB.acquire() print('%s 获得了b锁' % name) print('开始吃苹果,并且吃完了') mutexB.release() print('%s 释放了b锁' % name) mutexA.release() print('%s 释放了a锁' % name)def eat_watermelon(name): mutexB.acquire() print('%s 获得了b锁' % name) time.sleep(2) mutexA.acquire() print('%s 获得了a锁' % name) print('开始吃瓜,并且吃完了') mutexA.release() print('%s 释放了a锁' % name) mutexB.release() print('%s 释放了b锁' % name)if __name__ == '__main__': ll = ['lxx', 'alex', 'youngboy'] for name in ll: t1 = Thread(target=eat_apple,args=(name,)) t2 = Thread(target=eat_watermelon,args=(name,)) t1.start() t2.start()
3 什么是递归锁
为了解决死锁问题,python提供了可重入锁RLock。这个RLock内部维护者一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。
4 递归锁解决死锁示例:
# 解决死锁现象,使用递归锁from threading import Thread,RLockimport timemutexA = mutexB = RLock()def eat_apple(name): mutexA.acquire() print('%s 获得了a锁' % name) mutexB.acquire() print('%s 获得了b锁' % name) print('开始吃苹果,并且吃完了') mutexB.release() print('%s 释放了b锁' % name) mutexA.release() print('%s 释放了a锁' % name)def eat_watermelon(name): mutexB.acquire() print('%s 获得了b锁' % name) time.sleep(2) mutexA.acquire() print('%s 获得了a锁' % name) print('开始吃瓜,并且吃完了') mutexA.release() print('%s 释放了a锁' % name) mutexB.release() print('%s 释放了b锁' % name)if __name__ == '__main__': ll = ['lxx', 'alex', 'youngboy'] for name in ll: t1 = Thread(target=eat_apple,args=(name,)) t2 = Thread(target=eat_watermelon,args=(name,)) t1.start() t2.start()
三 信号量Semaphore
1 信号量Semaphore是什么
Semaphore管理一个内置的计数器,每当调用acquire()时内置计数器-1;调用release()时内置计数器+1;计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。
2 信号量与进程池的区别
信号量与进程池是完全不同的概念,进程池Pool(4),最大只能产生4个进程,而且从头到尾只有者四个进程,不会产生新的,而信号量是产生一堆线程/进程。
3 代码示例:
# (同时只有5个线程可以获得semaphore,即可以限制最大连接数为5):# Semaphore:信号量可以理解为多把锁,同时允许多个线程来更改数据from threading import Thread,Semaphoreimport timeimport randomsm = Semaphore(5) # 数字表示可以同时有多少个线程操作def task(name): sm.acquire() print('%s is in wc' % name) time.sleep(random.randint(1,5)) sm.release()if __name__ == '__main__': for i in range(15): t = Thread(target=task, args=('lxx %s 号'%i,)) t.start()
四 Event事件
1 什么是Event事件
一些线程需要等到其他线程执行完成之后才能执行,类似于发射信号。比如一个线程等待另一个线程执行结束之后再继续执行。这种情况下需要用到Event事件来实现。
2 Event事件的用法
1) event.isSet(): 返回event的状态值;2) event.wait(): 如果 event.isSet()==False将阻塞线程;3) event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态,等待操作系统调度;4) event.clear(): 恢复event的状态值为False。
3 代码示例:
# 通过event事件,实现两个线程,一个线程读文件前一半,写入另一个文件,另一个线程读后一半写入文件from threading import Thread, Eventimport osevent = Event() # 获取文件的大小size = os.path.getsize('zhuxian.txt')def read_first(): with open('zhuxian.txt', 'r', encoding='utf-8') as f: n = size // 2 # 取文件一半,整除 data = f.read(n) with open('aaa.txt', 'w', encoding='utf-8') as f1: f1.write(data) print('我读完一半了,发了个信号') event.set()def read_last(): event.wait() with open('zhuxian.txt', 'r', encoding='utf-8') as f: n = size // 2 # 取文件一半,整除 f.seek(n, 0) # 光标从文件开头开始,移动了n个字节,移动到文件一半 data = f.read() with open('aaa.txt', mode='at', encoding='utf-8') as f1: f1.write(data)if __name__ == '__main__': t1 = Thread(target=read_first) t1.start() t2 = Thread(target=read_last) t2.start()
五 线程Queue
1 为什么要使用线程的Queue
线程间通信,因为共享变量会出现数据不安全问题,用线程queue通信,不需要加锁,内部自带。
2 线程的Queue用法
# 进程的Queue队列from multiprocessing import Queue# 线程的Queue队列from queue import Queue,LifoQueue,PriorityQueue
# 三种线程方法之Queue :队列,先进先出from queue import Queue, PriorityQueue, LifoQueueq = Queue(5)ll = ['lxx', 'alex', 'blex', 'clex', 'dlex']for i in ll: q.put(i)# q.put('abp') # 超过队列的最大值,会卡住# q.put_nowait('snis') # queue.Full# 取值for i in range(len(ll)): print(q.get())# print(q.get()) # 队列没有值的时候,再取,会卡住# print(q.get_nowait()) # _queue.Empty
# 三种线程方法之LifoQueue: 堆栈,后进先出q = LifoQueue(5)ll = ['lxx', 'alex', 'blex', 'clex', 'dlex']for i in ll: q.put(i)for i in range(len(ll)): print(q.get())
# 三种线程方法之PriorityQueue:优先级队列,谁小谁先出q = PriorityQueue(5)ll = ['lxx', 'alex', 'blex', 'clex', 'dlex']lxx = [-10, 100, 99, 88, 102]count = 0for i in ll: q.put((lxx[count], i)) # 存值的时候要用元组 count += 1for i in range(len(ll)): print(q.get()) # 先打印出优先级数字小的
# 其他用法,同线程Queue
六 线程池和进程池的shutdown
# 主线程等待所有任务完成from concurrent.futures import ThreadPoolExecutorimport timepool = ThreadPoolExecutor(3)def task(name): print('%s 开始' % name) time.sleep(1) print('%s 结束' % name)if __name__ == '__main__': for i in range(20): pool.submit(task, '沙雕%s' % i) pool.shutdown(wait=True) # 放到for循环外面,等待所有任务完成,并且把池关闭 # pool.submit(task, 'qqqqq') # 关闭进程池之后不能再提交任务了 RuntimeError: cannot schedule new futures after shutdown print('>>>main<<<')
七 定时器了解
# 控制多长时间后执行一个任务from threading import Timerdef task(name): print('汤姆大战杰瑞————%s' % name)if __name__ == '__main__': # t = Timer(2, task, args=('lxx',)) # 本质是开一个线程,延迟两秒执行 t = Timer(2, task, kwargs={'name': 'lxx'}) # 本质是开一个线程,延迟两秒执行 t.start()
八 Python标准模块--concurrent.futures
1 基本使用
concurrent.futures模块提供了高度封装的异步调用接口;ThreadPoolExecutor: 线程池。提供异步调用;ProcessPoolExecutor: 进程池,提供异步调用;
# 基本方法1) submit(fn, *args, **kwargs) # 异步提交任务2) map(func, *iterables, timeout=None, chunksize=1) # 取代for循环submit的操作3) shutdown(wait=True) # 相当于进程池的pool.close()+pool.join()操作;wait=True,等待池内所有任务执行完毕回收完资源后才继续;wait=False,立即返回,并不会等待池内的任务执行完毕;但不管wait参数为何值,整个程序都会等到所有任务执行完毕;submit和map必须在shutdown之前。4) result(timeout=None) # 取得结果5) add_done_callback(fn) # 回调函数
2 线程池
2.1 为什么要有线程池?
不管是开进程和开线程,不能无限制开,通过池,假设池子里就有10个位子,不管怎么开,永远是这10个。
2.2 如何使用
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutorfrom threading import Threadimport time, randompool = ThreadPoolExecutor(5) # 数字是池的大小# pool = ProcessPoolExecutor(5) # 数字是池的大小def task(name): print('%s 任务开始' % name) time.sleep(random.randint(1, 4)) print('任务结束') return '%s 返回了' % namedef call_back(f): print(f.result())if __name__ == '__main__': # 基础版本 # ll = [] # for i in range(100): # res = pool.submit(task, '沙雕 %s 号' % i) # 不需要再写在args中了 # # res 是Feature对象 # # from concurrent.futures._base import Future # # print(type(res)) # <class 'concurrent.futures._base.Future'> # # print(res.result()) # 像join,只要执行result,就会等着结果回来,就变成串行了 # ll.append(res) # for res in ll: # print(res.result()) # 终极版本 for i in range(100): pool.submit(task,'沙雕 %s 号'%i).add_done_callback(call_back) # 向线程池中提交一个任务,等任务执行完成,自动回到到call_back函数执行
3 进程池
# 1 如何使用from concurrent.futures import ProcessPoolExecutorpool = ProcessPoolExecutor(2)pool.submit(get_pages, url).add_done_callback(call_back)

浙公网安备 33010602011771号