zoukankan      html  css  js  c++  java
  • 并发基础知识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操作较多的话,那么我们可以采取多线程的方法,因为线程的开设比较省资源。单涂过是第二种情况较多的话,就可以利用多核的多进程,这样更节省时间。

  • 相关阅读:
    12.12
    12.11
    1208
    1206
    2018-12-23丛晓强作业
    2018-12-17面向对象总结
    2018-12-17-丛晓强作业
    2018-12-13丛晓强作业
    2018-12-12丛晓强作业
    2018-12-11丛晓强作业
  • 原文地址:https://www.cnblogs.com/liqianxin/p/12762441.html
Copyright © 2011-2022 走看看