并发编程
进程、线程
进程、线程是操作系统中的概念
进程是操作系统的一个独立单位,进程、线程都是操作系统中的基本概念,也就是说进程和线程都是操作系统层面的东西,进程和线程都是操作系统来调度的,不是程序员自己操控的。(与后续所学的协程做对比,协程不是操作系统的概念,是程序员层面的,由程序员自己调用,不是操作系统调用的,操作系统中没有这个概念)
进程
进程的用处:用来做任务的,听歌、看视频等。
进程简单来说就是一个过程,而线程是真正干活的。
进程和线程的区别
进程的资源的消耗>线程>协程
1、程序是死的东西,无任何生命意义。
2、进程是有生命周期的,当一个程序结束,进程就不存在了。
进程中要有线程,进程中如果没有线程是无意义的,一个进程中可以有多个线程,一个进程中至少有一个线程。
CPU工作机制(来回切换做到的)
当cpu遇到I/O系统的时候,会剥夺CPU的执行权限
当遇到的任务需要占用大量时间时,也会剥夺执行权限
I/O:
I/O密集型:
遇到阻塞,不会占用大量CPU资源,需要等待
计算密集型:
没有遇到阻塞,但是需要占用大量CPU资源,也不需要等待
操作系统的调度算法
1、先来先服务调度算法
2、短作业优先调度算法
3、时间片轮转法(按时间)
4、多级反馈队列(以上三种不适用就用这种)
操作系统会自行选择哪一种算法。
进程的并发和并发
并行:同一时刻,同时执行。
想要同一时刻同时执行,要cpu有多个处理器(核)
单核的cpu无法并行
并发:在一段时间内,看起来是同时执行任务,不是同一时刻。
同步异步阻塞非阻塞
同步异步
关注的是消息通信机制。
同步:调用者主动等待调用结果
异步:调用发出后,不会立即得到结果,而是调用发出发出后,被调用者通过状态通知来告诉调用者。
举个例子:
例如买书,
如果是同步通讯机制:问书店老板是否有这本书,老板说查一下,稍等会,然后购买者就等,等到老板返回结果。
如果是异步通讯机制:问老板是否有这本书,老板说找到后给购买者打电话。但是其他人在此时也可以询问老板是否有书。
更多关注老板
阻塞非阻塞
关注的是程序在等待调用结果时的状态。
堵塞:调用结果返回之前,线程会被挂起,直到得到结果,才会返回。
非堵塞:不立刻得到结果之前,该调用不会阻塞当前线程。
例如:阻塞时调用:在书店老板找到这本书之前,会一直挂起,直到找到这本书。
非阻塞调用:在问书店老板是否有在这本书,不管有没有,都会去一边,偶尔返回看老板是否有给结果。(这个期间别人用不了这个窗口)
更多关注自己
同步阻塞,同步非阻塞,异步阻塞,异步非阻塞
开启进程
开启进程是为了完成任务的
使用的模块:multiprocess
在windows系统,开启进程必须写在__main__中,其他系统不用加。
from multiprocessing import Process def task(): with open('a.txt','w',encoding='utf8') as f: f.write('helloworld') if __name__ == '__main__': p = Process(target=task) #还没开始,只是实例化一个对象 p.start() #调用这个方法开始进程
group=None, target=None, name=None, args=(), kwargs={},*, daemon=None
每一个进程都有:进程名,进程id等属性
name: 代表的是进程的名字
args=(), 传参数的
kwargs={},
daemon:守护进程,主进程结束,子进程也跟着结束。
关于子进程和主进程:他们并不是包含关系,只是主进程带动了子进程而已,而在下列例子中task是子进程,调用task是主进程,而py文件又是前一个的主进程,当py文件是子进程的时候,pycharm是主进程。
简单的举例 :
from multiprocessing import Process def task(name,age): print('hi') print('name:',name) print('age:',age) if __name__ == '__main__': # group=None, target=None, name=None, args=(), kwargs={},*, daemon=None p = Process(target=task,name='lin',args=('rui',),kwargs={'age':18}) p.start() #查看进程名字 print(p.name) ''' lin #先走主进程,因为子进程开启需要时间,所以在主进程走完才会走子进程 hi name: rui age: 18 '''
加入daemon(守护进程)
from multiprocessing import Process def task(name,age): print('hi') print('name:',name) print('age:',age) if __name__ == '__main__': # group=None, target=None, name=None, args=(), kwargs={},*, daemon=None p = Process(target=task,name='lin',args=('rui',),kwargs={'age':18}) p.daemon = True ##守护进程的作用:主进程结束,子进程也结束,并且守护进程放在start之前,默认是False p.start() #查看进程名字 print(p.name)
方法介绍
join 等待子进程结束,再执行主进程
from multiprocessing import Process def task(name,age): print('hi') print('name:',name) print('age:',age) if __name__ == '__main__': # group=None, target=None, name=None, args=(), kwargs={},*, daemon=None p = Process(target=task,name='lin',args=('rui',),kwargs={'age':18}) # p.daemon = True #守护进程的作用:主进程结束,子进程也结束,并且守护进程放在start之前 p.start() p.join() #先执行子进程再执行主进程 #查看进程名字 print(p.name) ''' 结果: 因为加了join所以先执行子进程,子进程走完才走主进程 hi name: rui age: 18 lin '''
from multiprocessing import Process def task(name,age): print('hi') print('name:',name) print('age:',age) if __name__ == '__main__': # group=None, target=None, name=None, args=(), kwargs={},*, daemon=None p = Process(target=task,name='lin',args=('rui',),kwargs={'age':18}) p.start() p.terminate() #杀死进程 import time #查看进程名字 time.sleep(1) print(p.is_alive()) #判断进程是否存在 print(p.name) #杀死的只是task这个子进程,主进程还存活,可以运行 ''' 结果: False lin '''
join 执行完子进程,子进程完毕再执行主进程
(例子在多进程循环中展示)
如何开启多进程
开启进程的目的是为了提高效率,但是资源消耗大了。
不能无限开启多进程,非常容易造成内存奔溃。
开启多进程之后,并发形式执行。
因为进程需要时间,所以进程开启需要时间,所以执行无顺序。
关于join:
当join放进循环,会串行执行,时间长。
# 加入循环,多次传值 from multiprocessing import Process import time def task(i): print('hi:',i) time.sleep(1) if __name__ == '__main__': start = time.time() for i in range(5): # group=None, target=None, name=None, args=(), kwargs={},*, daemon=None p = Process(target=task,args=(i,)) p.start() p.join() print('hihihi') print('time:',time.time()-start) ''' 结果: hi: 0 hi: 1 hi: 2 hi: 3 hi: 4 hihihi time: 5.777955770492554 虽然有了顺序,但是时间会变得很慢,进程就无意义 '''
当join存放在子进程分别执行完,就会并行执行,时间短。
# 加入循环,多次传值 from multiprocessing import Process import time def task(i): print('hi:',i) time.sleep(1) if __name__ == '__main__': l = [] start_time = time.time() for i in range(5): p = Process(target=task,args=(i,)) p.start() l.append(p) for j in l: j.join() print('123') print(time.time() - start_time) ''' 结果: hi: 0 hi: 1 hi: 2 hi: 3 hi: 4 123 1.186903953552246 '''
进程锁
锁:为了保证数据安全。
当开启多个进程的时候,去执行同一个任务,有可能会发生任务错乱问题,而加锁来保证数据不发生错乱问题。
lock.acquire() 上锁
lock.release()释放锁
为了防止调用导致的调用进程时间短,程序要按顺序并且绑在一起执行。
from multiprocessing import Process from multiprocessing import Lock def task(i,lock): lock.acquire() #上锁 print('%s进程来了'%i) print('%s进程走了'%i) lock.release() #释放锁 if __name__ == '__main__': lock = Lock() for i in range(5): p = Process(target=task,args=(i,lock)) p.start() ''' 结果: 1进程来了 1进程走了 0进程来了 0进程走了 2进程来了 2进程走了 3进程来了 3进程走了 4进程来了 4进程走了 '''
进程间的通信(IPC机制)
进程是可以开启多个的,进程和进程之间相互独立,互补影响,进程是操作系统一个独立单元。
一个进程崩溃了,其他进程不受影响。
一个进程中不能修改另一个进程的数据,言外之意是进程之间的数据是隔离的,互不影响
想要在一个进程中使用一个进程的数据,需要让进程之间通信。
如何查看进程的id号
进程都有几个属性:进程名、进程id号(pid)
每个进程都有一个唯一的id号,通过这个id号就能找到这个进程。
os.getpid()这一句代码写在哪个进程下,输出的就是这个进程的进程号。
os.getppid() 这个代码写在哪里,就代表的是这个进程的父进程id号。
from multiprocessing import Process import os,time #进程号 def task(): print('task这个子进程号为:',os.getpid()) #指的是自己 print('主进程号为:',os.getppid()) #指的是主进程 if __name__ == '__main__': p = Process(target=task) p.start() print('子进程号1为:',p.pid) #指的是task的进程 print('主进程号1为:',os.getpid()) #指的是自己 time.sleep(5) ''' 运行结果为: 子进程号1为: 13080 主进程号1为: 15168 task这个子进程号为: 13080 主进程号为: 15168 '''
队列的使用
有个内置类Queue
常见的数据结构:
链表、单链表、多链表、循环链表、栈、队列、树、二叉树、平衡二叉树、红黑树、b树、b+树、b-树、图等。
队列:先进先出
正常数据满了不会报错,会等待
1、参数block = False :当队列满了,放不进去报错
2、timeout 放数据,等待秒数,放不进去报错
以上两个不会同时用
实例化的对象.put_nowait() 放数据的时候放不进去,直接报错。 (也就相当于block=False)
q = Queue(3) 例如默认大小为3
q.empty()判断是否为空
q,full()判断是否满了
q.qsize 队列大小,看队列中剩余多少数据,结果不太准确
1、关于Queue的put和get怎么用
from multiprocessing import Queue if __name__ == '__main__': q = Queue(3) q.put('hihihi') q.put('hihihi') q.put('hihihi') q.put('hihihi') #当put大于q的大小,程序就会卡住无法运行 print(q.get()) print(q.get()) print(q.get()) #放入多少数据取决于q的大小,并且一次get只能取一个put
2、关于Queue的方法怎么用
from multiprocessing import Queue if __name__ == '__main__': q = Queue(3) q.put('hihihi') q.put('hihihi') q.put('hihihi') # q.put('hihihi',block=False) #当put的数量比get多,block=false就会直接报错 # q.put('hihihi',timeout=5) #当put的数量比get多,等待5秒报错 # q.put_nowait('hihihi') #当put的数量比get多,不会等待直接报错,相当于block=false print(q.get()) print(q.get()) print(q.get())
解决进程之间的隔离问题
放进队列Queue中
from multiprocessing import Process,Queue def desk(q): #放入数据 q.put('你好你好') q.put('你好你好') q.put('你好你好') if __name__ == '__main__': q = Queue(5) p = Process(target=desk,args=(q,)) p.start() print(q.get()) print(q.get()) print(q.get()) #你好你好
生产者和消费者模型
在并发编程中,使用生产者消费者模型能够解决绝大多数并发问题,该模型通过平衡生产线程和消费线程的工作能力来提高程序的整体出路数据的速度。
1、最简单版本生产者消费者模型(最大的问题是消费者在取得时候,不知道生产者生产了多少)
#消费者生产者模型 from multiprocessing import Process,Queue def consumer(q): while True: print(q.get()) def producer(q): #放入数据 for i in range(5): q.put('放入第%s个'%i) if __name__ == '__main__': q = Queue(5) c = Process(target=consumer,args=(q,)) c.start() p = Process(target=producer,args=(q,)) p.start()
2、加入None解决消费者取生产者生产东西时,取完卡住的问题
#消费者生产者模型 from multiprocessing import Process,Queue def consumer(q): while True: res = q.get() print(res) if res is None: break def producer(q): #放入数据 for i in range(5): q.put('放入第%s个'%i) q.put(None) if __name__ == '__main__': q = Queue(10) c = Process(target=producer,args=(q,)) c.start() p = Process(target=consumer,args=(q,)) p.start()
3、多个生产者和多个消费者(少消费者,多生产者)
from multiprocessing import Process, Queue import os def producer(q, name, food): """让生产者生产10个包子""" for i in range(4): """生产的包子放在哪里? 队列里""" q.put("生产者:%s,生产了第%s个 %s" % (name, i, food)) print('%s生产了' % (os.getpid())) import time time.sleep(1) def consumer(q): while True: res = q.get() # if q.empty(): # break if res is None: break print('%s 吃 %s' % (os.getpid(), res)) # print(res) if __name__ == '__main__': q = Queue(20) # 4个生产者 p1 = Process(target=producer, args=(q, 'kevin', '包子')) p2 = Process(target=producer, args=(q, 'jason', '豆浆')) p3 = Process(target=producer, args=(q, 'tank', '面包')) p4 = Process(target=producer, args=(q, 'oscar', '豆汁')) p1.start() p2.start() p3.start() p4.start() # 两个消费者 p5 = Process(target=consumer, args=(q,)) p6 = Process(target=consumer, args=(q,)) p5.start() p6.start() p1.join() p2.join() p3.join() p4.join() """放的None的数量超过消费者的数量就可以正常结束程序""" q.put(None) q.put(None)
4、少生产者,多消费者
from multiprocessing import Process, Queue import os def producer(q, name, food): """让生产者生产10个包子""" for i in range(4): """生产的包子放在哪里? 队列里""" q.put("生产者:%s,生产了第%s个 %s" % (name, i, food)) print('%s生产了' % (os.getpid())) import time time.sleep(1) def consumer(q): while True: res = q.get() # if q.empty(): # break if res is None: break print('%s 吃 %s' % (os.getpid(), res)) # print(res) if __name__ == '__main__': q = Queue(20) # 1个生产者 p1 = Process(target=producer, args=(q, 'kevin', '包子')) p1.start() # 两个消费者 p5 = Process(target=consumer, args=(q,)) p6 = Process(target=consumer, args=(q,)) p5.start() p6.start() p1.join() """放的None的数量超过消费者的数量就可以正常结束程序""" q.put(None) q.put(None)
线程理论
进程是一个任务的执行过程,在这个过程中实际做事的是线程,线程是开在进程里面的,需要现有进程,再有线程,一个进程中至少有一个线程,可以有多个线程。
进程是资源分配的基本单位,线程是cpu执行的最小单位。
进程和线程都是操作系统调度的,协程是程序员调度的。
资源消耗最多的是进程》》》线程的资源》》》协程的资源
关于开启线程
开启一个简单的线程
from threading import Thread def desk(): pass if __name__ == '__main__': #线程可以不加,但是最好还是加上 '''group=None, target=None, name=None, args=(), kwargs=None, *, daemon=None):''' #和进程一样 t = Thread(target=desk) t.start()
关于线程的方法
1、os.getpid() 进程中每个进程的pid都不同,但是在线程中每个线程都和主进程pid一样。
def work(): print('hello',os.getpid()) if __name__ == '__main__': # part1:在主进程下开启多个线程,每个线程都跟主进程的pid一样 t1=Thread(target=work) t2=Thread(target=work) t1.start() t2.start() print('主线程/主进程pid',os.getpid()) # part2:开多个进程,每个进程都有不同的pid p1=Process(target=work) p2=Process(target=work) p1.start() p2.start() print('主线程/主进程pid',os.getpid())
2、其他方法
isAlive():返回线程是否活动的。但是这个不怎么用了,现在用is_alive()
threading.currentThread():返回当前的线程变量。
threading.enumerate():返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
threading.activeCount():返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
from threading import Thread def desk(): print('hihihi') if __name__ == '__main__': t = Thread(target=desk) t.start() print(t.is_alive()) #True t.setName('lin') #修改进程名 print(t.getName()) #获得进程名,默认为Thread-1,可以修改 print(t.getName()) #获得进程名,默认为Thread-1,可以修改 print(t.name) # 获得进程名,默认为Thread-1,可以修改 (这三个效果一致)
如何开启多线程
和进程一样,但是因为线程使用资源较少,所以会运行完子进程才运行主线程。
from threading import Thread def desk(): print('hihihi') # def list(): # pass if __name__ == '__main__': l =[] for i in range(5): d = Thread(target=desk) d.start() l.append(d) # l = Thread(target=list) # l.start() for j in l: j.join() print(123)
进程和线程的比较
1、进程的开销远远大于线程的开销
2、进程之间的数据是隔离的,在同一个进程下的线程之间的数据是共享的
GIL全局解释器锁
python在设计之初就考虑到要在主循环中,同时只有一个线程执行。
虽然python解释器中可以运行多个线程,但是任意时刻只有一个线程在解释器中运行。
对于 python虚拟机的访问由全局解释器锁GIL来控制,正是这个锁能保证同一时刻只有一个线程在运行。
1、python代码运行在解释器之上,有解释器来翻译执行。
2、python解释器的种类有:CPython、IPython、PyPy、Jython
3、GIL锁存在于CPython解释器中。
4、市面上95%都使用的是CPython解释器。
5、起一个垃圾回收线程,再起一个正常执行代码的线程,当垃圾回收线程还没回收完毕,其他线程有可能会抢夺资源,这种情况在python设计之处就不允许的。
6、python在设计之处,就在python解释器之上加了一把锁GIL锁,加这个锁的目的是:同一时刻只能有一个线程执行,不能同时有多个线程执行,如果开了多个线程,那么,线程要想执行权限,必须先拿到这把锁。
7、同一时刻多个线程只能有一个线程在运行,其他线程都处于等待状态。
如何理解:
1、python有GIL锁的原因,同一个进程下多个线程实际上同一个时刻只有一个线程在运行。
2、只有在python上开进程用的多,其他语言一般不开多进程,开线程就够了。
3、cpython解释器开多线程不能利用多核优势,只有开多进程才能利用多核优势,其他语言不存在这个问题。
4、8核cpu电脑,充分利用起八核,至少要八个进程,并且8个进程全是计算,计算机cpu使用率才是100%。
5、如果不存在GIL锁,一个进程下,开启8个线程,他就能充分利用cpu,跑满。
6、cpython解释器中好多代码都是基于GIL锁机制写起来的,改不了。
7、cpython解释器:io密集型使用多线程,计算密集型使用多进程。
I/O密集型:遇到I/O操作会切换cpu,假设开了8个线程,8个线程都有I/O操作,I/O操作不消耗cpu,一段时间内看上去,其实8个线程都执行了,选多线程好些。
计算密集型,消耗cpu,如果开了8个线程,第一个线程会一直占用cpu,而不会调度到其他线程执行,所以我们开8个进程,每个进程都有一个线程,8个进程的线程会被8个cpu执行,从而效率高。
互斥锁
多个线程去操作同一个数据,会出现并发安全问题,通过加锁解决。
1、多个线程抢占资源现象。
from threading import Thread import os,time def work(): global n temp=n time.sleep(1) #因为这里停顿了,线程全部卡在这里,当一秒后下面n的值还没来得及全局改变,都并行执行,所以结果是99 n=temp-1 # print(n) if __name__ == '__main__': n=100 l=[] for i in range(3): p=Thread(target=work) l.append(p) p.start() # print(n,1) for p in l: p.join() print('这个',n) #结果可能为99
2、加锁改变这个问题
from threading import Thread ,Lock import os,time def work(): lock.acquire() global n temp=n time.sleep(0.1) n=temp-1 # print(n) lock.release() if __name__ == '__main__': n = 100 lock = Lock() l=[] for i in range(3): p=Thread(target=work) l.append(p) p.start() # print(n,1) for p in l: p.join() print('这个',n) #结果97
线程队列
进程之间的数据是隔离的,所以用队列实现进程的通信。
线程之间数据是共享的,用队列是为了保证数据安全。
队列的底层是:管道+锁,锁是为了保证数据安全
线程内部使用队列,是为了保证线程的数据安全
进程Queue用于父进程和子进程(或同一个父进程中多个子进程)间数据传递。
python自己多个进程间交换数据或者与其他语言进程queue就无能为力。
que.Queue的缺点是他的实现涉及到多个锁和条件变量,因此可能会影响性能和内存效率/
只要加锁,必会影响性能和效率,但是好处是保证数据的安全。
线程队列的使用
1、先进先出(queue.Queue())
import queue q = queue.Queue() q.put('1') q.put('2') q.put('3') print(q.get()) print(q.get()) print(q.get()) ''' 结果: 1 2 3 '''
2、先进后出(queue.LifoQueue();Lifo: last input first output)
import queue q = queue.LifoQueue() q.put('1') q.put('2') q.put('3') print(q.get()) print(q.get()) print(q.get()) ''' 结果: 3 2 1 '''
3、优先级队列(queue.PriorityQueue())
put进入一个元组,元组的第一个元素是优先级,可以是数字,也可以是别的,数字越小优先级越高。
import queue q=queue.PriorityQueue() q.put(('40','a')) q.put(('20','b')) q.put(('30','c')) print(q.get()) print(q.get()) print(q.get()) ''' 结果: ('20', 'b') ('30', 'c') ('40', 'a') '''
进程池和线程池
池:存放多个元素值
进程池(ProcessPoolExecuto):存放多个进程的
提前定义一个池子,里面放很多个进程,只需要往池子里丢任务即可,这个池子里的任意一个进程来执行任务。
线程池(ThreadPoolExecutor):存放多个线程的
提前定义一个池子,里面放很多线程,只需要往池子里丢任务即可,这个池子里任意一个线程来执行任务。
异步提交任务
相当于进程池的pool.close() + pool.join()
def task(n,m): print('456') return n + m from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor def callback(res): print(res.result()) print('123') if __name__ == '__main__': pool = ThreadPoolExecutor(2) #池子里有2个工作者,实例化2个进程 pool.submit(task,n=1,m=2).add_done_callback(callback) #进程池回调函数给结果 pool.submit(task,n=6,m=4).add_done_callback(callback) #进程池回调函数给结果 # pool.shutdown() ''' 结果:3 task中的值返回给了callback,pool属于进程池 '''
多个回调函数
def task(n,m): return n + m def task1(): res = {'name':'lin'} return res from multiprocessing import Process from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor def callback(res): print(res.result()) def callback1(res): print(res.result()) print(res.result()['name']) if __name__ == '__main__': pool = ThreadPoolExecutor(2) #池子里有2个工作者,实例化2个进程 pool.submit(task,n=1,m=2).add_done_callback(callback) pool.submit(task1).add_done_callback(callback1) pool.shutdown() ''' 结果: 3 {'name': 'lin'} lin '''
协程
进程:进程解决高并发问题
线程:解决高并发问题
协程:他是单线程下的并发,是程序员级别的,我们来控制如何切换,什么时候切换
进程和线程是操作系统控制的。
协程是一种用户态(程序员)的轻量级线程,即协程是由用户程序自己来调度的。
进程的开销》》》线程的开销》》》协程的开销
协程需要借助第三方模块:gevent
必须先安装
协程实现的高并发程序
服务端:
from gevent import monkey monkey.patch_all() import gevent from socket import socket from threading import Thread def talk(conn): while True: try: data = conn.recv(1024) #接收数据 if len(data) == 0: break conn.send(data.upper()) #发送数据 except Exception as e: print(e) conn.close() def sever(ip,port): server = socket() server.bind((ip,port)) #连接传入的ip和端口 server.listen(5) while True: conn,addr = server.accept() gevent.spawn(talk,conn) #将conn传入sever中 if __name__ == '__main__': g1 = gevent.spawn(sever,'127.0.0.1',8080) #将ip和端口传入sever g1.join()
客户端:
import socket from threading import current_thread, Thread def socket_client(): cli = socket.socket() cli.connect(('127.0.0.1', 8080)) while True: ss = '%s say hello' % current_thread().getName() cli.send(ss.encode('utf-8')) data = cli.recv(1024) print(data) for i in range(10): t = Thread(target=socket_client) t.start()
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· DeepSeek在M芯片Mac上本地化部署
· 葡萄城 AI 搜索升级:DeepSeek 加持,客户体验更智能