简单讲讲多进程、多线程以及协程

2020-10-29  本文已影响0人  TK丰

一、基础概念描述

1.什么是进程

一个程序的执行实例就是一个进程,是一个动态概念,是操作系统进行资源(CPU、内存、磁盘、IO、带宽等)分配的基本(最小)单位,所以每个进程包含了程序执行过程中的所有资源。 进程间的数据交换需要中间件来进行传递。

一个python脚本(排除整个脚本用多进程方式编写)运行就是一个进程

# 以最简单的为例
print ('hello word')

1.2 什么是线程

线程是cpu的最小调度单位,同属一个进程里面的线程共享所有资源。
一个进程可由多个线程的执行单元组成,每个线程都运行在同一进程的上下文中,共享同样的代码和全局数据,所以线程间的数据交换会来得容易些

当我们直接运行脚本时,操作系统就会先创建一个进程,这个进程就会创建一个主线程,然后主线程会再创建其他的子线程(若有)。

二、 进程与线程的区别

1.进程是操作系统分配的最小单位,线程是CPU调度的最小单位
2.一个进程由一个或者多个线程组成,线程是一个进程中代码的不同执行路线
3.切换进程需要的花销(时间、资源等)比切换线程要大,切换线程也是需要花销的
4.进程间不能直接通信

通俗理解线程和进程的关系:
进程是火车头,线程是车厢
1.一个进程可以有多个线程-->一个火车头可以有多个车厢
2.进程间不能直接通信-->一个火车头不能直接走到另外一个火车头
3.线程间共享所有资源,可以直接通信-->一个人可以从一个车厢走到另外一个车厢
4.进程要比线程消耗更多的计算机资源。->多列火车总比多个车厢要贵

三、python实现多进程

3.1 多进程方法一:将方法作为进程

# import 多进程库
import multiprocessing

def worker1(name):
    print("worker1 name is " + name)

def worker2(name):
    print('worker2 name is' + name )


if __name__ == "__main__":
    # target后面传入要多进程的方法,args以元组的方式传入参数
    # 创建两个进程分别调用不同的方法
    p1 = multiprocessing.Process(target=worker1, args=('subprocess1',)) 
    p2 = multiprocessing.Process(target=worker2, args=('subprocess2'))

    #启动进程
    p1.start()
    p2.start()
    
    #停止进程
    p1.join()  
    p2.join()

3.2 多进程方法二:将类作为进程

import multiprocessing
import time

# 若在调试模式下运营,要将下面的注释恢复,不然会报错
# multiprocessing.set_start_method('spawn',True)

# 通过继承的方式来实现
class MyProcess(multiprocessing.Process):
    def __init__(self, loop):
        multiprocessing.Process.__init__(self)
        self.loop = loop

    # 需要重写run方法,把业务逻辑塞到这个方法下
    def run(self):
        for count in range(self.loop):
            time.sleep(1)
            print('Pid: ' + str(self.pid) + ' LoopCount: ' + str(count))


if __name__ == '__main__':
    for i in range(2, 5):
        p = MyProcess(i)
        p.start()

进程间的通信可以通过队列(Queue)来进行实现,Queue是多进程安全的队列,主要有两个方法put和get。关于Queue的更多用法可以自行百度下

3.3 通过队列Queue实现生产者-消费者模式

import multiprocessing

multiprocessing.set_start_method('spawn',True)
def Producer(q):      
    try:         
        q.put(1, block = False) 
    except:         
        pass   

def Consumer(q):      
    try:         
        print (q.get(block = False))
    except:         
        pass

if __name__ == "__main__":
    # 创建一个队列 
    q = multiprocessing.Queue()
    # 把队列作为参数传入
    producer = multiprocessing.Process(target=Producer, args=(q,))  
    producer.start()   
  
    # 把队列作为参数传入
    consumer = multiprocessing.Process(target=Consumer, args=(q,))  
    consumer.start()  

    consumer.join()  
    producer.join()

进程间还可以通过管道Pipe进行通信。Pipe 可以是单向 (half-duplex),也可以是双向 (duplex)。我们通过 mutiprocessing.Pipe (duplex=False) 创建单向管道 (默认为双向)。一个进程从 PIPE 一端输入对象,然后被 PIPE 另一端的进程接收,单向管道只允许管道一端的进程输入,而双向管道则允许从两端输入

3.3 通过队列Pipe实现生产者-消费者模式

队列用方法作为进程,Pipe就用类作为进程来试试吧

import multiprocessing 


class Consumer(multiprocessing.Process):
    def __init__(self, pipe):
        multiprocessing.Process.__init__(self)
        self.pipe = pipe

    def run(self):
        # 消费者发送信息'Consumer Words'
        self.pipe.send('Consumer Words')
        # 消费者接收来自管道的信息
        print ('Consumer Received:', self.pipe.recv())


class Producer(multiprocessing.Process):
    def __init__(self, pipe):
        multiprocessing.Process.__init__(self)
        self.pipe = pipe

    def run(self):
        # 生产者接收来自管道另一端的信息'Consumer Words'
        print ('Producer Received:', self.pipe.recv())
        # 生产者发送信息'Producer Words'
        self.pipe.send('Producer Words')


if __name__ == '__main__':
    pipe = multiprocessing.Pipe()
    p = Producer(pipe[0])
    c = Consumer(pipe[1])
    
    p.start()
    c.start()
    p.join()
    c.join()

