一、基础概念描述
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 协程跟进程、线程的区别
协程既不是进程也不是线程,协程仅仅是一个特殊的函数,协程它进程和进程不是一个维度的。
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
喜欢的点个在看呗