Python并发编程之多线程
一、线程理论
1、进程是用来把资源集中到一起,而线程才是cpu上的执行单位。例如,一个个的火箭发射中心是进程,而火箭是线程。
2、多线程共享一个内存地址空间
3、线程开销比进程小、速度快、更容易创建撤销。线程的创建比进程要快10-100倍
4、计算密集型并不能获得性能上的增强,反倒由于不断切换速度不如并行。I/O密集型会加快程序执行的速度
5、在多cpu系统中,为了最大限度的利用多核,可以开启多个线程,比开进程开销要小的多。(这一条并不适用于python)
二、开启多线程的两种方式
# 方式一 import time import random from threading import Thread def piao(name): print('%s piaoing' % name) time.sleep(random.randrange(1, 5)) print('%s piao end' % name) if __name__ == '__main__': t1 = Thread(target=piao, args=('egon',)) t1.start() print('主线程') # 方式二 from threading import Thread import time class Mythread(Thread): def __init__(self, name): super().__init__() self.name = name def run(self): # 一定要叫这个名字,不能是别的 print('%s is running' % self.name) time.sleep(3) print('%s is done' % self.name) if __name__ == '__main__': t1 = Mythread('子线程1') t1.start() print('主线程')
import time from threading import Thread from multiprocessing import Process def piao(name): print('%s piaoing' % name) time.sleep(2) print('%s piap end' % name) if __name__ == '__main__': # p1 = Process(target=piao, args=('egon',)) # p1.start() # 主程序先运行 t1 = Thread(target=piao, args=('egon',)) t1.start() # 线程先运行 print('主')
2、同一进程内的多个线程共享该进程的地址空间(内存空间)
import time from threading import Thread from multiprocessing import Process n = 100 def task(): global n n = 0 if __name__ == '__main__': # p1 = Process(target=task) # p1.start() # p1.join() # print('主', n) # 主 100 t1 = Thread(target=task) t1.start() t1.join() print('主', n) # 主 0
3、pid
# 进程 from multiprocessing import Process, current_process import os def task(): print(current_process().pid) # 1217 # print('子进程PID<%s> 父进程PID<%s>' % (os.getpid(), os.getppid())) # 子进程PID<1207> 父进程PID<1206> if __name__ == '__main__': p1 = Process(target=task) p1.start() p1.join() # print('主', current_process().pid) # 主 1216 # print('主', os.getpid()) # 主 1206 # 线程 from threading import Thread import os def task(): print("子线程PID:%s" % os.getpid()) # 子线程PID:1255 if __name__ == '__main__': t1 = Thread(target=task) t1.start() print('主线程', os.getpid()) # 主线程 1255
介绍
Thread实例对象的方法 # isAlive(): 返回线程是否活动的。 # getName(): 返回线程名。 # setName(): 设置线程名。 threading模块提供的一些方法: # threading.currentThread(): 返回当前的线程变量。 # threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。 # threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
from threading import Thread, currentThread, active_count, enumerate import time def task(): print('%s is running' % currentThread().getName()) # 子线程 is running time.sleep(2) print('%s is done' % currentThread().getName()) # 儿子线程1 is done if __name__ == '__main__': t = Thread(target=task, name='子线程') t.start() # t.setName('儿子线程1') # t.join() # print(t.getName()) # 子线程 # currentThread().setName('主线程') # # print(t.is_alive()) # False # print(currentThread().getName()) # 主线程 # t.join() # print(active_count()) # 1 print(enumerate())
进程和线程都遵循**守护XX会等待主XX运行完毕后被销毁。 运行完毕这个词在进程和线程中分别有不同的含义。
1、对主进程来说,运行完毕指的是主进程代码运行完毕。
2、对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕。
from threading import Thread import time def sayhi(name): time.sleep(2) print('%s say hello' % name) if __name__ == '__main__': t = Thread(target=sayhi, args=('egon',)) # t.setDaemon(True) # 必须在t.start()之前设置 t.daemon = True # 和上面的效果一样 t.start() print('主线程') print(t.is_alive()) # True
六、互斥锁
和进程的互斥锁一样
from threading import Thread, Lock import time n = 100 def task(): global n mutex.acquire() temp = n time.sleep(0.1) n = temp - 1 mutex.release() if __name__ == '__main__': mutex = Lock() t_l = [] for i in range(100): t = Thread(target=task) t_l.append(t) t.start() for t in t_l: t.join() print('主', n)
七、GIL全局解释器锁
定义
结论
在Cpython解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势。
GIL和Lock的区别
GIL是解释器级别的,保护的是解释器级别的数据;Lock是保护用户自己开发的应用程序的数据的。
GIL与多线程
进程可以利用多核,但开销大;线程开销小,但却无法利用多核。这不是说多线程没有用武之地,对于计算密集型的任务当然是核数越多,速度越快,但是对于IO密集型的任务,不管你有多少核,碰到IO都需要等待,随意多核对I/O密集型没有什么用处,这时候就不如用线程快了。
# 计算密集型:用多进程 from multiprocessing import Process from threading import Thread import os, time def work(): res = 0 for i in range(100000000): res *= i if __name__ == '__main__': l = [] print(os.cpu_count()) # 本机为12核 start = time.time() for i in range(12): # p = Process(target=work) # 耗时9s多 p = Thread(target=work) # 耗时49s多 l.append(p) p.start() for p in l: p.join() stop = time.time() print('run time is %s' % (stop - start)) ## IO密集型:用多线程 from multiprocessing import Process from threading import Thread import os, time def work(): time.sleep(2) # print('===>') if __name__ == '__main__': l = [] print(os.cpu_count()) # 本机为12核 start = time.time() for i in range(400): # p = Process(target=work) # 耗时2.96s多,大部分时间耗费在创建进程上 p = Thread(target=work) # 耗时2.0X多 l.append(p) p.start() for p in l: p.join() stop = time.time() print('run time is %s' % (stop - start))
八、死锁与递归所
死锁
死锁指两个或两个以上的进程或线程在执行过程中,因争夺泽源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
from threading import Thread, Lock import time mutexA = Lock() mutexB = Lock() class MyThread(Thread): def run(self): self.f1() self.f2() def f1(self): mutexA.acquire() print('%s 拿到A锁' % self.name) mutexB.acquire() print('%s 拿到B锁' % self.name) mutexB.release() mutexA.release() def f2(self): mutexB.acquire() print('%s 拿到B锁' % self.name) time.sleep(0.1) mutexA.acquire() print('%s 拿到A锁' % self.name) mutexA.release() mutexB.release() if __name__ == '__main__': for i in range(10): t = MyThread() t.start()
递归锁是死锁的解决方法,在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁的RLock。
RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require,直到一个线程的acquire全部被require,其他的线程才能获得资源。与Lock不同的是,Lcck(互斥锁)只能被acquire一次,而递归锁可以被acquire多次。
# 递归锁:可以acquire多次,每acquire一次,计数器加1,只有计数为0时,其他线程才能抢到 from threading import Thread, RLock, Lock import time # mutexA = mutexB = Lock() # 自己把自己锁死了 mutexA = mutexB = RLock() class MyThread(Thread): def run(self): self.f1() self.f2() def f1(self): mutexA.acquire() print('%s 拿到A锁' % self.name) mutexB.acquire() print('%s 拿到B锁' % self.name) mutexB.release() mutexA.release() def f2(self): mutexB.acquire() print('%s 拿到B锁' % self.name) time.sleep(1) mutexA.acquire() print('%s 拿到A锁' % self.name) mutexA.release() mutexB.release() if __name__ == '__main__': for i in range(10): t = MyThread() t.start()
信号量
信号量也是一把锁,它管理内置的一个计数器。每当调用acquire()时内置计数器-1,每当调用release()时内置计数器+1。计数器不能小于0,当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。
同时只有5个线程可以获得semaphore,即可以限制最大连接数为5
from threading import Thread, Semaphore, currentThread import time import random sm = Semaphore(3) def task(): # sm.acquire() # print('%s in ' % currentThread().getName()) # time.sleep(random.randint(1, 3)) # sm.release() with sm: # 可以用上下文管理的形式来加锁 print('%s in ' % currentThread().getName()) time.sleep(random.randint(1, 3)) if __name__ == '__main__': for i in range(10): t = Thread(target=task) t.start()
Event
如果一个线程需要通过判断其他线程的状态来确认自己的下一步操作,这就需要使用到Event对象了。Event对象包含一个可由线程设置的信号标志,初始状态下是False。如果一个线程等待一个为假的Event对象,那么这个线程会一直被阻塞,知道这个标志为真为止。如果一个线程把Event的标志信号变成True,它将唤醒所有等待这个Event对象的线程。如果一个线程等待的一个Event对象是True,那么它将忽略这个事件,继续执行。
用法
event.isSet():返回event的状态值; event.wait():如果 event.isSet()==False将阻塞线程; event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度; event.clear():恢复event的状态值为False。
from threading import Thread, Event import time event = Event() # 用来实现线程之间的通话 def student(name): print('学生%s 正在听课' % name) event.wait(2) # 参数是超时时间,到时间了即使你没有发送信号,也会继续执行 print('学生%s 正在操场玩' % name) def teacher(name): print('老师%s 正在授课' % name) time.sleep(7) print('下课') event.set() # 这个执行了之后,event.wait()下面的代码才会执行。 if __name__ == '__main__': stu1 = Thread(target=student, args=('alex',)) stu2 = Thread(target=student, args=('wxx',)) stu3 = Thread(target=student, args=('yxx',)) t1 = Thread(target=teacher, args=('egon',)) stu1.start() stu2.start() stu3.start() t1.start() # 模仿网站验证码,错误次数超过三次就不让登陆了 from threading import Thread, Event, currentThread import time event = Event() def conn(): n = 0 while not event.is_set(): if n == 3: print('%s try too many times' % currentThread().getName()) return print('%s try %s' % (currentThread().getName(), n)) event.wait(0.5) n += 1 print('%s is connected' % currentThread().getName()) def check(): print('%s is checking' % currentThread().getName()) time.sleep(5) event.set() if __name__ == '__main__': for i in range(3): t = Thread(target=conn) t.start() t = Thread(target=check) t.start() # 自己实现 from threading import Thread, Event, currentThread import time event = Event() flag = False def conn(): n = 0 while not flag: if n == 3: print('%s try too many times' % currentThread().getName()) return print('%s try %s' % (currentThread().getName(), n)) time.sleep(0.5) n += 1 print('%s is connected' % currentThread().getName()) def check(): print('%s is checking' % currentThread().getName()) time.sleep(5) global flag flag = True event.set() if __name__ == '__main__': for i in range(3): t = Thread(target=conn) t.start() t = Thread(target=check) t.start()
定时器
指定N秒后执行操作
# 5秒后执行操作 from threading import Timer def task(name): print('hello %s' % name) t = Timer(5, task, args=('edward',)) t.start() # 每5秒刷新一次验证码 # from threading import Timer # # # def task(name): # print('hello %s' % name) # # # t = Timer(5, task, args=('edward',)) # t.start() import random from threading import Timer class Code: def __init__(self): self.make_cache() def make_cache(self, interval=5): self.cache = self.make_code() print(self.cache) self.t = Timer(interval, self.make_cache) self.t.start() def make_code(self, n=4): res = '' for i in range(n): s1 = str(random.randint(0, 9)) s2 = chr(random.randint(65, 90)) res += random.choice([s1, s2]) return res def check(self): while True: code = input('请输入你的验证码>>>').strip() if code.upper() == self.cache: print('验证码输入正确') self.t.cancel() break obj = Code() obj.check()
十、线程queue
queue is especially useful in threaded programming when information must be exchanged safely between multiple threads.
先进先出
import queue # 先进先出 -> 队列 q = queue.Queue(3) def a(): return 1 + 2 q.put('first') q.put(a()) q.put('thrid') # q.put(4, block=False) # 报错 queue.Full q.put_nowait(4) # q.put(4, block=True, timeout=3) # 3秒后报错 queue.Full print(q.get()) print(q.get()) print(q.get()) # print(q.get(block=False)) # queue.Empty q.get_nowait() # print(q.get_nowait()) # print(q.get(block=True, timeout=3))
后进先出
# 后进先出 -> 堆栈 # 想象你叠衣服,把衣服一件一件放起来,最后放的衣服拿走了,才能拿倒数第二放的衣服 # 队列就是吃了啦,先吃进去的先吐出来;堆栈就是吃了吐,后吃进去的先吐出来。 import queue q = queue.LifoQueue() q.put('first') q.put(3) q.put('thrid') print(q.get()) # thrid print(q.get()) # 3 print(q.get()) # first
# 优先级队列 -> 数字越小,优先级越高 import queue q = queue.PriorityQueue(3) q.put((10, 'one')) q.put((40, 'two')) q.put((30, 'three')) print(q.get()) # (10, 'one') print(q.get()) # (30, 'three') print(q.get()) # (40, 'two')
十一、进程池和线程池
进程池和线程池就是对进程数或线程数加以控制,让机器在一个自己可以承受的范围之内。
官网:https://docs.python.org/dev/library/concurrent.futures.html concurrent.futures模块提供了高度封装的异步调用接口 ThreadPoolExecutor:线程池,提供异步调用 ProcessPoolExecutor: 进程池,提供异步调用 Both implement the same interface, which is defined by the abstract Executor class.
基本方法
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) 回调函数
进程池
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor import os import time import random # 进程池 def task(name): print('name:%s pid:%s run' % (name, os.getpid())) time.sleep(random.randint(1, 3)) if __name__ == '__main__': pool = ProcessPoolExecutor(4) # 同时只能有4个任务运行。如果不指定,默认就是CPU的核数。 它们的pid不一样 for i in range(10): pool.submit(task, 'egon%s' % i) # 异步提交:提交完就立马就。 pool.shutdown(wait=True) # 把提交的入口关掉了,不能往里面提交任务了。默认wait=True,意思是等到进程池的任务结束,和join的功能一样。 print('主')
线程池
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor from threading import currentThread import os import time import random def task(): print('name:%s pid:%s run' % (currentThread().getName(), os.getpid())) time.sleep(random.randint(1, 3)) if __name__ == '__main__': pool = ThreadPoolExecutor(5) # pid一样 for i in range(10): pool.submit(task, ) # 异步提交:提交完就立马就。 pool.shutdown(wait=True) # 把提交的入口关掉了,不能往里面提交任务了。默认wait=True,意思是等到进程池的任务结束,和join的功能一样。 print('主')
map方法
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor import os,time,random def task(n): print('%s is runing' %os.getpid()) time.sleep(random.randint(1,3)) return n**2 if __name__ == '__main__': executor=ThreadPoolExecutor(max_workers=3) # for i in range(11): # future=executor.submit(task,i) executor.map(task,range(1,12)) #map取代了for+submit
异步提交与回调机制
进程池或线程池里的每个进程或线程都可以绑定一个函数,该函数在进程或线程的任务执行完毕后自动触发,并接收任务的返回值,该函数称为回调函数。
# 提交任务的两种方式 # 1、同步调用:提交完任务后,就在原地等待任务执行完毕,拿到结果,再执行下一行代码,导致程序串行执行。 from concurrent.futures import ThreadPoolExecutor import time import random def la(name): print("%s is laing" % name) time.sleep(random.randint(3, 5)) res = random.randint(7, 13) * '#' return {'name': name, 'res': res} def weigh(shit): name = shit['name'] size = len(shit['res']) print('%s 啦了 <%s>kg' % (name, size)) if __name__ == '__main__': pool = ThreadPoolExecutor(13) shit1 = pool.submit(la, 'alex').result() weigh(shit1) shit2 = pool.submit(la, 'wupeiqi').result() weigh(shit2) shit3 = pool.submit(la, 'yuanhao').result() weigh(shit3) # 2、异步调用,提交完任务后,不等待任务执行完毕 from concurrent.futures import ThreadPoolExecutor import time import random def la(name): print("%s is laing" % name) time.sleep(random.randint(3, 5)) res = random.randint(7, 13) * '#' return ({'name': name, 'res': res}) def weigh(shit): shit = shit.result() # 此刻拿到结果不会等,只有futures执行完了,weigh才会触发 name = shit['name'] size = len(shit['res']) print('%s 啦了 <%s>kg' % (name, size)) if __name__ == '__main__': pool = ThreadPoolExecutor(13) pool.submit(la, 'alex').add_done_callback(weigh) # 执行完后自动触发weigh,自动把futures对象,也就是pool.submit(la, 'alex')作为对象传进去(也就是上面的shit1) pool.submit(la, 'wupeiqi').add_done_callback(weigh) # 这是回调机制,实现了程序的解耦合。 pool.submit(la, 'yuanhao').add_done_callback(weigh) # 线程池练习 from concurrent.futures import ThreadPoolExecutor import requests import time def get(url): print('GET %s' % url) response = requests.get(url) time.sleep(3) return {'url': url, 'content': response.text} def parse(res): res = res.result() print('%s prase res is %s' % (res['url'], len(res['content']))) if __name__ == '__main__': urls = [ 'https://www.cnblogs.com/linhaifeng', 'https://www.python.org', 'https://www.openstack.org', ] pool = ThreadPoolExecutor(2) for url in urls: pool.submit(get, url).add_done_callback(parse)