Python 高级教程之线程进程和协程

Posted 海拥✘

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python 高级教程之线程进程和协程相关的知识,希望对你有一定的参考价值。

进程

进程是指在系统中正在运行的一个应用程序,是 CPU 的最小工作单元。

进程 5 种基本状态

一个进程至少具有 5 种基本状态:初始态、就绪状态、等待(阻塞)状态、执行状态、终止状态。

  • 初始状态:进程刚被创建,由于其他进程正占有CPU资源,所以得不到执行,只能处于初始状态。
  • 就绪状态:只有处于就绪状态的经过调度才能到执行状态
  • 等待状态:进程等待某件事件完成
  • 执行状态:任意时刻处于执行状态的进程只能有一个(对于单核CPU来讲)。
  • 停止状态:进程结束

进程的特点

  • 动态性:进程是程序的一次执行过程,动态产生,动态消亡。
  • 独立性:进程是一个能独立运行的基本单元。是系统分配资源与调度的基本单元。
  • 并发性:任何进程都可以与其他进程并发执行。
  • 结构性:进程由程序、数据和进程控制块三部分组成。

multiprocessing 是比 fork 更高级的库,使用 multiprocessing 可以更加轻松的实现多进程程序。

#!/usr/bin/env python
# -*- coding:utf-8 -*- 
from multiprocessing import Process
import threading
import time

def foo(i):
    print 'say hi',i

for i in range(10):
    p = Process(target=foo,args=(i,))
    p.start()

注意:由于进程之间的数据需要各自持有一份,所以创建进程需要的非常大的开销。并且python不能再Windows下创建进程!

使用多进程的时候,最好是创建和和 CPU 核数相等的进程数。

进程间数据共享

系统中的进程与其他进程共享 CPU 和主存资源,为了更好的管理主存,操作系统提供了一种对主存的抽象概念,即为虚拟存储器(VM)。它也是一个抽象的概念,它为每一个进程提供了一个假象,即每个进程都在独占地使用主存。

虚拟存储器主要提供了三个能力:

  • 将主存看成是一个存储在磁盘上的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据,通过这种方式,更高效地使用主存
  • 为每个进程提供一致的地址空间,从而简化存储器管理
  • 保护每个进程的地址空间不被其他进程破坏

由于进程拥有自己独占的虚拟地址空间,CPU通过地址翻译将虚拟地址转换成真实的物理地址,每个进程只能访问自己的地址空间。因此,在没有其他机制(进程间通信)的辅助下,进程之间是无法共享数据的。

进程各自持有一份数据,默认无法共享数据。默认的进程之间相互是独立,如果想让进程之间数据共享,就得有个特殊的数据结构,这个数据结构就可以理解为他有穿墙的功能 如果你能穿墙的话两边就都可以使用了

#!/usr/bin/env python
#coding:utf-8
 
from multiprocessing import Process
from multiprocessing import Manager
import time
 
li = []
 
def foo(i):
    li.append(i)
    print 'say hi',li
  
for i in range(10):
    p = Process(target=foo,args=(i,))
    p.start()
     
print 'ending',li

使用特殊的数据类型,来进行穿墙:

#通过特殊的数据结构:数组(Array)

from multiprocessing import Process,Array

#创建一个只包含数字类型的数组(python中叫列表)
#并且数组是不可变的,在C,或其他语言中,数组是不可变的,之后再python中数组(列表)是可以变得
#当然其他语言中也提供可变的数组
#在C语言中数组和字符串是一样的,如果定义一个列表,如果可以增加,那么我需要在你内存地址后面再开辟一块空间,那我给你预留多少呢?
#在python中的list可能用链表来做的,我记录了你前面和后面是谁。列表不是连续的,数组是连续的

'''
上面不是列表是“数组"数组是不可变的,附加内容是为了更好的理解数组!
'''

temp = Array('i', [11,22,33,44]) #这里的i是C语言中的数据结构,通过他来定义你要共享的内容的类型!点进去看~
 