大家运行下就知道什么效果了。关于多进程还有很多一些其他东西,例如:锁、事件、信号量、进程池、守护进程等,请大家自行百度,这里就不再细讲

四、python实现多线程

4.1 前言-GIL锁

在讲python的多线程时,不得不先讲一下python大名鼎鼎的GIL锁-Global Interpreter Lock(全局解释器锁)。

简单的说,GIL规定了线程在运行时,需要先拿到通行证,否则就不能运行,也就意味着一个python的进程里,无论你有多少个线程,永远只能单线程运行。

插入一个小知识点,还记得上文说过,线程是cpu最小调度单位吗?也就意味着,python多线程是无法使用多核的,但是多进程是可以利用多核的

4.2 那是不是python的多线程就是没用呢?

直接说结论:如果你的程序是CPU密集型的,那么python的多线程是完全没有意义,甚至由于线程切换的花销,会导致更慢点。

但如果你的是IO密集型,那么多线程的提升还是很明显的。

再插入一个小知识点: IO可以简单理解成对数据的读写。例如:等待网络数据到来、从文件中读/写数据等。
P.S. 一般web应用也是IO密集型,所以做了个服务端,响应很慢的话要看看是不是自己代码问题,而不是怪GIL

4.3 多线程方法一:将方法作为线程

# import 线程库
import threading

# 这个函数名可随便定义
def run(n):
    print("current task:", n)

if __name__ == "__main__":
    # 创建线程
    t1 = threading.Thread(target=run, args=("thread 1",))
    t2 = threading.Thread(target=run, args=("thread 2",))
    t1.start()
    t2.start()

4.4 多线程方法二:将类作为线程

import threading

# 继承线程库的类
class MyThread(threading.Thread):
    def __init__(self, n):
        super(MyThread, self).__init__()  
        self.n = n

    # 重构run方法,把业务逻辑写下面
    def run(self):
        print("current task:", n)

if __name__ == "__main__":
    t1 = MyThread("thread 1")
    t2 = MyThread("thread 2")

    t1.start()
    t2.start()

五、关于进程以及线程的进一步讲解

5.1 什么是协程

协程是属于一种操作,是由用户自己去操作线程的切换(在用户态进行切换),这样的话,就可以大大降低线程切换(在内核态切换)的花销。

补充个小知识点,线程的切换一般是由cpu控制。假设为一个单核的cpu,那么同一时间只会有一个线程运行。这时候如果有多个线程任务运行,那么cpu将会根据当前线程的状态进行调度。
协程就是当代码中出现有io处理的时候,先代码自行调度,将这个操作挂起,然后去继续执行其他操作。这样的话,cpu就不会因为代码中出现io处理进行线程切换,从而减少线程切换的花销,提升运行速度

5.2 协程跟进程、线程的区别

  1. 协程既不是进程也不是线程,协程仅仅是一个特殊的函数,协程它进程和进程不是一个维度的。

2.一个进程可以包含多个线程,一个线程可以包含多个协程。

3.一个线程内的多个协程虽然可以切换,但是多个协程是串行执行的,只能在一个线程内运行,没法利用CPU多核能力。

4.协程与进程一样,切换是存在上下文切换问题的。

5.3 实现协程的方法一:使用yield的方法

def Consumer():           
    r = ''          
    while True:                
        n = yield r    
        if not n:                                  
            return          
        print('Consumer Get %s...' % n) 
        r = '200 OK'                               
                                                     
def Producer(c):                                    
    c.send(None)                     
    n = 0                                         
    while n < 5:                                   
        n = n + 1                                  
        print('Producer Words %s...' % n)
        r = c.send(n) 
        print('Consumer return: %s' % r)
    c.close()                                      

c = Consumer()                                    
Produce(c)                                

由于篇幅有限,yield的具体使用方法不在这里详细讲。yield这个关键字本身就是为了上下文切换而生的,换句话说是为了协程而生的

5.4 实现协程的方法一:使用asyncio库

自己通过yield一点点实现肯定非常复杂的,所以可以借助大神的力量

import asyncio
 # 需要利用队列来进行协程之间的数据交换
queue =  asyncio.Queue()

async def Producer():
        n = 0
        while True:
            await asyncio.sleep(2)
            print('add value to queue:',str(n))
            await queue.put(n)
            n = n + 1

async def Consumer():
    while True:
        try:
            r = await  asyncio.wait_for(queue.get(), timeout=1.0)
            print('consumer value>>>>>>>>>>>>>>>>>>', r)
        except asyncio.TimeoutError:
            print('get value timeout')
            continue
        except:
            break
    print('quit')
loop = asyncio.get_event_loop()
tasks = [Producer(), Consumer()]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

通过代码我们可以发现,两个方法都有无限循环在里面。若是采用非协程的做法,就需要多进程或者多线程的方式来实现两个无限循环方法间的交互(无限循环会阻塞线程)。但使用协程的时候,我们可以让程序在我们需要的时候让出控制权,从而执行我们需要的代码。通过协程(一个线程)完成了多进程、多线程的消费者模型

最后

在写这篇文章的时候,限于篇幅有很多概念、区别等没有完全讲述清楚,后续会继续深入探讨

Thank you for attention

To Be Continued

喜欢的点个在看呗
上一篇 下一篇

猜你喜欢

热点阅读