python 并发之多线程
线程 - Thread
线程是一条时间线,假设这一条时间线是烧水喝茶的事件,可以先开始烧水,在等待水开的时候(IO操作)去刷一下杯子,本质是时间(CPU资源)的合理利用
-
启动和停止线程
开启一个线程只需要调用Thread库函数,但是并发可不是简单的调库而已
from threading import Thread def func(param): time.sleep(1) return a = Thread(target=func,args=(param,)) # 创建一个执行func函数的线程 b = Thread(target=func,args=(param,)) # 注意这其实启动了三个线程,一个主线程(这个程序的运行),两个子线程 a.start() b.start()
-
__init__(self, group=None, target=None, name=None,args=(), kwargs=None, *, daemon=None):
group
:指定所创建的线程隶属于哪个线程组target
:指定所创建的线程要调度的目标方法;args
:以元组的方式,为 target 指定的方法传递参数,args=(param,)
[^一定要这样]kwargs
:以字典的方式,为 target 指定的方法传递参数daemon
:指定所创建的线程是否为守护线程(后台线程,只要主线程执行完毕,不管子线程有没有结束,都会被强制退出)
-
.start()
开启线程 -
.join()
等待此线程运行结束,再运行主进程(普通情况下,主线程只要将子线程开启,就会开始运行接下来的部分,不管子线程有没有结束 -
.is_alive()
判断此线程是否结束 -
.getName()
获取此线程名字,若没有指定,则默认Thread-num
-
.setName()
设置线程名字 -
.setDaemon(True)
或.daemon=True
[^这是使用了setter的效果] 设置此线程为守护线程,注意,这个方法一定要用在.start()
方法之前,活跃的线程无法再被设置为守护线程
自定义线程
继承式自定义线程就是重写了Thread的run方法
class MyThread(Thread): """自定义线程开始提示""" def __init__(self): super().__init__() def run(self): print(f'{self.getName()}线程开始!') a = MyThread() a.start() # Thread-1线程开始!
一次性开启多个线程
-
自定义开启
from threading import Thread import time class MyThread(Thread): """自定义线程开始提示""" def __init__(self): super().__init__() def run(self): print(f'{self.getName()}线程开始!') time.sleep(2) # 模拟IO操作 start = time.time() pool = [] for i in range(10): t = MyThread() pool.append(t) t.start() # 将开启的线程实例添加进列表,统一添加跟随主线程, for i in pool: i.join() print(f"耗时{time.time()-start}") # Thread-1线程开始! # Thread-2线程开始! # Thread-3线程开始! # Thread-4线程开始! # Thread-5线程开始! # Thread-6线程开始! # Thread-7线程开始! # Thread-8线程开始! # Thread-9线程开始! # Thread-10线程开始! # 耗时2.0030786991119385
-
线程池
线程池要导入的东西很多
线程在被加入线程池就会自动执行
线程池引入了 Future对象,理解的不够,以后补充
submit(fn, *args, **kwargs)
:将fn
函数提交给线程池。*args
代表传给fn
函数的参数,*kwargs
代表以关键字参数的形式为fn
函数传入参数。map(func, *iterables, timeout=None, chunksize=1)
:该函数类似于全局函数map(func, *iterables)
,只是该函数将会启动多个线程,以异步方式立即对iterables
执行 map 处理。shutdown(wait=True)
:关闭线程池。
import time from concurrent.futures import ThreadPoolExecutor, wait, ALL_COMPLETED def mythread(name): print(f'开始线程{name}') time.sleep(2) # 创建一个包含2条线程的线程池 start = time.time() pool = ThreadPoolExecutor(max_workers=2) # 规定最大并发线程,超出会被阻塞,直到释放资源 test1 = pool.submit(mythread,'参数1') test2 = pool.submit(mythread,'参数2') # 返回一个 Future 对象 # 或者 # pool.map(mythread,['参数一','参数2']) # 等待所有线程执行完毕, wait([test1,test2], return_when=ALL_COMPLETED) print(test1.done()) # 判断 test1 线程是否执行完毕 print(f"耗时{time.time()-start}") # 关闭进程池 # 也可以使用 with 自动管理上下文 pool.shutdown()
Future 提供了如下方法:
cancel()
:取消该 Future 代表的线程任务。如果该任务正在执行,不可取消,则该方法返回 False;否则,程序会取消该任务,并返回 True。cancelled()
:返回 Future 代表的线程任务是否被成功取消。done()
:如果该 Future 代表的线程任务被成功取消或执行完成,则该方法返回 True。result(timeout=None)
:获取该 Future 代表的线程任务最后返回的结果。如果 Future 代表的线程任务还未完成,该方法将会阻塞当前线程,其中 timeout 参数指定最多阻塞多少秒。exception(timeout=None)
:获取该 Future 代表的线程任务所引发的异常。如果该任务成功完成,没有异常,则该方法返回 None。add_done_callback(func)
:为该 Future 代表的线程任务注册一个“回调函数”,当该任务成功完成时,程序会自动触发该func
函数。wait(fs, timeout=None, return_when=ALL_COMPLETED)
是主线程阻塞,fs 是一个Future对象的序列,当all_completed时,结束等待
-
-
线程间通信
线程间通信通常使用队列或队列来完成,使用线程队列有一个要注意的问题是,向队列中添加数据项时并不会复制此数据项,线程间通信实际上是在线程间传递对象引用。如果你担心对象的共享状态,那你最好只传递不可修改的数据结构(如:整型、字符串或者元组)或者一个对象的深拷贝
-
生产者-消费者模型
如果生产线程处理速度很快,而消费线程处理速度很慢,那么生产线程就必须等待消费线程处理完,才能继续生产数据。同样的道理,如果消费线程的处理能力大于生产线程,那么消费线程就必须等待生产线程
.get(block=True, timeout=None)
出队,允许阻塞,当阻塞超时队列依然为空时,报错.put(block=True, timeout=None)
入队,允许阻塞,当阻塞超时队列依然满时,报错.join()
在队列中有未完成任务时阻塞,队列为空时,正常执行.test_done()
执行一次put
会让未完成任务 +1 ,但是执行get
并不会让未完成任务 -1 ,需要使用task_done
让未完成任务 -1 ,否则join
就无法判断
队列为空时执行会报错:ValueError: task_done() called too many times
.quize()
获取当前队列数据量,.full()
判断当前队列是否已满,.empty()
判断当前队列是否为空,这三个方法都不可靠,因为在生产者-消费者模型中,一直在不停地入队出队,会导致不准确
import queue import threading import time def producter(): #生产者 for i in range(3): q.put(i) print(f'初始化添加{i}') for i in range(3,10): time.sleep(1.5) q.put(i) print(f'生产者添加{i}') def customer(): #消费者 while True: temp = q.get() q.task_done() print(f"消费者取出{temp}") time.sleep(1) q = queue.Queue() t1 = threading.Thread(target=producter) t2 = threading.Thread(target=customer) t1.start() t2.start() q.join() # 在队列中有未完成任务时阻塞,队列为空时,正常执行 print('这时队列空了')
-
队列种类
- FIFO 先进先出
- LIFO 后进先出
- Priority 优先队列
- deque 双端队列(即双向列表)
-
-
线程的高阶
-
锁与信号量
当两个线程同时争抢操作一个资源时,就会发生不可预知的事情(线程不安全),对这个资源加锁,同一时间只能有一个线程获取到这个资源的使用权
-
互斥锁
-
.acquire()
获得锁 -
.release()
释放锁 -
.locked()
检查是否获得锁
import time import threading,Lock def run(): lock.acquire() #修改数据前加锁 global num num +=1 lock.release() #修改完后释放 lock=Lock() num=0 threads = [] for i in range(1000): t=threading.Thread(target=run) t.start() threads.append(t) for t in threads: # 将开启的线程实例添加进列表,统一添加跟随主线程, t.join() print(num)
-
-
信号量
互斥锁,在同一时间只允许一把锁获取资源,而信号量允许同一时间多把多访问资源
-
.acquire()
获得锁 -
.release()
释放锁
from threading import BoundedSemaphore,Thread # BoundedSemaphore 内部维护了一个列表,每次 acquire 都计数加一 def run(): semaphore.acquire() #修改数据前加锁 global num num +=1 semaphore.release() #修改完后释放 semaphore = BoundedSemaphore(5)
- 主要用在数据库应用中,比如连接数据库的连接,限制同时连接的数量,如数据库连接池。这是线程不安全的。
-
-
-
事件 Event 参考
python线程的事件用于主线程控制其他线程的执行,事件是一个简单的线程同步对象,跟一把互斥锁很像
事件处理的机制:全局定义了一个“Flag”,当flag值为“False”,那么
event.wait()
就会阻塞,当flag值为“True”,那么event.wait()
便不再阻塞。.set()
将flag设为True ,就像是获取锁.clear()
将flag设为False,就像是释放锁.is_set()
检查当前flag的boolwait(timeout=None)
若 flag=True则继续运行,若为False则阻塞,知道flag=True
#利用Event类模拟红绿灯 import threading import time event = threading.Event() def lighter(): count = 0 event.set() #初始值为绿灯 while True: if 5 < count <=10 : event.clear() # 红灯,清除标志位 print("\33[41;1mred light is on...\033[0m") elif count > 10: event.set() # 绿灯,设置标志位 count = 0 else: print("\33[42;1mgreen light is on...\033[0m") time.sleep(1) count += 1 def car(name): while True: if event.is_set(): #判断是否设置了标志位 print("[%s] running..."%name) time.sleep(1) else: print("[%s] sees red light,waiting..."%name) event.wait() print("[%s] green light is on,start going..."%name) light = threading.Thread(target=lighter,) light.start() car = threading.Thread(target=car,args=("MINI",)) car.start()
-
参考
遗留的问题
- 线程既然是一个时间线,为什么还需要锁?
- python线程无法真正实现多线程,那为什么程序的运行速度确实加快了呢?
线程能提高速度,但是python由于GIL多的存在,并不能发挥出现在机器多核CPU的优势,大意了,一个线程就有太多要整理,还有进程,协程,异步IO,
💃 现在是2021.6.14,最近黑客要杀疯了啊,又勒索了麦当劳,这一连串的动作,会不会带动网络安全这个行业的进步呢,有点期待