Python概念之多线程+多进程+协程
文案来自于
只是将文案中的代码自己修改了一下,解决了自己对于文章中不懂的地方,谨做记录,平时工作中,除了appium处理多个手机的时候用过多进程,其实很少用到这方面的知识,所以做一个简单的梳理,概念性的记录
多进程
linux或者Mac操作系统
Unix/Linux操作系统提供了一个fork()
系统调用,它非常特殊。普通的函数调用,调用一次,返回一次,但是fork()
调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。
子进程永远返回0
,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()
就可以拿到父进程的ID。
Python的os
模块封装了常见的系统调用,其中就包括fork
,可以在Python程序中轻松创建子进程:
import os
print(os.getpid())
pid = os.fork() # 什么都不会打印,只是会创建一个子进程,从此之后执行的所有操作,都会父进程执行一遍,子进程执行一遍
print(pid) # 但是子进程永远返回0,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()就可以拿到父进程的ID。
print("hello")
print("erzi")
print(os.getppid()) # 打印父进程
结果
19499 #这个就是print(os.getpid())打印出来的当前的进程id
19500 #这个就是父进程执行print(pid)打印出来的,父进程返回子进程的ID
hello #这个就是父进程执行print("hello")打印出来的
erzi #这个就是父进程执行print("erzi")打印出来的
50863 #这个就是父进程执行print(os.getppid()),获取了父进程的id
0 #这个就是子进程执行print(pid)打印出来的,但是子进程永远返回0
hello #这个就是子进程执行print("hello")打印出来的
erzi #这个就是子进程执行print("erzi")打印出来的
19499 #这个就是子进程执行print(os.getppid()),获取了父进程的id,也就是第一行打印出来的19499
综上所述,新建了一个进程之后,父进程做啥,子进程就做啥
多平台multiprocessing
由于Windows没有fork
调用,上面的代码在Windows上无法运行。而Mac系统是基于BSD(Unix的一种)内核,所以,在Mac下运行是没有问题的,推荐大家用Mac学Python!
如果你打算编写多进程的服务程序,Unix/Linux无疑是正确的选择。由于Windows没有fork
调用,难道在Windows上无法用Python编写多进程的程序?
由于Python是跨平台的,自然也应该提供一个跨平台的多进程支持。multiprocessing
模块就是跨平台版本的多进程模块。
multiprocessing
模块提供了一个Process
类来代表一个进程对象
import multiprocessing
def add(name):
print(name + name + name)
if __name__ == '__main__':
for i in range(3):
p = multiprocessing.Process(target=add, args=(i,)) # target就是目标执行函数,args里就是函数的入参
p.start()
p.join() # join()方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。
print('进程%d执行完毕' % i)
结果
0
进程0执行完毕
3
进程1执行完毕
6
进程2执行完毕
如果我们注释掉p.join()这一行,结果就成了下面这样,所以join更通俗易懂的解释应该是,有了join,就可以让一个进程执行完毕之后再执行另一个进程
进程0执行完毕
进程1执行完毕
进程2执行完毕
0
3
6
多线程
Python的标准库提供了两个模块:_thread
和threading
,_thread
是低级模块,threading
是高级模块,对_thread
进行了封装。绝大多数情况下,我们只需要使用threading
这个高级模块。
启动一个线程就是把一个函数传入并创建Thread
实例,然后调用start()
开始执行
import threading
def add(name):
print('现在运行的线程为%s' % threading.current_thread().name)
print('name的值为%s' % name)
if __name__ == '__main__':
for i in range(3):
p = threading.Thread(target=add, args=(i,))
p.start()
p.join()
print('第%d个线程执行完毕' % i)
结果
现在运行的线程为Thread-1
name的值为0
第0个线程执行完毕
现在运行的线程为Thread-2
name的值为1
第1个线程执行完毕
现在运行的线程为Thread-3
name的值为2
第2个线程执行完毕
我们再把p.join()注释掉,执行结果是乱的,每一次都不一样
现在运行的线程为Thread-1
name的值为0
第0个线程执行完毕
现在运行的线程为Thread-2
name的值为1
第1个线程执行完毕
现在运行的线程为Thread-3
name的值为2
第2个线程执行完毕
Lock
多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。
协程
协程 ,又称为微线程,它是实现多任务的另一种方式,只不过是比线程更小的执行单元。因为它自带CPU的上下文,这样只要在合适的时机,我们可以把一个协程切换到另一个协程。
通俗的理解: 在一个线程中的某个函数中,我们可以在任何地方保存当前函数的一些临时变量等信息,然后切换到另外一个函数中执行,注意不是通过调用函数的方式做到的 ,并且切换的次数以及什么时候再切换到原来的函数都由开发者自己确定。
协程与线程的差异:
在实现多任务时, 线程切换__从系统层面__远不止保存和恢复CPU上下文这么简单。操作系统为了程序运行的高效性,每个线程都有自己缓存Cache等等数据,操作系统还会帮你做这些数据的恢复操作,所以线程的切换非常耗性能。但是__协程的切换只是单纯地操作CPU的上下文__,所以一秒钟切换个上百万次系统都抗的住。
import time
def one():
while True:
print("one start")
yield #带yield的函数是一个生成器,而不是一个函数了,这个生成器有一个函数就是next函数,next就相当于“下一步”生成哪个数,这一次的next开始的地方是接着上一次的next停止的地方执行的
print("one end")
time.sleep(0.5)
def two():
while True:
print("two start")
yield
print("two end")
time.sleep(0.5)
if __name__ == "__main__":
t1 = one() # 生成器对象
t2 = two()
while True:
next(t1) # 1、唤醒生成器t1,执行到yield后,保存上下文,挂起任务;下次再次唤醒之后,从yield后继续往下执行,调用next()函数获取由yield语句返回的下一个值
print("\nThe main thread!\n") # 2、继续往下执行
next(t2) # 3、唤醒生成器t2,....