并发基础知识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. 锁只有在处理数据的部分加用来保证数据的安全,只在争夺数据的环节加锁处理即可。

进程间通信

不同的进程之间在默认情况下是相互隔离的,可以通过队列进行通信。

队列表现为一个管道 的东西,数据可以在里面排着队出来,先进的先出,同时内部含有锁的机制,也就是一旦有一个进程取走数据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执行的,是执行单位,线程在执行过程中需要的资源要到进程中去拿。相当于矿工。

进程运行的时候会发生以下几件事情:

  1. 申请内存空间
  2. 将硬盘上的代码加载一份到内存中。

基于一个进程的多个线程之间的数据是共享资源的,且开设线程无需消耗内存空间。

# 每一个线程实现的功能是不相同的,多个线程共同完成我们在进程上所想要达到的目标。

开启线程的两种方式

开启线程的方式和开启进程的方式是一样的,除了导入的模块不相同。

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全局解释锁

要注意以下几点:

  1. 这是Cpython解释器的问题,不是python本身的问题。
  2. 这是针对cpython解释器的锁,同一时刻只有一个线程能够拿到Cpython解释器。
  3. 弊端:会导致同一进程下的多个线程无法同时进行,牺牲了多核优势。
  4. 针对不同的数据依然需要加锁处理。
  5. 解释器语言的通病:同一进程下多个线程无法利用多核优势。

我们要明白,进程只是一块内存空间,真正让程序执行的是线程,同时,计算机是无法识别我们写的代码的,当我们执行我们的代码的时候,是先将代码给解释器,解释器翻译给操作系统,然后调用内核操作硬件,最后完成程序的执行。

此时在上述整个环节中,GIL全局解释锁给解释器上了一把锁,导致同一进程上同一时刻只能有一个线程被执行,这样就牺牲了多核优势。只有当拿到锁的线程遇到IO操作或者运行过长时间,才会把锁丢掉给别的线程抢。

虽然在过程上显得很浪费时间 ,但是由于运行速度过快,所以在我们感觉上就像同时运行的。

补充:

那么我们应该怎么选择多线程还是多进程呢?

前面讲过,如果当线程拿到解释器的时候,会一直运行到遇到IO操作或运行时间过长这两个条件才会丢掉锁。遇到第一种情况,也就是IO操作较多的话,那么我们可以采取多线程的方法,因为线程的开设比较省资源。单涂过是第二种情况较多的话,就可以利用多核的多进程,这样更节省时间。

posted @ 2020-04-23 17:43  小菜鸟是我  阅读(154)  评论(0编辑  收藏  举报