并发基础知识2
并发编程知识2
查看PID号
计算机会给每一个正在运行的进程分配一个PID号。
- windows上输入tasklist查看进程
- mac上在终端输入ps aux查看
from multiprocessing import Process,current_process
# os模块下的方法
os.getpid() # 查看当前进程的进程号
os.getppid() # 查看当前进程的父进程的进程号
# Process和current_process下的方法
terminate() # 杀死当前进程:不会立马杀死,需要一定的时间
current——Process().pid # 查看当前进程的进程号
is_alive() # 判断当前进程是否存活。
僵尸进程和孤儿进程
僵尸进程
当我们中断已经开设了子进程的进程之后,进程死后不会被立刻释放占用的pid号。因为其父进程有时要通过其查看它的子进程的基本信息。
所有的进程都会进入僵尸进程。父进程在创造子进程之后,假设子进程退出,而父进程并没有获取到,没有回收子进程占用的pid号,那么这子进程是僵尸进程,一般情况下,父进程都会等待子进程运行结束在结束。
孤儿进程
子进程存活而父进程意外死亡,此时就会有1号进程init来接管这些孤儿程序,进行回收各种资源。
守护进程
守护进程就是随着父进程一起死亡的进程,一旦父进程结束,守护进程也会跟着结束。而且守护进程之内没有办法在开启新的子进程。
通过在p.start()之前设置p.daemon=True来实现将p设置为守护进程。
互斥锁
当多个操作同时操作同一份数据的时候,就会出现数据错乱的问题。
这时候可以采用加锁处理:互斥锁。将并发变成串行,虽然降低效率但是保证了数据安全。
from multiprocessing import Process,Lock
import time
import json
import random
def search(name):
with open("a.txt","r") as f:
msg = json.load(f)
time.sleep(random.randint(1,3))
print("票还剩下%s张"%msg.get("ticket"))
def buy(name):
with open("a.txt", "r") as f:
msg = json.load(f)
if msg.get("ticket") > 0:
time.sleep(random.randint(1,3))
msg["ticket"] -= 1
print(name, "抢票成功")
with open("a.txt", "w") as f:
json.dump(msg, f)
else:
print(name,"购票失败")
def run(i,mutex):
search(i)
# 给买票的环节加锁。
mutex.acquire() # 抢占资源
buy(i)
mutex.release() # 释放资源
if __name__ == '__main__':
mutex = Lock()
for i in range(4):
p = Process(target=run,args=(i,mutex))
p.start()
-
锁不能轻易使用,容易造成死锁现象。
-
锁只有在处理数据的部分加用来保证数据的安全,只在争夺数据的环节加锁处理即可。
进程间通信
不同的进程之间在默认情况下是相互隔离的,可以通过队列进行通信。
队列表现为一个管道 的东西,数据可以在里面排着队出来,先进的先出,同时内部含有锁的机制,也就是一旦有一个进程取走数据1,那么其他进程就只能去取数据2了。
from multiprocessing import Queue
q = Queue(5) # 创建一个队列,括号内为队列排列数据大小。默认为一个很大的数字。
q.put(data) # 将数据data排入队列
q.full() # 判断数据是否满了
q.get() # 从队列中拿数据
q.empty() # 判断队列是不是空的
q.get_nowait() # 如果从队列中取不到数据就报错
q.get(timeout=3) # 等待3秒,如果还没取到数据就报错
在多进程中应用队列的话,是无法保证进程不会取乱数据的。
队列还有这样的特性,如果队列满了依然往队列里面放数据,程序会卡在方数据的那一行代码,他会一直等到自己的数据能够放到队列中,同样地,如果队列空了依然还要取数据,程序会卡在取数据那一行代码,一直到能够从队列中取到数据。
以上两种情况都是卡在某行代码,而不会报错
IPC机制
多个进程之间通过队列来进行通信。
from multiprocessing import Queue, Process
def productor(q):
q.put("123")
def consumer(q):
print(q.get())
if __name__ == '__main__':
q = Queue()
p1 = Process(target=productor, args=(q,))
p2 = Process(target=consumer, args=(q,))
p1.start()
p2.start()
生产者消费者模型
多进程中有进程是专门为队列生产数据的,而有进程是专门从队列中取数据,前者是生产者,后者是消费者。而中间通信的渠道则是队列。
from multiprocessing import Queue, Process
import time
def productor(name,product,q):
for i in range(5):
time.sleep(1)
q.put(f"{product}{i}")
print(f"{name}制作了产品名:{product} 编号:{i}")
# 当生产完毕之后,发送一个None代表产品生产完毕了。
q.put(None)
def consumer(name,q):
while True:
time.sleep(1.5)
product = q.get()
# 如果得到的产品是一个None,就可以吧这个消费者走人了。
if product is None:break
print(f"{name}吃了{product}")
if __name__ == '__main__':
q = Queue()
# 创建生产者
p1 = Process(target=productor,args=("tom", "包子", q))
p2 = Process(target=productor,args=("jack", "八宝粥", q))
p1.start()
p2.start()
# 等到生产完毕之后,才让消费者对象吃
p1.join()
p2.join()
# 消费者对象
c1 = Process(target=consumer,args=("son", q))
c2 = Process(target=consumer,args=("grandson", q))
# 消费者开吃
c1.start()
c2.start()
想要让程序不卡在消费者等待吃的东西的代码处,在把队列的数据吃完之后,就需要给消费者传一个None值,让消费者知道队列没数据了,加一个条件判断,就可以不让程序卡顿,注意有几个消费者需要传给队列几个None,上述是因为生产者正好等于消费者,才在函数内部添加的。
当然还有更好的解决办法,那就是利用一个JoinableQueue.
JoinableQueue() # 创建一个队列,其内部有一个计数器,向队列添加数据会+1,取出数据-1
q.task_done() # 告诉队列已经将取出的值处理完毕了,它跟get()是一一对应的,数量不符会报错。
q.join() # 只有当队列中的数据处理完毕,即计数器为0 的时候才会执行之后的代码
from multiprocessing import JoinableQueue, Process
import time
def productor(name,product,q):
for i in range(5):
time.sleep(1)
q.put(f"{product}{i}")
print(f"{name}制作了产品名:{product} 编号:{i}")
def consumer(name,q):
while True:
time.sleep(1.5)
product = q.get()
print(f"{name}吃了{product}")
q.task_done() # 跟q.get()对应,表示将该值处理完毕了
if __name__ == '__main__':
q = JoinableQueue()
p1 = Process(target=productor,args=("tom", "包子", q))
p2 = Process(target=productor,args=("jack", "八宝粥", q))
p1.start()
p2.start()
p1.join()
p2.join()
c1 = Process(target=consumer,args=("son", q))
c2 = Process(target=consumer,args=("grandson", q))
c1.daemon = True # 设置成守护进程,避免成为孤儿进程
c2.daemon = True
c1.start()
c2.start()
q.join() # 将队列中的数据处理完毕才会执行之后的代码
线程理论
线程是依赖进程的,一个进程可以有多个线程。
进程其实只是在内存空间中申请得一块内存空间,是一个资源单位。相当于矿。
线程则是真正被cpu执行的,是执行单位,线程在执行过程中需要的资源要到进程中去拿。相当于矿工。
进程运行的时候会发生以下几件事情:
- 申请内存空间
- 将硬盘上的代码加载一份到内存中。
基于一个进程的多个线程之间的数据是共享资源的,且开设线程无需消耗内存空间。
# 每一个线程实现的功能是不相同的,多个线程共同完成我们在进程上所想要达到的目标。
开启线程的两种方式
开启线程的方式和开启进程的方式是一样的,除了导入的模块不相同。
from threading import Thread
def task(n):
print(n)
if __name__ == "__main__":
t = Thread(target=task) # 创建一个线程对象
t.join() # 让主线程等子线程进行完毕之后,在运行子线程
t.start() # 运行线程,这个速度很快 ,对系统资源的消耗很少
开启线程不像开启进程一样需要在main之下,因为它是不需要申请内存空间的,但是一般情况下会写在双下main下。
TCP服务端实现并发的效果
首先老师讲了在学习中要有看源码的习惯,等到了一定程度,就可以用自己的思想去实现代码了。
当使用多并发TCP服务端的时候,用开启线程的方式进行,也可以使用进程的方式。
# 服务端的代码
import socket
from threading import Thread
server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.bind(("127.0.0.1",8080))
server.listen(5)
# 将连接封装成一个函数
def connection(conn):
while True:
try:
msg = conn.recv(1024)
if not msg:break
conn.send(msg.upper())
except ConnectionRefusedError:
break
conn.close()
while True:
# 等待链接
conn,addr = server.accept()
# 链接以后创造一个线程,线程执行连接用户,主线程依旧运行,在accept处等待
t = Thread(target=connection,args=(conn,))
t.start()
import socket
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(("127.0.0.1",8080))
while True:
msg = input("请输入信息:").strip()
if not msg:break
client.send(msg.encode("utf-8"))
data = client.recv(1024)
print(data.decode("utf-8"))
注意:在同一个进程之下的多个线程的数据是共享的。线程都是基于进程之上实现的。一旦某个线程对进程中的数据进行更改,其他线程是能拿到更改后的结果的 。
但是在进程中就不一样,进程是以模块的形式展现的,不同的进程之间默认是不相通的。
from threading import Thread,active_count,current_thread
active_count() # 统计正在进行的线程。
current_thread().name # 获取线程的名字
守护线程:守护线程就是皇帝(主线程)身边的太监,一旦皇帝驾崩,守护进程也会一起陪葬。而且他也不能产生子线程。
守护线程的设定就是t.daemon = True
。这行代码出现在start()之前,就是运行之前就要确定它属于守护进程。
线程互斥锁
互斥锁就是多个强盗去争夺一个数据。谁抢到了别的人就不能使用了,主要用于对数据的保护。而在线程中也是这样,一旦对某些数据上了锁,那么线程1拿到该数据以后,别的线程就不能抢了,只能乖乖等待线程1用完释放。
from threading import Lock
mutex = Lock() # 创造一个锁
mutex.acquire() # 对某些数据或者操作进行加锁,代码放到数据或操作之前。
mutex.release() # 这是解锁操作,运行到这一步,线程就会把宝贝锁丢掉让别人去抢。
还有一种操作就是利用with来管理。跟打开文件的操作一样,会在最后自动进行释放锁。
with mutex:
上锁的代码
GIL全局解释锁
要注意以下几点:
- 这是Cpython解释器的问题,不是python本身的问题。
- 这是针对cpython解释器的锁,同一时刻只有一个线程能够拿到Cpython解释器。
- 弊端:会导致同一进程下的多个线程无法同时进行,牺牲了多核优势。
- 针对不同的数据依然需要加锁处理。
- 解释器语言的通病:同一进程下多个线程无法利用多核优势。
我们要明白,进程只是一块内存空间,真正让程序执行的是线程,同时,计算机是无法识别我们写的代码的,当我们执行我们的代码的时候,是先将代码给解释器,解释器翻译给操作系统,然后调用内核操作硬件,最后完成程序的执行。
此时在上述整个环节中,GIL全局解释锁给解释器上了一把锁,导致同一进程上同一时刻只能有一个线程被执行,这样就牺牲了多核优势。只有当拿到锁的线程遇到IO操作或者运行过长时间,才会把锁丢掉给别的线程抢。
虽然在过程上显得很浪费时间 ,但是由于运行速度过快,所以在我们感觉上就像同时运行的。
补充:
那么我们应该怎么选择多线程还是多进程呢?
前面讲过,如果当线程拿到解释器的时候,会一直运行到遇到IO操作或运行时间过长这两个条件才会丢掉锁。遇到第一种情况,也就是IO操作较多的话,那么我们可以采取多线程的方法,因为线程的开设比较省资源。单涂过是第二种情况较多的话,就可以利用多核的多进程,这样更节省时间。