def Foo(i):
    temp[i] = 100+i
    for item in temp:
        print i,'----->',item
 
for i in range(2):
    p = Process(target=Foo,args=(i,))
    p.start()
    
第二种方法:
#方法二:manage.dict()共享数据
from multiprocessing import Process,Manager  #这个特殊的数据类型Manager
 
manage = Manager()
dic = manage.dict() #这里调用的时候,使用字典,这个字典和咱们python使用方法是一样的!
 
def Foo(i):
    dic[i] = 100+i
    print dic.values()
 
for i in range(2):
    p = Process(target=Foo,args=(i,))
    p.start()
    p.join()

既然进程之间可以进行共享数据,如果多个进程同时修改这个数据是不是就会造成脏数据?是不是就得需要锁!

进程的锁和线程的锁使用方式是非常一样的知识他们是用的类是在不同地方的。

进程池

进程池内部维护一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池序列中没有可供使用的进进程,那么程序就会等待,直到进程池中有可用进程为止。

进程池中有两个方法:

  • apply
  • apply_async
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from  multiprocessing import Process,Pool
import time

def Foo(i):
    time.sleep(2)
    return i+100

def Bar(arg):
    print arg

pool = Pool(5) #创建一个进程池
#print pool.apply(Foo,(1,))#去进程池里去申请一个进程去执行Foo方法
#print pool.apply_async(func =Foo, args=(1,)).get()

for i in range(10):
    pool.apply_async(func=Foo, args=(i,),callback=Bar)

print 'end'
pool.close()
pool.join()#进程池中进程执行完毕后再关闭,如果注释,那么程序直接关闭。

'''
apply 主动的去执行
pool.apply_async(func=Foo, args=(i,),callback=Bar) 相当于异步,当申请一个线程之后,执行FOO方法就不管了,执行完之后就在执行callback ,当你执行完之后,在执行一个方法告诉我执行完了
callback 有个函数,这个函数就是操作的Foo函数的返回值!
'''

进程的缺点

无法即时完成的任务带来大量的上下文切换代价与时间代价。

进程的上下文:当一个进程在执行时,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容被称为该进程的上下文。

上下文切换:当内核需要切换到另一个进程时,它需要保存当前进程的所有状态,即保存当前进程的上下文,以便在再次执行该进程时,能够得到切换时的状态并执行下去。

线程

线程的定义

在计算中,进程是正在执行的计算机程序的一个实例。任何进程都有 3 个基本组成部分:

  • 一个可执行程序。
  • 程序所需的相关数据(变量、工作空间、缓冲区等)
  • 程序的执行上下文(进程状态)

线程是进程中可以调度执行的实体。此外,它是可以在 OS(操作系统)中执行的最小处理单元。

简而言之,线程是程序中的一系列此类指令,可以独立于其他代码执行。为简单起见,您可以假设线程只是进程的子集!

线程在线程控制块 (TCB)中包含所有这些信息:

  • 线程标识符:为每个新线程分配唯一 id (TID)
  • 堆栈指针:指向进程中线程的堆栈。堆栈包含线程范围内的局部变量。
  • 程序计数器:存放线程当前正在执行的指令地址的寄存器。
  • 线程状态:可以是running、ready、waiting、start或done。
  • 线程的寄存器集:分配给线程进行计算的寄存器。
  • 父进程指针:指向线程所在进程的进程控制块 (PCB) 的指针。

多线程被定义为处理器同时执行多个线程的能力。

在一个简单的单核 CPU 中,它是通过线程之间的频繁切换来实现的。这称为上下文切换。在上下文切换中,只要发生任何中断(由于 I/O
或手动设置),就会保存一个线程的状态并加载另一个线程的状态。上下文切换发生得如此频繁,以至于所有线程似乎都在并行运行(这被称为多任务)。

在 Python 中,threading模块提供了一个非常简单直观的 API,用于在程序中生成多个线程。

使用线程模块的简单示例

让我们考虑一个使用线程模块的简单示例:

