python9-多线程
1,概念
- 线程:
- 能被操作系统调度(被CPU执行)的最小单位。(进程负责圈资源,线程负责执行)
- 同一个进程中的多个线程能够同时被多个CPU执行。(线程也可以利用多核)
- 同一个进程的多个线程是数据共享的。(必然存在数据不安全)(进程与线程的区别之一:进程是数据隔离的,线程是数据共享的)
- 线程的开启、关闭、切换的时间开销相比与进程要小的多。(一般情况下开启的进程数不会超过CPU个数的两倍,而开启的线程数可以很多)(因此绝大部分的并发都是由线程完成的)
- CPython中的多线程:
- 垃圾回收机制(gc):引用计数+分代回收。
- 全局解释器锁(GIL:global interpreter lock)的出现主要是为了使gc对不同线程的引用计数的变化记录的更加精准。但是GIL也导致同一个进程中的多个线程只能有一个真正被CPU执行(线程/数据安全、效率低)(但是I/O操作会释放GIL,这也意味着I/O密集型的程序可以从多线程中受益)。(CPython解释器下,GIL/全局解释器锁,导致同一进程中的多个线程不能利用多核)
- pypy的垃圾回收机制(gc)也使用GIL,同一进程下的多线程同样不能使用多核。
- jpython的垃圾回收机制则使得同一进程下的多线程可以使用多核。
2,threading模块
-
开启线程、传参、join、获取当前线程的对象
-
Thread, targert, args, start, join, ident(线程id),current_thread(获取当前线程的对象),enumerate(列表,存储了所有活着的线程对象,注意包括主线程),active_count(就是计算enumerate结果的长度,也就是所有活着的线程的数目),daemon(守护线程:守护线程随主线程的结束而结束,主线程等所有子线程结束后结束)(其他子线程结束——》主线程结束——》进程结束——》整个进程的所有资源被回收——》守护线程结束被回收(守护线程的结束在进程终止之后))
- 守护进程与守护线程的区别
- 守护进程会随着主进程的代码结束而结束,如果主进程代码结束以后还有其他的子进程运行,守护进程不守护。
- 守护线程随着主线程/进程的结束而结束,如果主线程结束之后还有其他子线程在运行(此时进程也不会结束),守护线程也守护。
- 为什么有这种区别?
- 父进程负责回收所有开的子进程的资源;但主线程没有必要回收子线程的资源(资源回收和线程没有关系);主线程的结束意味着进程的结束,进程的结束进程所有的资源回收
- 守护进程与守护线程的区别
-
线程没有terminate(线程不能从外部关闭,只能自己执行完后关闭)
-
以对象方式开启线程,线程的启动使用start(start是开启线程,而run只是执行方法,必须要先开启线程才能在线程中执行run方法)
'''例子一''' import time from threading import Thread def func(i): print(f'start {i}') time.sleep(2) print(f'end {i}') threadList = [] for i in range(10): th = Thread(target=func, args=(i, )) th.start() #开启 threadList.append(th) for thread in threadList: thread.join() #等全部结束了再打印end print('\033[31mend\033[0m')
'''例子二,对比进程和线程的开启速度,线程是真的快(时间开销差距大概在100倍左右)''' import time from threading import Thread from multiprocessing import Process def func(i): print(f'start {i}') time.sleep(2) print(f'end {i}') if __name__ == '__main__': for i in range(10): th = Thread(target=func, args=(i, )) th.start() #开启 # p = Process(target=func, args=(i, )) # p.start() print('\033[31mend\033[0m') #开启完所有线程就打印end '''进程几乎都是先打印代码最后的end,再打印start{i};而线程则是先打印start{i},再打印代码最后的end;这说明了两者在开启速度上的差别'''
'''例子三,线程也有id,此id与进程的pid无关''' import time import os from threading import Thread def func(i): print(f'start {i}') time.sleep(2) print(f'end {i}') if __name__ == '__main__': for i in range(10): th = Thread(target=func, args=(i, )) th.start() print(th.ident, os.getpid()) #打印线程id,以及进程pid print('\033[31mend\033[0m')
'''例子四,在函数中获取线程的id''' '''方法一:current_thread,特点:在哪个线程中执行,current_thread所获取的就是哪个线程所对应的对象''' import time import os from threading import Thread, current_thread def func(i): print(f'start {i}', current_thread(), current_thread().ident) time.sleep(2) print(f'end {i}') if __name__ == '__main__': for i in range(10): th = Thread(target=func, args=(i, )) th.start() print(th.ident) #打印线程id print('\033[31mend\033[0m')
'''例子五,enumerate 与 active_count''' import time import os from threading import Thread, current_thread, enumerate, active_count def func(i): print(f'start {i}') time.sleep(2) print(f'end {i}') if __name__ == '__main__': for i in range(10): th = Thread(target=func, args=(i, )) th.start() print(enumerate()) #列出所有的线程 print(len(enumerate()), active_count()) print('\033[31mend\033[0m')
'''例子六,面向对象的方式开启线程''' import time from threading import Thread, enumerate class myThread(Thread): def __init__(self, args): super(myThread, self).__init__() self.args = args def run(self): print(f'start {self.args}') time.sleep(2) print(f'end {self.args}') if __name__ == '__main__': for i in range(10): th = myThread((i, 2)) th.start() #注意这里是start(start是开启线程,而run只是执行方法,必须要先开启线程才能在线程中执行run方法) print(enumerate()) print(len(enumerate())) print('\033[31mend\033[0m')
'''例子七,线程之间的数据是共享的''' from threading import Thread n = 100 def func(): global n n -= 1 tList = [] for i in range(100): th = Thread(target=func) th.start() tList.append(th) for t in tList: t.join() print(n) #最后的结果是0,s
'''例子八,守护线程''' ''' ============================= 主线程会等待子线程的结束再结束(主线程结束主进程就会结束,而之后回收资源) ============================= ''' import time from threading import Thread def son(): while True: print('in son') time.sleep(1) Thread(target=son).start() ''' ============================= 1,线程开启的速度很快,从flagA到flagB的打印几乎瞬间完成 2,守护线程守护的是主线程,随主线程的结束而结束 ============================= ''' import time from threading import Thread def son(): while True: print('in son') time.sleep(1) print('flagA') th = Thread(target=son) th.daemon = True th.start() print('flagB')
3,线程锁(互斥锁)
-
+=, -=, *=, /=, while, if 都是数据不安全的;列表中或者字典中的方法操作全局变量时是数据安全的。
-
解决数据不安全的问题就是加锁
-
如何避免数据不安全:不要操作全局变量、不要在类里操作静态变量(单例模式跑不掉)
from threading import Thread, Lock n = 0 def increase(lock): for i in range(2200000): global n with lock: n += 1 def reduce(lock): for i in range(2200000): global n with lock: n -= 1 lock = Lock() t1 = Thread(target=increase, args=(lock, )) t2 = Thread(target=reduce, args=(lock, )) t1.start() t2.start() t1.join() t2.join() print(n)
'''单例模式''' '''=====================================不安全版本=====================================''' import time class A: __instance = None def __new__(cls, *args, **kwargs): if not cls.__instance: time.sleep(0.0000001) #这里强制CPU轮转,单例模式出现了多个实例(此处只要有CPU轮转,都可能会有多个实例) cls.__instance = super().__new__(cls) return cls.__instance def func(): a = A() print(a) from threading import Thread for i in range(10): Thread(target=func).start() '''=====================================安全版本=====================================''' import time from threading import Thread, Lock class A: __instance = None __lock = Lock() def __new__(cls, *args, **kwargs): with cls.__lock: if not cls.__instance: time.sleep(0.0000001) cls.__instance = super().__new__(cls) return cls.__instance def func(): a = A() print(a) for i in range(10): Thread(target=func).start()
4,递归锁和死锁现象
-
死锁
-
产生原因:最主要的原因是有多把锁,并且在多个线程中进行交叉使用;死锁现象和锁是互斥锁还是递归锁无关;但是如果可以只有一把锁就能够解决死锁问题(比如将所有的互斥锁都改为一把递归锁)。
-
from threading import Thread, Lock import time noodleLock = Lock() forkLock = Lock() def eat1(name): noodleLock.acquire() print(name, '抢到面了') forkLock.acquire() print(name, '抢到叉子了') print(name, '吃面') time.sleep(0.1) forkLock.release() print(name, '放下叉子') noodleLock.release() print(name, '放下面') def eat2(name): forkLock.acquire() print(name, '抢到叉子了') noodleLock.acquire() print(name, '抢到面了') print(name, '吃面') time.sleep(0.1) forkLock.release() print(name, '放下叉子') noodleLock.release() print(name, '放下面') Thread(target=eat1, args=('a')).start() Thread(target=eat2, args=('b')).start() Thread(target=eat1, args=('c')).start() Thread(target=eat2, args=('d')).start()
-
-
递归锁
-
可以多次acquire;acquire几次就要release几次
-
递归锁的效率比互斥锁效率低
-
from threading import Thread, RLock import time noodleLock = forkLock = RLock() def eat1(name): noodleLock.acquire() print(name, '抢到面了') forkLock.acquire() print(name, '抢到叉子了') print(name, '吃面') time.sleep(0.1) forkLock.release() print(name, '放下叉子') noodleLock.release() print(name, '放下面') def eat2(name): forkLock.acquire() print(name, '抢到叉子了') noodleLock.acquire() print(name, '抢到面了') print(name, '吃面') time.sleep(0.1) forkLock.release() print(name, '放下叉子') noodleLock.release() print(name, '放下面') Thread(target=eat1, args=('a')).start() Thread(target=eat2, args=('b')).start() Thread(target=eat1, args=('c')).start() Thread(target=eat2, args=('d')).start()
-
5,队列(queue:Queue/LifoQueue/PriorityQueue)
-
线程之间数据安全的容器
-
用法
q = queue.Queue(n) #FIFO,先进先出(队列);用于任务处理 q.get() q.get_nowait() q.put() q.put_nowait() #会丢失数据 try: q.get_nowait() except queue.Empty: pass q = queue.LifoQueue #LIFO,后进先出(栈);用于算法 '''用法与队列类似''' priq = queue.PriorityQueue #优先级队列;用于服务、运维等 priq.put((x, y)) #根据x的ascii码排序 priq.get()
6,池 concurrent.futures
-
线程池的开启过程:
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor tp = ThreadPoolExecutor(20) #启线程池,线程的数目一般是CPU的核心数*5 ret = tp.submit(func) #向线程池中提交任务 ret.result() #获取结果 '''=============回调函数=============''' ret.add_done_callback(lambda x: print(f'\033[33m{x}{x.result()}\033[0m'))
-
概念:
- 没有池——》有多少任务就开多少个进程/线程
- 有了池——》程序开始的时候还没提交任务就先创建几个进程/线程放在一个池子里,这就是池
-
为什么要用池?
- 如果先开好进程/线程,有任务之后就可以直接使用池中的进程/线程;
- 开好的进程/线程会一直存在在池中,可以被多个任务反复使用,极大的减少了开启、关闭、调度线程/进程的时间开销;
- 池中的进程/线程个数意味着操作系统需要调度的任务个数,控制池中的进程/线程个数有利于提高操作系统的效率,减轻操作系统的负担。
-
模块发展史
- threading 线程模块,没有池
- multiprocessing 仿照threading写的进程模块,有进程池
- concurrent.futures 进程池和线程池都有,能够用相似的方式开启,使用。
-
用例:
'''1,启动池''' '''====================================线程池====================================''' import time import random from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor from threading import current_thread def func(): print(current_thread().ident) #观察当前线程的编号 # time.sleep(1) #强制轮转 # time.sleep(random.randint(1, 4)) tp = ThreadPoolExecutor(20) #启线程池,线程的数目一般是CPU的核心数*5 for i in range(40): tp.submit(func) #向线程池中提交任务 '''====================================进程池====================================''' import time import random import os from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor from threading import current_thread def func(a, b): print(os.getpid(), current_thread().ident, 'start:', a,b) #观察当前线程的编号 time.sleep(1) #强制轮转 # time.sleep(random.randint(1, 4)) print(os.getpid(), current_thread().ident, 'end:', a, b) # 观察当前线程的编号 tp = ProcessPoolExecutor(5) #启进程池,进程的数目一般是CPU的核心数+1 if __name__ == '__main__': for i in range(40): # tp.submit(func, i, i+1) #向线程池中提交任务,传参 tp.submit(func, a=i, b=i+1) #向线程池中提交任务,传参
'''2,传参''' import time import random from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor from threading import current_thread def func(a, b): print(current_thread().ident, 'start:', a,b) #观察当前线程的编号 # time.sleep(1) #强制轮转 # time.sleep(random.randint(1, 4)) tp = ThreadPoolExecutor(20) #启线程池,线程的数目一般是CPU的核心数*5 for i in range(40): # tp.submit(func, i, i+1) #向线程池中提交任务,传参 tp.submit(func, a=i, b=i+1) #向线程池中提交任务,传参
'''3,获取任务结果''' '''====================================同步====================================''' import time import random import os from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor from threading import current_thread def func(a, b): print(os.getpid(), current_thread().ident, 'start:', a,b) #观察当前线程的编号 time.sleep(1) #强制轮转 # time.sleep(random.randint(1, 4)) print(os.getpid(), current_thread().ident, 'end:', a, b) # 观察当前线程的编号 return a*b tp = ProcessPoolExecutor(5) #启进程池,进程的数目一般是CPU的核心数+1 if __name__ == '__main__': for i in range(40): ret = tp.submit(func, i, i+1) #向线程池中提交任务,传参 print(f'\033[33m{ret}\033[0m') #获取结果,一个future对象 print(f'\033[33m{ret.result()}\033[0m') #这里print阻塞必须要等函数的执行结果,变成同步了 '''====================================异步====================================''' import time import random import os from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor from threading import current_thread def func(a, b): print(os.getpid(), current_thread().ident, 'start:', a,b) #观察当前线程的编号 # time.sleep(1) #强制轮转 time.sleep(random.randint(1, 4)) print(os.getpid(), current_thread().ident, 'end:', a, b) # 观察当前线程的编号 return a*b tp = ProcessPoolExecutor(5) #启进程池,进程的数目一般是CPU的核心数+1 if __name__ == '__main__': resList = [] for i in range(40): ret = tp.submit(func, i, i+1) #向线程池中提交任务,传参 resList.append(ret) for res in resList: #列表内部是同步的,如果其中一个res没有返回会一直等只到获取结果;并且获取的结果与放入的顺序是一致的 print(f'\033[33m{res.result()}\033[0m')
'''4,map''' import time import random import os from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor from threading import current_thread def func(dataIn): a, b = dataIn print(os.getpid(), current_thread().ident, 'start:', a,b) #观察当前线程的编号 # time.sleep(1) #强制轮转 time.sleep(random.randint(1, 4)) print(os.getpid(), current_thread().ident, 'end:', a, b) # 观察当前线程的编号 return a*b tp = ProcessPoolExecutor(5) #启进程池,进程的数目一般是CPU的核心数+1 if __name__ == '__main__': ret = tp.map(func, ((i, i+1) for i in range(20))) #只适合传递简单的参数并且必须是可迭代对象 for res in ret: print(f'\033[33m{res}\033[0m')
'''5,回调函数add_done_callback:异步阻塞''' import time import random import os from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor from threading import current_thread def func(dataIn): a, b = dataIn print(current_thread().ident, 'start:', a,b) #观察当前线程的编号 # time.sleep(1) #强制轮转 time.sleep(random.randint(1, 4)) print(current_thread().ident, 'end:', a, b) # 观察当前线程的编号 return a*b tp = ThreadPoolExecutor(5) #启进程池,进程的数目一般是CPU的核心数+1 if __name__ == '__main__': future_l = [] for i in range(20): ret = tp.submit(func, dataIn=(i, i+1)) ret.add_done_callback(lambda x: print(f'\033[33m{x}{x.result()}\033[0m'))#在ret执行结束的瞬间就会将 ret的结果传给通过add_done_callback绑定的func,并执行func '''add_done_callback回调函数,异步阻塞。给ret对象绑定一个回调函数,等待ret对应的任务有了结果之后立即调用 传入的函数就可以对结果进行立即处理,而不用按照顺序接受ret在进行处理。这样一来:计算func和获取结果的过程都是异步 的。'''
'''6,使用queue模拟回调函数''' import queue import time import random from threading import Thread def func(q,i): print('start', i) time.sleep(random.randint(1, 10)) print('end', i) q.put(i*(i+1)) #return i*(i+1) q = queue.Queue() for i in range(20): Thread(target=func, args=(q, i)).start() for i in range(20): Thread(target=lambda x: print(x.get()), args=(q,)).start()
行动是治愈恐惧的良药,而犹豫拖延将不断滋养恐惧。