# Python程序说明线程的概念
# 导入线程模块
import threading

def print_cube(num):
    """
    打印给定数字立方的函数
    """
    print("立方: ".format(num * num * num))

def print_square(num):
    """
    打印给定数字平方的函数
    """
    print("平方: ".format(num * num))

if __name__ == "__main__":
    # creating thread
    t1 = threading.Thread(target=print_square, args=(10,))
    t2 = threading.Thread(target=print_cube, args=(10,))

    # starting thread 1
    t1.start()
    # starting thread 2
    t2.start()

    # 等到线程 1 完全执行
    t1.join()
    # 等到线程 2 完全执行
    t2.join()

    # 两个线程完全执行
    print("完成!")
平方: 100
立方: 1000
完成!

代码解析

让我们试着理解上面的代码:

  • 要导入线程模块,我们这样做:
import threading
  • 要创建一个新线程,我们创建一个Thread类的对象。它需要以下参数:
    • target : 线程要执行的函数
    • args:要传递给目标函数的参数

在上面的示例中,我们创建了 2 个具有不同目标函数的线程:

t1 = threading.Thread(target=print_square, args=(10,)) 
t2 = threading.Thread(target=print_cube, args=(10,))
  • 要启动一个线程,我们使用 Thread 类的 start 方法。
t1.start() 
t2.start()
  • 一旦线程启动,当前程序(你可以把它想象成一个主线程)也会继续执行。为了在线程完成之前停止当前程序的执行,我们使用join方法。
t1.join() 
t2.join()

结果,当前程序将首先等待 t1 的完成,然后 t2 。一旦它们完成,则执行当前程序的剩余语句。

协程

协程(Coroutine,又称微线程,纤程)是一种比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制。

我们都熟悉函数,也称为子例程、过程、子过程等。函数是打包为一个单元以执行特定任务的指令序列。当一个复杂函数的逻辑被分成几个独立的步骤,这些步骤本身就是函数时,这些函数被称为辅助函数或子程序。

Python 中的子程序由负责协调这些子程序的使用的主函数调用。子程序只有一个入口点。 协程是子程序的泛化。它们用于协作式多任务处理,其中一个进程定期或在空闲时自愿放弃(放弃)控制权,以使多个应用程序能够同时运行。协程和子程序的区别是:

  • 与子程序不同,协程有许多用于暂停和恢复执行的入口点。协程可以暂停其执行并将控制权转移给其他协程,并且可以从中断点重新开始执行。
  • 与子程序不同,没有主函数可以按特定顺序调用协程并协调结果。协程是协作的,这意味着它们链接在一起形成管道。一个协程可能会使用输入数据并将其发送给其他处理它的协程。最后,可能会有一个协程来显示结果。

协程与线程

现在您可能在想协程与线程有何不同,两者似乎都在做同样的工作。
在线程的情况下,它是根据调度程序在线程之间切换的操作系统(或运行时环境)。而在协程的情况下,决定何时切换协程的是程序员和编程语言。协程通过程序员在设定点暂停和恢复来协同工作多任务。

Python 协程

在 Python 中,协程类似于生成器,但几乎没有额外的方法,而且我们使用yield语句的方式也有细微的变化。生成器为迭代生成数据,而协程也可以使用数据
在 Python 2.5 中,引入了对 yield 语句的轻微修改,现在 yield 也可以用作表达式。例如在作业的右侧——

line = (yield)

我们发送给协程的任何值都会被(yield)表达式捕获并返回。

可以通过send()方法将值发送到协程。例如,考虑这个协程,它打印出带有前缀“Dear”的名称。我们将使用 send() 方法将名称发送到协程。

# 用于演示协程执行的 Python3 程序

def print_name(prefix):
    print("Searching prefix:".format(prefix))
    while True:
        name = (yield)
        if prefix in name:
            print(name)

# 调用协程,什么都不会发生
corou = print_name("Dear")

# 这将开始执行协程并打印第一行 "Searching prefix..."
# 并将执行推进到第一个 yield 表达式
corou.__next__()

# 发送输入
corou.send("Haiyong")
corou.send("Dear Haiyong")

输出:

Searching prefix:Dear
Dear Haiyong

协程的执行

协程的执行类似于生成器。当我们调用协程时,什么都没有发生,它只在响应next()send ()方法时运行。在上面的例子中可以清楚地看到这一点,因为只有在调用__next__()方法之后,我们的协程才开始执行。在这个调用之后,执行前进到第一个 yield 表达式,现在执行暂停并等待值被发送到 corou 对象。当第一个值被发送给它时,它会检查前缀和打印名称(如果存在前缀)。打印完名称后,它会遍历循环,直到再次遇到name = (yield)表达式。

关闭协程

协程可能无限期运行,关闭协程使用close()方法。当协程关闭时,它会生成GeneratorExit异常,该异常可以以通常捕获的方式捕获。关闭协程后,如果我们尝试发送值,它将引发StopIteration异常。下面是一个简单的例子:

# Python3 program for demonstrating
# closing a coroutine

def print_name(prefix):
    print("Searching prefix:".format(prefix))
    try :
        while True:
                name = (yield)
                if prefix in name:
                    print(name)
    except GeneratorExit:
            print("关闭协程!!")

corou = print_name("Dear")
corou.__next__()
corou.send("Haiyong")
corou.send("Dear Haiyong")
corou.close()

输出:

搜索前缀:Dear 
Dear Haiyong
关闭协程!!

链接协程以创建管道

协程可用于设置管道。我们可以使用 send() 方法将协程链接在一起并通过管道推送数据。管道需要:

  • 初始源(生产者)派生整个管道。生产者通常不是协程,它只是一个简单的方法。
  • 一个 sink,它是管道的端点。接收器可能会收集所有数据并显示它。

以下是一个简单的链接示例

# 用于演示协程链接的 Python 程序

def producer(sentence, next_coroutine):
    '''
    producer 只是拆分字符串并将其
    提供给 pattern_filter 协程
    '''
    tokens = sentence.split(" ")
    for token in tokens:
        next_coroutine.send(token)
    next_coroutine.close()

def pattern_filter(pattern="ing", next_coroutine=None):
    '''
    在接收到的令牌中搜索模式,如果模式匹配,
    将其发送到 print_token() 协程进行打印
    '''
    print("Searching for ".format(pattern))
    try:
        while True:
            token = (yield)
            if pattern in token:
                next_coroutine.send(token)
    except GeneratorExit:
        print("过滤完成!!")

def print_token():
    '''
    充当接收器,只需打印接收到的令牌
    '''
    print("我沉了,我会打印令牌")
    try:
        while True:
            token = (yield)
            print(token)
    except GeneratorExit:
        print("打印完成!")

pt = print_token()
pt.__next__()
pf = pattern_filter(next_coroutine = pt)
pf.__next__()

sentence = "Haiyong is running behind a fast moving car"
producer(sentence, pf)

输出:

我沉了,我会打印令牌
Searching for ing
running 
moving 
过滤完成!
打印完成!

总结

1.线程和协程推荐在 IO 密集型的任务(比如网络调用)中使用,而在CPU密集型的任务中,表现较差。
2.对于CPU密集型的任务,则需要多个进程,绕开GIL的限制,利用所有可用的CPU核心,提高效率。
3.在高并发下的最佳实践就是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。

  • CPU 密集型: 多进程
  • IO 密集型: 多线程(协程维护成本较高,而且在读写文件方面效率没有显著提升)
  • CPU 密集和 IO 密集: 多进程+协程

以上是关于Python 高级教程之线程进程和协程的主要内容,如果未能解决你的问题,请参考以下文章

Flask 之分析线程和协程

Python:简述 线程进程和协程

简述python进程,线程和协程的区别及应用场景

线程池Python 线程进程和协程

Python进程线程和协程实战指归

Python进程线程和协程实战指归