网络与并发之多线程与多进程

Python-网络与并发

第五章 多线程与多进程

​ 现代操作系统比如 Mac OS X,Linux,Windows 等,都是支持“多任务”的操作系统什 么叫“多任务”呢?简单地说,就是操作系统可以同时运行多个任务。打个比方,你一边在用 逛淘宝,一边在听音乐,一边在用微信聊天,这就是多任务,至少同时有 3 个任务正在运行。

在这里插入图片描述

​ 还有很多任务悄悄地在后台同时运行着,只是桌面上没有显示而已。现在,多核 CPU 已经非常普及了,但是,即使过去的单核 CPU,也可以执行多任务。由 于 CPU 执行代码都是顺序执行的,那么,单核 CPU 是怎么执行多任务的呢?

在这里插入图片描述

​ 我们知道,在一台计算机中,我们可以同时打开许多软件,比如同时浏览网页、听音乐、打字等等,看似非常正常。但仔细想想,为什么计算机可以做到这么多软件同时运行呢?这就涉及到计算机中的两个重要概念:多进程和多线程了。

进程和线程都是操作系统中的重要概念,既相似,又不同

对于一般的程序,可能会包含若干进程;而每一个进程又可能包含多个同时执行的线程。进程是资源管理的最小单位,而线程则是程序执行的最小单位。

1. 进程和线程的概念

​ 对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动 一个浏览器进程,就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开 一个 Word 就启动了一个 Word 进程。

进程:直观地说,进程就是正在执行的程序,为多任务操作系统中执行任务的基本单元,是包含了程序指令和相关资源的集合。在Windows下,可以打开任务管理器,在进程标签栏中就可以看到当前计算机中正在运行的进程,操作系统隔离各个进程可以访问的地址空间。如果进程间需要传递信息,则需要使用进程间通信或者其他方式,非常不方便而且消耗CPU时间片段。为了能够更好地支持信息共享和减少切换开销,从进程中演化出了线程。

线程:线程是进程的执行单元。对于大多数程序来说,可能只有一个主线程。但是,为了能够提高效率,有些程序会采用多线程,在系统中所有的线程看起来都是同时执行的。例如,现在的多线程网络下载程序中,就使用了这种线程并发的特性,程序将欲下载的文件分成多个部分,然后同时进行下载,从而加快速度。

进程和线程的对比

​ 明确进程和线程的区别,这一点对于使用Python编程是非常重要的。一般的,进程是重量级的。具体包括进程映像的结构、执行细节以及进程间切换的方法。在进程中,需要处理的问题包括进程间通信、临界区管理和进程调度等。这些特性使得新生成一个进程的开销比较大。而线程刚好相反,它是轻量级的。线程之间共享许多资源,容易进行通信,生成一个线程的开销较小。但是使用线程会有死锁、数据同步和实现复杂等问题。

在这里插入图片描述

并发编程解决方案

  • 启动多个进程,每个进程虽然只有一个线程,但多个进程可以一块执行多个任务
  • 启动一个进程,在一个进程内启动多个线程,这样,多个线程也可以一块执行多个任务
  • 启动多个进程,每个进程再启动多个线程,这样同时执行的任务就更多了,当然这种模型更复杂,实际很少采用。

​ 由于Python语言使用了全局解释器锁(Global Interpretor Lock,GIL)和队列模块,其在线程实现的复杂度上相对于其他语言来说要低得多。需要注意的是,由于GIL的存在,所以Python解释器并不是线程安全的。因为当前线程必须持有这个全局解释器锁,才可以安全地访问Python对象。虽然使用GIL使得Python不能够很好地利用多CPU优势,但是现在还没有比较好的办法来代替它,因为去掉GIL会带来许多问题。
所以,针对I/O受限的程序,如网络下载类,可以使用多线程来提高程序性能。而对于CPU受限的程序,如科学计算类,使用多线程并不会带来效率的提升。这个时候,建议使用进程或者混合进程和线程的方法来实现。

2. 进程的开发

​ 在前面提到,Python对于进程和线程处理都有很好的支持。接下来介绍在Python语言的标准库中相关的模块:

模块 介绍 模块 介绍
os/sys 包含基本进程管理函数 subprocess Python基本库中多进程编程相关模块,适用于与外部进程交互,调用外部进程;
multiprocessing 也是Python基本库中多进程编程模块,核心机制是fork,重开一个进程,首先会把父进程的代码copy重载一遍 threading Python基本库中多线程管理相关模块

创建进程

subprocess模块

​ subprocess最早在2.4版本引入。用来生成子进程,并可以通过管道连接他们的输入/输出/错误,以及获得他们的返回值。用来替换多个旧模块和函数

​ 运行python的时候,我们都是在创建并运行一个进程,(linux中一个进程可以fork一个子进程,并让这个子进程exec另外一个程序)。在python中,我们通过标准库中的subprocess包来fork一个子进程,并且运行一个外部的程序。subprocess包中定义有数个创建子进程的函数,这些函数分别以不同的方式创建子进程,所欲我们可以根据需要来从中选取一个使用。另外subprocess还提供了一些管理标准流(standard stream)和管道(pipe)的工具,从而在进程间使用文本通信。

通俗地说就是通过这个模块,你可以在Python的代码里执行操作系统级别的命令,比如“ipconfig”、“du -sh”等等。subprocess模块替代了一些老的模块和函数,比如:

os.system
os.spawn*

subprocess过去版本中的call,check_call和check_output已经被run方法取代了。run方法为3.5版本新增。大多数情况下,推荐使用run方法调用子进程,执行操作系统命令。在更高级的使用场景,你还可以使用Popen接口。其实run()方法在底层调用的就是Popen接口。

run

subprocess 模块首先推荐使用的是它的 run 方法,更高级的用法可以直接使用 Popen 接口。

注意,run()方法返回的不是我们想要的执行结果或相关信息,而是一个CompletedProcess类型对象。

subprocess.run(args, *, stdin=None, input=None, stdout=None, stderr=None, capture_output=False, shell=False, cwd=None, timeout=None, check=False, encoding=None, errors=None, text=None, env=None, universal_newlines=None)
  • args:表示要执行的命令。必须是一个字符串,字符串参数列表。
  • stdin、stdout 和 stderr:子进程的标准输入、输出和错误。其值可以是 subprocess.PIPE、subprocess.DEVNULL、一个已经存在的文件描述符、已经打开的文件对象或者 None。subprocess.PIPE 表示为子进程创建新的管道。subprocess.DEVNULL 表示使用 os.devnull。默认使用的是 None,表示什么都不做。另外,stderr 可以合并到 stdout 里一起输出。
  • timeout:设置命令超时时间。如果命令执行时间超时,子进程将被杀死,并弹出 TimeoutExpired 异常。
  • check:如果该参数设置为 True,并且进程退出状态码不是 0,则弹 出 CalledProcessError 异常。
  • encoding: 如果指定了该参数,则 stdin、stdout 和 stderr 可以接收字符串数据,并以该编码方式编码。否则只接收 bytes 类型的数据。
  • shell:如果该参数为 True,将通过操作系统的 shell 执行指定的命令。

run 方法调用方式,返回 CompletedProcess 实例,和直接 Popen 差不多,实现是一样的,实际也是调用 Popen,与 Popen 构造函数大致相同,看一个例子

# coding=utf-8
# 文件名:subprocess_cmd.py

# 导入模块subprocess
import subprocess

# 这里我们使用了这么几个参数args,encoding,shell
# dir 在windows命令行中是遍历这个目录下的文件
# Linux可以使用 ls 命令
runcmd = subprocess.run(['dir', 'C:\\'], encoding='utf8', shell=True)

# 打印结果
print(runcmd)

看看运行结果

 驱动器 C 中的卷没有标签。
 卷的序列号是 0AA3-2019

 C:\ 的目录

2018/03/16  10:16    <DIR>          EFI
2020/03/18  17:33    <DIR>          Intel
2020/07/13  11:10    <DIR>          LeakHotfix
2020/07/15  16:50    <DIR>          Program Files
2020/07/18  17:40    <DIR>          Program Files (x86)
2020/06/22  11:51    <DIR>          QMDownload
2020/03/18  17:43    <DIR>          Users
2020/07/18  16:55    <DIR>          Windows
               0 个文件              0 字节
               8 个目录 90,635,427,840 可用字节
CompletedProcess(args=['dir', 'C:\\'], returncode=0)

在这里插入图片描述

这里我们看到不仅执行了命令,并且返回了一个 CompletedProcess 实例,其中returncode: 执行完子进程状态,通常返回状态为0则表明它已经运行完毕,若值为负值 “-N”,表明子进程被终。

定义一个函数,写一个基于windows cmd命令行的实例

# coding=utf-8
# 文件名:subprocess_cmd_1.py

# 导入模块subprocess
import subprocess


# 定义一个函数run_cmd进行subprocess.run操作
def run_cmd(command):

    # subprocess.run实例化一个变量return_cmd,需要注意一点,
    # 因为做subprocess.PIPE 有字符需要解码,我这里的encoding使用的是GBK,
    # 这是Windows默认常用的编码字符,因为存在部分中文字符存在异常,当然可以修改,Linux下的是utf8,注意区分
    return_cmd = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='GB18030', shell=True,)

    # 判断实例化属性是否有存在异常returncode
    if return_cmd.returncode == 0:
        print("success:")
        print(return_cmd)
    else:
        print("error:")
        print(return_cmd)


run_cmd(["dir", "C:\\"])  # 序列参数
run_cmd("exit 1")  # 字符串参数

执行结果:

success:
CompletedProcess(args=['dir', 'C:\\'], returncode=0, stdout='
驱动器 C 中的卷没有标签。
卷的序列号是 0AA3-2019

C:\\ 的目录

2018/03/16  10:16    <DIR>          EFI
2020/03/18  17:33    <DIR>          Intel
2020/07/13  11:10    <DIR>          LeakHotfix
2020/07/15  16:50    <DIR>          Program Files
2020/07/18  17:40    <DIR>          Program Files (x86)
2020/06/22  11:51    <DIR>          QMDownload
2020/03/18  17:43    <DIR>          Users
2020/07/18  16:55    <DIR>          Windows
0 个文件              0 字节
8 个目录 89,087,983,616 可用字节
', stderr='')
error:
CompletedProcess(args='exit 1', returncode=1, stdout='', stderr='')

在这里插入图片描述

我们看到对应的两条命令是没有问题的,成功与错误的信息与returncode的代码显示提示一致,我们的结果也相应地打印处理出来,详解一下参数

1、args 启动进程的参数,通常是个列表或字符串。

2、returncode 进程结束状态返回码。0表示成功状态。

3、stdout 获取子进程的stdout。通常为bytes类型序列,None表示没有捕获值。如果你在调用run()方法时,设置了参数stderr=subprocess.STDOUT,则错误信息会和stdout一起输出,此时stderr的值是None。

4、stderr 获取子进程的错误信息。通常为bytes类型序列,None表示没有捕获值。

5、check_returncode() 用于检查返回码。如果返回状态码不为零,弹出CalledProcessError异常。

6、subprocess.DEVNULL用于传递给stdout、stdin和stderr参数。表示使用os.devnull作为参数值。

7、subprocess.PIPE管道,可传递给stdout、stdin和stderr参数。

8、subprocess.STDOUT特殊值,可传递给stderr参数,表示stdout和stderr合并输出。

这里再详细介绍一下args与shell的参数

args参数可以接收一个类似'ls -la'的字符串,也可以传递一个类似['ls', '/b']的字符串分割列表

shell参数默认为False,设置为True的时候表示使用操作系统的shell执行命令

一般在命令行里面运行,或者是Linux里面使用时候会默认设置为shell=True

而且在Linux的环境中,args参数为字符串是,shell必须为True

而在Windows的环境中,args参数不论是字符串还是列表,shell建议为True

但是不是所有的操作系统命令都像‘dir’或者‘ipconfig’那样单纯地返回执行结果,还有很多像‘python’这种交互式的命令,如果需要输入点什么,然后它返回执行的结果。使用run()方法怎么向stdin里输入?

错误代码示范

import subprocess

ret = subprocess.run("python", stdin=subprocess.PIPE, stdout=subprocess.PIPE,shell=True)
ret.stdin = "print('haha')"     # 错误的用法
print(ret)

这样是不行的,ret作为一个CompletedProcess对象,根本没有stdin属性。那怎么办呢?前面说了,run()方法的stdin参数可以接收一个文件句柄。比如在一个1.txt文件中写入print('hello,python')。然后参考下面的使用方法

>>> import subprocess
>>> fd = open("D:\\1.txt")
>>> ret = subprocess.run("python", stdin=fd, stdout=subprocess.PIPE, shell=True)
>>> print(ret.stdout.decode('utf8'))
hello,python

>>> fd.close()
>>>

这样做,虽然可以达到目的,但是很不方便,也不是以代码驱动的方式。这个时候,我们可以使用Popen类。

Popen

Popen 是 subprocess的核心,子进程的创建和管理都靠它处理。

用法和参数与run()方法基本类同,但是注意哦,

它的返回值是一个Popen对象,而不是CompletedProcess对象。

subprocess模块中定义了一个Popen类,通过它可以来创建进程,并与其进行复杂的交互。查看一下它的构造函数:

构造函数:

class subprocess.Popen(args, bufsize=-1, executable=None, stdin=None, stdout=None, stderr=None, preexec_fn=None, close_fds=True, shell=False, cwd=None, env=None, universal_newlines=False, startupinfo=None, creationflags=0,restore_signals=True, start_new_session=False, pass_fds=(), * , encoding=None, errors=None)

常用的参数有:

  • args:shell命令,可以是字符串或者序列类型(如:list,元组)
  • bufsize:缓冲区大小。当创建标准流的管道对象时使用,默认-1。
    0:不使用缓冲区
    1:表示行缓冲,仅当universal_newlines=True时可用,也就是文本模式
    正数:表示缓冲区大小
    负数:表示使用系统默认的缓冲区大小。
  • stdin, stdout, stderr:分别表示程序的标准输入、输出、错误句柄
  • preexec_fn:只在 Unix 平台下有效,用于指定一个可执行对象(callable object),它将在子进程运行之前被调用
  • shell:如果该参数为 True,将通过操作系统的 shell 执行指定的命令。
  • cwd:用于设置子进程的当前目录。
  • env:用于指定子进程的环境变量。如果 env = None,子进程的环境变量将从父进程中继承。

创建一个子进程,然后执行一个简单的命令:

# coding=utf-8
# 文件名:subprocess_cmd.py

# 导入模块subprocess
import subprocess

# 这里我们使用了这么几个参数args,encoding,shell
# dir 在windows命令行中是遍历这个目录下的文件
# Linux可以使用 ls 命令
runcmd = subprocess.Popen(['dir', 'C:\\'], encoding='utf8', shell=True)

# 打印结果
print(runcmd)

查看运行结果:

<subprocess.Popen object at 0x00000295D613C2C8>
 驱动器 C 中的卷没有标签。
 卷的序列号是 0AA3-2019

 C:\ 的目录

2018/03/16  10:16    <DIR>          EFI
2020/03/18  17:33    <DIR>          Intel
......

在这里插入图片描述

Popen对象的stdin、stdout和stderr是三个文件句柄,可以像文件那样进行读写操作。而且我们看到其返回值是一个Popen的对象,

>>> import subprocess
>>> ret = subprocess.Popen("python", stdout=subprocess.PIPE, stdin=subprocess.PIPE, shell=True)
>>> ret.stdin.write(b"import os\n")
10
>>> ret.stdin.write(b"print(os.environ)")
17
>>> ret.stdin.close()
>>> out = ret.stdout.read().decode("GBK")
>>> ret.stdout.close()
>>> print(out)
environ({
    
    
    'ALLUSERSPROFILE': 'C:\\ProgramData',
    '......',
    'USERPROFILE': 'C:\\Users\\Administrator.SC-202003181819', 
    'WINDIR': 'C:\\Windows'
})

这里我们看到,通过s.stdin.write()可以输入数据,而s.stdout.read()则能输出数据。自由的在交互程序,进行交互信息,完成数据的整体提取或写入,这些事Popen类模块提供的比较多的方法

下面是其他的一些比较常用的方法

  • poll(): 检查进程是否终止,如果终止返回 returncode,否则返回 None。
  • wait(timeout): 等待子进程终止。
  • communicate(input,timeout): 和子进程交互,发送和读取数据。
  • send_signal(singnal): 发送信号到子进程 。
  • terminate(): 停止子进程,也就是发送SIGTERM信号到子进程。
  • kill(): 杀死子进程。发送 SIGKILL 信号到子进程。

multiprocessing模块

​ multiprocessing 是一个用与 threading 模块相似API的支持产生进程的包。 multiprocessing 包同时提供本地和远程并发,使用子进程代替线程,有效避免 Global Interpreter Lock 带来的影响。因此, multiprocessing 模块允许程序员充分利用机器上的多个核心。Unix 和 Windows 上都可以运行。
​ multiprocessing 模块还引入了在 threading 模块中没有类似物的API。这方面的一个主要例子是 Pool 对象,它提供了一种方便的方法,可以跨多个输入值并行化函数的执行,跨进程分配输入数据(数据并行)。

multiprocessing模块的功能众多,支持子进程,通信,共享数据,执行不同形式的同步。为些它提供了Process、Queue、Pipe、Lock等组件

注意:使用 multiprocessing模块if __name__ == '__main__' 部分是必需的,这样避免避免共享状态,避免杀死进程等。总而言之:这是官方规定的编程习惯。所以我们有两个方式创建进程:

1、第一种:函数包装:使用一个子进程调用某一个函数

# -*- coding: UTF-8 -*-
# 文件名:Process_a.py

# 导入模块
from multiprocessing import Process
import os
from time import sleep, time


def test1(name):
    '''
    测试进程
    :param name: 进程对象
    :return: 无返回值
    '''

    print("当前进程的ID", os.getpid())
    print("父进程的ID", os.getppid())
    print("当前进程的名字:", name)
    # 休息3秒
    sleep(3)

# 入口
if __name__ == '__main__':
    start = time()
    # 创建多个子进程,并且把这些子进程放入列表中
    process_list = []
    print("主进程的ID", os.getpid())
    for i in range(10):
        # args:表示被调用对象的位置参数元组,这里Process就是属于Process类
        p = Process(target=test1, args=('process-%s' % i,))
        # 开始进程
        p.start()
        process_list.append(p)

既然要等到子进程结束后再执行父进程的后续部分,那么是不是感觉到这样多进程就没什么用了?其实不然,一般情况下我们的父进程是不会执行任何其它操作的,它会创建多个子进程来进行任务的处理。当这些子进程全部结束完成后,我们再关闭我们的父进程。

注意: join是等待当前的进程结束

2、第二种:类包装:自定义一个Process进程类,该类中的run函数由一个子进程调用执行

​ 继承Process类,重写run方法就可以了

# -*- coding: UTF-8 -*-
# 文件名:Process_b.py
# 导入模块
from multiprocessing import Process
import os
from time import sleep, time


# 自定义一个进程类 继承Process类
class MyProcess(Process):
    def __init__(self, name):
        Process.__init__(self)
        self.name = name

    def run(self):
        '''
        重写run方法
        :return: 无返回值
        '''

        print("当前进程的ID", os.getpid())
        print("父进程的ID", os.getppid())
        print("当前进程的名字:", self.name)
        sleep(3)


# 入口
if __name__ == '__main__':
    print("主进程ID", os.getpid())
    # 返回当前时间的时间戳
    start = time()
    process_list = []
    for i in range(10):
        # args:表示被调用对象的位置参数元组
        p = MyProcess("process-%s" % i)
        # 开始进程
        p.start()
        process_list.append(p)

    for p in process_list:
        # 我们一般都会需要父进程等待子进程结束再执行父进程后面的代码,需要加join,等待所有的子进程结束
        p.join()
        # 计算时间 每个子进程开始至接受结束运行时间,浮点秒数
        end = time() - start
        print(end)

总结:

  • 使用进程优点:

    可以使用计算机多核,进行任务的并发执行,提高执行效率

    运行不受其他进程影响,创建方便

    空间独立,数据安全

  • 使用进程缺点:

    进程的创建和删除消耗的系统资源较多

  • 全局变量在多个进程中不能共享

    在子进程中修改全局变量对父进程中的全局变量没有影响。因为父进程在创建子进程时对全局变量做了一个备份,父进程中的全局变量与子进程的全局变量完全是不同的两个变量。全局变量在多个进程中不能共享。

进程间的通信

Python 提供了多种实现进程间通信的机制,主要有以下 2 种:

  1. Python multiprocessing 模块下的 Queue 类,提供了多个进程之间实现通信的诸多方法

  2. Pipe,又被称为“管道”,常用于实现 2 个进程之间的通信,这 2 个进程分别位于管道的两端

Queue 实现进程间通信

​ 需要使用 multiprocessing 模块中的 Queue 类。简单的理解 Queue 实现进程间通信的方式,就是使用了操作系统给开辟的一个队列空间,各个进程可以把数据放到该队列中,当然也可以从队列中把自己需要的信息取走。

​ 现在有这样一个需求:我们有两个进程,一个进程负责写(write)一个进程负责读(read)。当写的进程写完某部分以后要把数据交给读的进程进行使用, 这时候我们就需要使用到了multiprocessing模块的Queue(队列):write()将写完的数据交给队列,再由队列交给read()

# -*- coding: UTF-8 -*-
# 文件名:Queue_b.py
# 导入模块
import os, time
from multiprocessing import Process, Queue


class WriterProcess(Process):
    '''自定义写类,继承Process'''
    def __init__(self, name, mq):
        '''
        初始化
        :param name: 写名称
        :param mq: 子进程
        '''
        Process.__init__(self)
        self.name = name
        self.mq = mq

    def run(self):
        '''
        重写run方法
        :return: 无返回值
        '''
        print("进程%s,已经启动,ID是:%s" % (self.name, os.getpid()))
        for i in range(4):
            self.mq.put(i)  # writer进程负责把数据写出去
            time.sleep(1)
        print("进程%s,已经结束" % self.name)


class ReaderProcess(Process):
    '''自定义读类,继承Process'''
    def __init__(self, name, mq):
        '''
        初始化
        :param name: 读名称
        :param mq: 子进程
        '''
        Process.__init__(self)
        self.name = name
        self.mq = mq

    def run(self):
        '''
        重写run方法
        :return: 无返回值
        '''
        while True:  # 阻塞(必须要用),等待获取write的值
            value = self.mq.get(True)
            print(value)
        print("结束子进程:%s" % self.name)  # 永远不会执行


if __name__ == '__main__':
    '''入口'''
    # 父进程创建队列,并传递给子进程
    q = Queue()
    pw = WriterProcess("write", q)
    pr = ReaderProcess("read", q)
    # 进程开始
    pw.start()
    pr.start()

    # 进程结束
    pw.join()
    # pr进程是一个死循环,无法等待其结束,只能强行结束(写进程结束了,所以读进程也可以结束了)
    pr.terminate()
    print("父进程结束")

注意: 代码里面的while循环、join、terminate等函数

Pipe 实现进程间通信

​ Pipe 直译过来的意思是“管”或“管道”,该种实现多进程编程的方式,和实际生活中的管(管道)是非常类似的。通常情况下,管道有 2 个口,而 Pipe 也常用来实现 2 个进程之间的通信,这 2 个进程分别位于管道的两端,一端用来发送数据,另一端用来接收数据。

1、send(obj)

​ 发送一个 obj 给管道的另一端,另一端使用 recv() 方法接收。需要说明的是,该 obj 必须是可序列化的,如果该对象序列化之后超过 32MB,则很可能会引发 ValueError 异常。

2、recv()

​ 接收另一端通过 send() 方法发送过来的数据

3、close()

​ 关闭连接

4、poll([timeout])

​ 返回连接中是否还有数据可以读取

5、end_bytes(buffer[, offset[, size]])

​ 发送字节数据。如果没有指定 offset、size 参数,则默认发送 buffer 字节串的全部数据;如果指定了 offset 和 size 参数,则只发送 buffer 字节串中从 offset 开始、长度为 size的字节数据。通过该方法发送的数据,应该使用 recv_bytes() 或 recv_bytes_into 方法接收。

6、recv_bytes([maxlength])

​ 接收通过 send_bytes() 方法发送的数据,maxlength 指定最多接收的字节数。该方法返回接收到的字节数据

7、recv_bytes_into(buffer[, offset])

​ 功能与 recv_bytes() 方法类似,只是该方法将接收到的数据放在 buffer 中

# -*- coding: UTF-8 -*-
# 文件名:Pipe_a.py
# 导入的模块
import os, time
import multiprocessing
from multiprocessing import Process, Pipe


class WriterProcess(Process):
    '''写类,继承Process'''
    def __init__(self, name, pip):
        # 初始化
        Process.__init__(self)
        self.name = name
        self.pip = pip

    def run(self):
        '''重写run方法'''

        print("进程%s,已经启动,ID是:%s" % (self.name, multiprocessing.current_process().pid))
        print("进程%s,已经启动2222,ID是:%s" % (self.name, os.getpid()))
        for i in range(4):
            # 子进程通过管道写数据出去
            self.pip.send(i)  # writer进程负责把数据写出去
            time.sleep(1)
        print("进程%s,已经结束" % self.name)


class ReaderProcess(Process):
    '''读类,继承Process'''
    def __init__(self, name, pip):
        '''初始化'''
        Process.__init__(self)
        self.name = name
        self.pip = pip

    def run(self):
        '''重写run方法'''
        while True:  # 阻塞(必须要用),等待获取write的值
            # 子进程通过管道存好的数据获取
            value = self.pip.recv()
            print(value)
        print("结束子进程:%s" % self.name)  # 永远不会执行


if __name__ == '__main__':
    # 父进程创建两个pipe,并传递给子进程
    p1, p2 = Pipe()
    pw = WriterProcess("write", p1)
    pr = ReaderProcess("read", p2)
    pw.start()
    pr.start()
    pw.join()
    # pr进程是一个死循环,无法等待其结束,只能强行结束(写进程结束了,所以读进程也可以结束了)
    pr.terminate()
    print("父进程结束")

进程池Pool

​ Python 提供了更好的管理多个进程的方式,就是使用进程池。进程池可以提供指定数量的进程给用户使用,即当有新的请求提交到进程池中时,如果池未满,则会创建一个新的进程用来执行该请求;反之,如果池中的进程数已经达到规定最大值,那么该请求就会等待,只要池中有进程空闲下来,该请求就能得到执行。

使用进程池的优点

  1. 提高效率,节省开辟进程和开辟内存空间的时间及销毁进程的时间

  2. 节省内存空间

Pool中的函数说明:

  • Pool(12):创建多个进程,表示可以同时执行的进程数量。默认大小是CPU的核心数果。

  • join():进程池对象调用join,会等待进程池中所有的子进程结束完毕再去结束父进程。

  • close():如果我们用的是进程池,在调用join()之前必须要先close(),并且在close()之后不能再继续往进程池添加新的进程

  • pool.apply_async(func,args,kwds) : 异步执行 ;将事件放入到进程池队列 。args以元组的方式传参,kwds以字典的方式传参。

  • pool.apply_async(func,args,kwds):同步执行;将事件放入到进程池队列。

    这里解释一下同步与异步

    同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication)。

    同步,就是调用某个东西时,调用方得等待这个调用返回结果才能继续往后执行。

    异步,和同步相反 调用方不会理解得到结果,而是在调用发出后调用者可用继续执行后续操作,被调用者通过状体来通知调用者,或者通过回掉函数来处理这个调用

# -*- coding: UTF-8 -*-
# 文件名:Pool_a.py
# 导入的模块
import random
from multiprocessing.pool import Pool
from time import sleep, time

import os


def run(name):
    '''自定义run方法'''
    print("%s子进程开始,进程ID:%d" % (name, os.getpid()))
    start = time()
    # 随机休息时间
    sleep(random.choice([1, 2, 3, 4]))
    end = time()
    # 执行进程的时间
    print("%s子进程结束,进程ID:%d。耗时0.2%f" % (name, os.getpid(), end - start))


if __name__ == "__main__":
    '''入口'''
    print("父进程开始")
    # 创建多个进程,表示可以同时执行的进程数量。默认大小是CPU的核心数
    p = Pool(4)
    for i in range(10):
        # 创建进程,放入进程池统一管理,异步非阻塞式
        p.apply_async(run, args=(i,))
    # 如果我们用的是进程池,在调用join()之前必须要先close(),并且在close()之后不能再继续往进程池添加新的进程
    p.close()
    # 进程池对象调用join,会等待进程吃中所有的子进程结束完毕再去结束父进程
    p.join()
    print("父进程结束。")

**注意:**因为我们Pool(4)指定了同时最多只能执行4个进程(Pool进程池默认大小是CPU的核心数),但是我们多放入了6个进程进入我们的进程池,所以程序一开始就会只开启4个进程。
​ 而且子进程执行是没有顺序的,先执行哪个子进程操作系统说了算的。而且进程的创建和销毁也是非常消耗资源的,所以如果进行一些本来就不需要多少耗时的任务你会发现多进程甚至比单进程还要慢

3. 线程的开发

​ Python 的标准库提供了两个模块:_thread 和 threading,_thread 是低级模块,threading 是高级模块,对_thread 进行了封装。绝大多数情况下,我们只需要使用threading 这个高级模块。

多线程概念

​ 多线程使得系统可以在单独的进程中执行并发任务。虽然进程也可以在独立的内存空间中并发执行,但是其系统开销会比较大。生成一个新进程必须为其分配独立的地址空间,并维护其代码段、堆栈段和数据段等,这种开销是巨大的。另外,进程间的通信实现也不方便。在程序功能日益复杂的时候,需要有更好的系统模型来满足要求,线程由此产生了。
​ 线程是“轻量级”的,一个进程中的线程使用同样的地址空间,且共享许多资源。启动线程的时间远远小于启动进程的时间和空间,而且,线程间的切换也要比进程间的切换快得多。由于使用同样的地址空间,所以线程之间的数据通信比较方便,一个进程下的线程之间可以直接使用彼此的数据。当然,这种方便性也会带来一些问题,特别是同步问题。
​ 多线程对于那些I/O受限的程序特别适用。其实使用多线程的一个重要目的,就是最大化地利用CPU的资源。当某一线程在等待I/O的时候,另外一个线程可以占用CPU资源。如最简单的GUI程序,一般需要有一个任务支持前台界面的交互,还要有一个任务支持后台的处理。这时候,就适合采用线程模型,因为前台UI是在等待用户的输入或者鼠标单击等操作。除此之外,多线程在网络领域和嵌入式领域的应用也比较多。

多线程类似于同时执行多个不同程序,多线程运行有如下优点:

  • 使用线程可以把占据长时间的程序中的任务放到后台去处理。
  • 用户界面可以更加吸引人,比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度。
  • 程序的运行速度可能加快。
  • 在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,线程就比较有用了。在这种情况下我们可以释放一些珍贵的资源如内存占用等等。

每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

每个线程都有他自己的一组CPU寄存器,称为线程的上下文,该上下文反映了线程上次运行该线程的CPU寄存器的状态。

指令指针和堆栈指针寄存器是线程上下文中两个最重要的寄存器,线程总是在进程得到上下文中运行的,这些地址都用于标志拥有线程的进程地址空间中的内存

线程可以分为:

  • **内核线程:**由操作系统内核创建和撤销。
  • **用户线程:**不需要内核支持而在用户程序中实现的线程。

线程的状态

​ 一个线程在其生命周期内,会在不同的状态之间转换。在任何一个时刻,线程总是处于某种线程状态中。虽然不同的操作系统可以实现不同的线程模型,定义不同的线程状态,但是总的说来,一个线程模型中下面几种状态是通用的。
1、就绪状态:线程已经获得了除CPU外的其他资源,正在参与调度,等待被执行。当被调度选中后,将立即执行。
2、运行状态:占用CPU资源,正在系统中运行。
3、睡眠状态:暂时不参与调度,等待特定事件发生,如I/O事件。
4、中止状态:线程已经运行结束,等待系统回收其线程资源。
​ Python使用全局解释器锁(GIL)来保证在解释器中只包含一个线程,并在各个线程之间切换。当GIL可用的时候,处于就绪状态的线程在获取GIL后就可以运行了。线程将在指定的间隔时间内运行。当时间到期后,正在执行的线程将重新进入就绪状态并排队。GIL重新可用并且为就绪状态的线程获取。当然,特定的事件也有可能中断正在运行的线程。具体的线程状态转移将在下面进行详细介绍。

​ 现在,Python语言中已经为各种平台提供了多线程处理能力,包括Windows、Linux等系统平台。在具体的库上,提供了两种不同的方式。一种是低级的线程处理模块thread,仅仅提供一个最小的线程处理功能集,在实际的代码中最好不要直接使用;另外一种是高级的线程处理模块threading,现在大部分应用的线程实现都是基于此模块的。threading模块是基于thread模块的,部分实现思想来自于Java的threads类。
​ 多线程设计的最大问题是如何协调多个线程。因此,在threading模块中,提供了多种数据同步的方法。为了能够更好地实现线程同步,Python中提供了Queue模块,用来同步线程。在Queue模块中,含有一个同步的FIFO队列类型,特别适合线程之间的数据通信和同步。
​ 由于大部分程序并不需要有多线程处理的能力,所以在Python启动的时候,并不支持多线程。也就说,Python中支持多线程所需要的各种数据结构特别是GIL还没有创建。当Python虚拟机启动的时候,多线程处理并没有打开,而仅支持单线程。这样做的好处是使得系统处理更加高效。只有当程序中使用了如thread.start_new_thread等方法的时候,Python才意识到需要多线程处理的支持。这时,Python虚拟机才会自动创建多线程处理所需要的数据结构和GIL。

创建线程

Python3 通过两个标准库 _thread 和 threading 提供对线程的支持。

_thread 提供了低级别的、原始的线程以及一个简单的锁,它相比于 threading 模块的功能还是比较有限的。

threading 模块除了包含 _thread 模块中的所有方法外,还提供的其他方法:

  • threading.currentThread(): 返回当前的线程变量。
  • threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
  • threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。

我们的线程创建方法也类似进程两种,第一种函数包装,即方法包装,另一种是类包装,线程的执行统一通过start()方法。

使用_thread模块

​ thread模块作为低级模块,虽然不推荐直接使用,但是在某些简单场合也是可以试试的,因为其用法非常简单。其中的核心函数是start_new_thread方法,可以用来生成线程。此方法接受一个函数对象作为参数,必须是个元组tuple类型,此方法将会返回生成新线程的标识符。

# -*- coding: UTF-8 -*-
# 文件名:thread_b.py
# 导入的模块
import _thread
import time


def worker(index, create_time):  # 具体的线程
    '''工作进程'''
    # 打印时间现在时间与创建子进程时间之差 创建第几个子进程
    print(f"{(time.time() - create_time)}  {index}\n")
    # 打印第几个进程退出
    print(f"Thread {index} exit...\n" )


for index in range(5):
    # _thread模块中start_new_thread方法的创建子进程并开始,其中worker为需要执行的任务,(index, time.time())参数为元组
    _thread.start_new_thread(worker, (index, time.time()))  # 启动线程


​ 我们看到代码再for循环中生成5个子线程。一般说来,在进程启动的时候,会生成一个默认的线程,这个线程又叫作主线程。这里的主线程就是Python解释器。而由主线程衍生出来的线程称为子线程,同样也有自己的程序入口等。在大部分线程模型中,除了主线程比较特殊以外,其他线程之间并没有明显的从属关系或者层次关系。
​ 在最后4行代码中,在for循环中5次调用了_thread模块的start_new_thread方法,从而生成5个子线程。

​ 在start_new_thread函数中,后面有两个参数。其中一个是worker函数,这是在代码中定义的函数,另外一个是这个worker函数的参数,注意到这里的参数是一个元组。更具体的,在worker的两个参数中,第一个参数是index值,而第二个是线程创建的时间。
​ 在worker函数中,为了简单起见,仅仅是打印出了本线程运行所用的时间和其index值。其中,前者通过将打印时的时间和创建时间比较得到。

​ 可以多次运行结果来看看,输出会不一样,这也是多线程程序的一个特点。因为线程之间的调度是很难预知的。当然,如果执行次数足够多,有可能会出现乱序的情况,只是这种可能性比较低。下面将对代码稍加修改,使得线程的并发性得到体现。

# -*- coding: UTF-8 -*-
# 文件名:thread_b.py
# 导入的模块
import _thread
import time


def worker(index, create_time):  # 具体的线程
    '''工作进程'''
    time.sleep(1)
    # 打印时间现在时间与创建子进程时间之差 创建第几个子进程
    print(f"{(time.time() - create_time)}  {index}\n")
    # 打印第几个进程退出
    print(f"Thread {index} exit...\n" )


for index in range(5):
    # _thread模块中start_new_thread方法的创建子进程并开始,其中worker为需要执行的任务,(index, time.time())参数为元组
    _thread.start_new_thread(worker, (index, time.time()))  # 启动线程

print("Main thread exit...")
while 1:
    pass

​ 多次运行看看会发现,index值会出现不同的排序。另外,注意到运行结果里面打印的时间,也是存在无序的,并不是说结束时间早的反而在后面打印,而是与线程调度、输出缓冲是有关的。可以看出线程的并发性。
在上面的代码中,并没有出现退出线程的语句或者函数。实际上,当线程函数执行结束的时候,线程就已经默认终止了。当然,也可以使用thread模块中的显式退出方法exit,这将触发SystemExit异常。如果此异常没有被捕获,线程将会安静终止。注意,这里的线程终止并不会影响其他线程的运行。

下面也是一些_thread模块常用的方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4zzfnzMq-1596616396638)(/imgs/thread模块方法.png
)]

使用threading模块

​ 使用threading模块来创建线程是很方便的。简单地说,只要将类继承于threading.Thread,然后在__init__方法中调用threading.Thread类中的__init__方法,重写类的run方法就可以了。看一些例子。

单线程执行:

# -*- coding: UTF-8 -*-
# 文件名:threading_a.py

# 导入的模块
import time


def saySorry():
    print("这是单线程,单线程,单线程!")
    time.sleep(1)


if __name__ == "__main__":
    start = time.time()
    for i in range(5):
        saySorry()
    end = time.time()
    print(f'使用时间:{end - start}')

多线程执行:

# -*- coding: UTF-8 -*-
# 文件名:threading_b.py

# 导入的模块
import threading
import time


def saySorry():
    print("这是多线程,多线程,多线程")
    time.sleep(1)


if __name__ == "__main__":
    '''入口'''
    start = time.time()
    for i in range(5):
        # 通过threading下的Thread方法创建线程,并执行函数
        t = threading.Thread(target=saySorry)
        t.start()  # 启动线程,即让线程开始执行
    end = time.time()
    print(f'使用时间:{end - start}')
  1. 可以明显看出使用了多线程并发的操作,花费时间要短很多
  2. 创建好的线程,需要调用start()方法来启动

​ 我们从多线程与单线程对比,可以很明显的发现多线程的等待时间大大缩减了,程序是通过threading模块下的Thread的类,去实例化一个此对象,并调用方法,实现生成多线程,target参数表示线程需要执行的方法,通过对象的start的方法,开启线程。在使用start方法的时候需要注意,此方法一个线程最多只能调用一次

看下面的代码:

# -*- coding: UTF-8 -*-
# 文件名:threading_c.py

import threading
from time import sleep,ctime

def sing():
    for i in range(3):
        print("正在唱歌...%d"%i)
        sleep(1)

def dance():
    for i in range(3):
        print("正在跳舞...%d"%i)
        sleep(1)

if __name__ == '__main__':
    print('---开始---:%s'%ctime())

    t1 = threading.Thread(target=sing)
    t2 = threading.Thread(target=dance)

    t1.start()
    t2.start()

    sleep(5) # 屏蔽此行代码,试试看,程序是否会立马结束?
    print('---结束---:%s'%ctime())

​ 主线程会等待所有的子线程结束后才结束,运行看效果,当我们执行完所有的线程后,主线程后才会打印结束,,但是如果屏蔽,或者将我们的sleep(5)这行代码再来看尼,会发现主程序立马结束,但是线程还在运行?

join方法

​ 该方法将等待,一直到它调用的线程终止. 它的名字表示调用的线程会一直等待,直到指定的线程加入它.

​ 当一个进程启动之后,会默认产生一个主线程,因为线程是程序执行流的最小单元,当设置多线程时,主线程会创建多个子线程,在python中,默认情况下(其实就是setDaemon(False)),主线程执行完自己的任务以后,就退出了,此时子线程会继续执行自己的任务,直到自己的任务结束

​ 而join所完成的工作就是线程同步,即主线程任务结束之后,进入阻塞状态,一直等待其他的子线程执行结束之后,主线程在终止,需要注意的是,不要启动线程后立即join(),很容易造成串行运行,导致并发失效

函数写法

# -*- coding: UTF-8 -*-
# 文件名:join_a.py

# 方法包装-启动多线程
# 导入模块
from threading import Thread
from time import sleep, time


def run(name):
    '''执行任务'''
    print("Threading:{} start".format(name))
    sleep(3)
    print("Threading:{} end".format(name))


if __name__ == '__main__':
    '''入口'''
    # 开始时间
    start = time()
    # 创建线程列表
    t_list = []
    # 循环创建线程
    for i in range(10):
        t = Thread(target=run, args=('t{}'.format(i),))
        t.start()
        t_list.append(t)
    # 等待线程结束
    for t in t_list:
        t.join()
    # 计算使用时间
    end = time() - start
    print(end)

类写法

# -*- coding: UTF-8 -*-
# 文件名:join_b.py

# 类包装-启动多线程
from threading import Thread
from time import sleep, time


class MyThread(Thread):

    def __init__(self, name):
        Thread.__init__(self)
        self.name = name

    def run(self):
        print("Threading:{} start".format(self.name))
        sleep(3)
        print("Threading:{} end".format(self.name))


if __name__ == '__main__':
    '''入口'''
    # 开始时间
    start = time()
    # 创建线程列表
    t_list = []
    # 循环创建线程
    for i in range(10):
        t = MyThread(f"t{i}")
        t.start()
        t_list.append(t)
    # 等待线程结束
    for t in t_list:
        t.join()
    # 计算时间
    end = time() - start
    print(end)

​ 我们可以看到,join方法下,我们等待子线程执行完后,结束的主线程。主线程在完成自己任务同时,阻塞状态下,等待子线程完成后结束进程。

setDaemon

​ 将线程声明为守护线程,必须在start() 方法调用之前设置, 如果不设置为守护线程程序会被无限挂起。这个方法基本和join是相反的。当我们 在程序运行中,执行一个主线程,如果主线程又创建一个子线程,主线程和子线程 就分兵两路,分别运行,那么当主线程完成想退出时,会检验子线程是否完成。如 果子线程未完成,则主线程会等待子线程完成后再退出。但是有时候我们需要的是 只要主线程完成了,不管子线程是否完成,都要和主线程一起退出,这时就可以 用setDaemon方法

如果没有用户线程,那么守护线程也没有存活下去的意义了

# -*- coding: UTF-8 -*-
# 文件名:threading_e.py

from threading import Thread
import time


def foo():
    print(123)
    time.sleep(1)
    print("end123")


def bar():
    print(456)
    time.sleep(3)
    print("end456")


t1 = Thread(target=foo)
t2 = Thread(target=bar)

t1.daemon = True
t1.start()
t2.start()
print("main-------")

​ 主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束。

​ 主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束。

这就是通过Thread中join方法,对每个线程都调用了join方法,让主线程等待子线程的完成

​ join方法还有一个可选的超时参数timeout。如果进程没有正常退出或者通过某个异常退出,且超时的情况下,主线程就不再等待子线程了。由于join的返回值始终是None,所以当在join方法中有超时参数的情况下,join返回后无法判断子线程是否已经结束。这个时候,则必须使用Thread类中的isAlive方法来判断是否发生了超时。

​ 在join中,使用join方法的时候,还需要注意以下问题。
1)在超时参数不存在的情况下,join操作将会一直阻塞,直到线程终止。
2)一个线程可以多次使用join方法。
3)线程不能在自己的运行代码中调用join方法,否则会造成死锁。
4)在线程调用start方法之前使用join方法,将会出现错误。

下面给出threading模块中Thread类的常用方法。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OkoSHDnY-1596616396641)(/imgs/threading模块Thread中方法.png
)]

同步

​ 一种数据之间同步的简单方法就是使用锁机制。这在低级thread模块和高级threading模块中都有提供。当然,threading模块中的锁机制也是基于thread模块实现的。在Python中,这是最低层次的数据同步原语。一个锁总是处于下面两种状态之中:“已锁”和“未锁”。为此提供了两种操作:“加锁”和“解锁”,分别用来改变锁的状态。对于一个锁来说,如果是未锁的状态,则线程在进入部分将此锁使用“加锁”操作将其状态变为“已锁”。

​ 由于同一进程中的所有线程都是共享数据的,如果对线程中数据的并发访问不加以限制,结果将不可预期,在严重的情况下,还会产生死锁。

# -*- coding: UTF-8 -*-
# 文件名:Share_a.py

from threading import Thread
import time

g_num = 100

def work1():
    global g_num
    for i in range(3):
        g_num += 1

    print("----in work1, g_num is %d---"%g_num)


def work2():
    global g_num
    print("----in work2, g_num is %d---"%g_num)


print("---线程创建之前g_num is %d---"%g_num)

t1 = Thread(target=work1)
t1.start()

#延时一会,保证t1线程中的事情做完
time.sleep(1)

t2 = Thread(target=work2)
t2.start()

看运行结果:

---线程创建之前g_num is 100---
----in work1, g_num is 103---
----in work2, g_num is 103---

Process finished with exit code 0

列表当做实参传递到线程中

# -*- coding: UTF-8 -*-
# 文件名:Share_b.py

from threading import Thread
import time

def work_1(nums):
    nums.append(44)
    print("----in work1---",nums)


def work_2(nums):
    #延时一会,保证t1线程中的事情做完
    time.sleep(1)
    print("----in work2---",nums)

g_nums = [11,22,33]

t1 = Thread(target=work_1, args=(g_nums,))
t1.start()

t2 = Thread(target=work_2, args=(g_nums,))
t2.start()
  • 在一个进程内的所有线程共享全局变量,能够在不适用其他方式的前提下完成多线程之间的数据共享(这点要比多进程要好)
  • 缺点就是,线程是对全局变量随意遂改可能造成多线程之间对全局变量的混乱(即线程非安全)

为了解决这个问题,需要允许线程独占地访问共享数据,这就是线程同步。需要注意的是,这些问题在进程中也是存在的,只是在多线程环境下更见常见而已。

​ 有时候需要在每个线程中使用各自独立的变量,一个显而易见的方法就是每个线程都使用自己的私有变量。为了方便,Python中提供了一种简单的机制threading.local来解决这个问题。其使用方法也很简单,其成员变量就是在每个线程中不同的。看代码

# -*- coding: UTF-8 -*-
# 文件名:local_a.py

import threading
import random, time


class ThreadLocal():
    '''线程锁'''
    def __init__(self):
        '''初始化'''
        self.local = threading.local()  # 生成local数据对象

    def run(self):
        '''执行程序run方法'''
        time.sleep(random.random())  # 随机休眠时间
        # 定义local数据对象属性number空列表
        self.local.number = []
        for _ in range(10):
            # 加入随机数
            self.local.number.append(random.choice(range(10))
        # 打印当前的线程对象,值
        print(threading.currentThread(), self.local.number)

# 创建对象
threadLocal = ThreadLocal()
threads = []
for i in range(5):
    t = threading.Thread(target=threadLocal.run())
    t.start()
    threads.append(t)
for i in range(5):
    threads[i].join()

​ 在ThreadLocal类中使用了threading.local()生成了类局部变量。此变量将在不同的线程中保存为不同的值。
​ ThreadLocal类中的run方法主要是将10个随机数放到前面生成的局部变量中,并打印出来。

# 下面是这段代码执行的一种结果。
<_MainThread(MainThread, started 12060)> [8, 9, 8, 4, 3, 1, 4, 4, 9, 5]
<_MainThread(MainThread, started 12060)> [3, 4, 9, 1, 2, 7, 0, 0, 1, 1]
<_MainThread(MainThread, started 12060)> [1, 9, 7, 8, 0, 7, 0, 9, 0, 2]
<_MainThread(MainThread, started 12060)> [2, 6, 3, 3, 8, 2, 6, 3, 5, 6]
<_MainThread(MainThread, started 12060)> [7, 3, 0, 4, 9, 9, 0, 4, 6, 4]

Process finished with exit code 0

# 从上面的输出结果中可以看到,每个线程都有自己不同的值。

同步锁与GIL的关系

GIL本质是一把互斥锁,但GIL锁住的是解释器级别的数据

同步锁,锁的是解释器以外的共享资源,例如:硬盘上的文件 控制台,对于这种不属于解释器的数据资源就应该自己加锁处理

​ Python的线程在GIL的控制之下,线程之间,对整个python解释器,对python提供的C API的访问都是互斥的,这可以看作是Python内核级的互斥机制。但是这种互斥是我们不能控制的,我们还需要另外一种可控的互斥机制———用户级互斥。内核级通过互斥保护了内核的共享资源,同样,用户级互斥保护了用户程序中的共享资源。

​ GIL 的作用是:对于一个解释器,只能有一个thread在执行bytecode。所以每时每刻只有一条bytecode在被执行一个thread。GIL保证了bytecode 这层面上是thread safe的。但是如果你有个操作比如 x += 1,这个操作需要多个bytecodes操作,在执行这个操作的多条bytecodes期间的时候可能中途就换thread了,这样就出现了data races的情况了。

​ 假设两个线程t1和t2都要对num=0进行增1运算,t1和t2都各对num修改10次,num的最终的结果应该为20。但是由于是多线程访问,有可能出现下面情况:

在num=0时,t1取得num=0。此时系统把t1调度为”sleeping”状态,把t2转换为”running”状态,t2也获得num=0。然后t2对得到的值进行加1并赋给num,使得num=1。然后系统又把t2调度为”sleeping”,把t1转为”running”。线程t1又把它之前得到的0加1后赋值给num。这样,明明t1和t2都完成了1次加1工作,但结果仍然是num=1。

# -*- coding: UTF-8 -*-
# 文件名:Sync_a.py

from threading import Thread
import time

g_num = 0


def test1():
    global g_num
    for i in range(1000000):
        g_num += 1

    print("---test1---g_num=%d" % g_num)


def test2():
    global g_num
    for i in range(1000000):
        g_num += 1

    print("---test2---g_num=%d" % g_num)


p1 = Thread(target=test1)
p1.start()

# time.sleep(3) #取消屏蔽之后 再次运行程序,结果会不一样,,,为啥呢?

p2 = Thread(target=test2)
p2.start()

print("---g_num=%d---" % g_num)

运行结果(可能不一样,但是结果往往不是2000000):

---g_num=164930---
---test1---g_num=1363325
---test2---g_num=1371213

Process finished with exit code 0

取消屏蔽之后,再次运行结果如下:

---test1---g_num=1000000
---g_num=1017466---
---test2---g_num=2000000

Process finished with exit code 0

问题产生的原因就是没有控制多个线程对同一资源的访问,对数据造成破坏,使得线程运行的结果不可预期。这种现象称为“线程不安全”。

同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。

"同"字从字面上容易理解为一起动作

其实不是,"同"字应是指协同、协助、互相配合。

如进程、线程同步,可理解为进程或线程A和B一块配合,A执行到一定程度时要依靠B的某个结果,于是停下来,示意B运行;B依言执行,再将结果给A;A再继续操作。这既是同步,

当线程间共享全局变量,多个线程对该变量执行不同的操作时,该变量最终的结果可能是不确定的(每次线程执行后的结果不同),如:对变量执行加减操作,变量的值是不确定的,要想变量的值是一个确定的需对线程执行的代码段加锁。

python对线程加锁主要有Lock和Rlock模块

互斥锁

​ 在python 中,无论你有多少核,(在Cpython 中)永远都是假象。无论你是4 核,8核,还是16 核…,同一时间执行的线程只有一个线程,它就是这个样子的。这个是python 的一个开发时候,设计的一个缺陷,所以说python 中的线程是假线程。
​ Python GIL(Global Interpreter Lock)Python 代码的执行由Python 虚拟机(也叫解释器主循环,CPython 版本)来控制,Python 在设计之初就考虑到要在解释器的主循环中,同时只有一个线程在执行,即在任意时刻,只有一个线程在解释器中运行。对Python 虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行为什么要用这个GIL 锁呢?
​ 因为Python 的线程是调用C 语言的原生线程。因为Python 是用C 写的,启动的时候就是调用的C 语言的接口。因为启动的C 语言的远程线程,那它要调这个线程去执行任务就必须知道上下文,所以Python 要去调C 语言的接口的线程,必须要把这个上下文关系传给Python,那就变成了一个我在加减的时候要让程序串行才能一次计算。就是先让线程1,再让线程2…

在这里插入图片描述

注意:
GIL 并不是Python 的特性,它是在实现Python 解析器(CPython)时所引入的一个概念,同样一段代码可以通过CPython,PyPy,Psyco 等不同的Python 执行环境来执行,就没有GIL 的问题。然而因为CPython 是大部分环境下默认的Python 执行环境。所以在很多人的概念里CPython 就是Python,也就想当然的把GIL 归结为Python 语言的缺陷

例如下面的代码块

# -*- coding: UTF-8 -*-
# 文件名:GIL_a.py

from threading import Thread


def func1(name):
    print('Threading:{} start'.format(name))
    global num
    for i in range(50000000):  # 有问题
        # for i in range(5000): # 无问题
        num += 1
    print('Threading:{} end num={}'.format(name, num))


if __name__ == '__main__':
    num = 0
    # 创建线程列表
    t_list = []
    # 循环创建线程
    for i in range(5):
        t = Thread(target=func1, args=('t{}'.format(i),))
        t.start()
        t_list.append(t)
    # 等待线程结束
    for t in t_list:
        t.join()

​ Python 使用线程的时候,会定时释放GIL 锁,这时会sleep,所以才会出现上面的问题。面对这个问题,如果要解决此问题,类似我们之前使用Lock 解决问题

看下面的代码:

# -*- coding: UTF-8 -*-
# 文件名:lock_b.py

from threading import Thread, Lock


def func1(name):
    print('Threading:{} start'.format(name))
    global num

    lock.acquire()
    for i in range(1000000):  # 有问题
        # for i in range(5000): # 无问题
        num += 1
    lock.release()
    print('Threading:{} end num={}'.format(name, num))


if __name__ == '__main__':
    # 创建锁
    lock = Lock()
    num = 0
    # 创建线程列表
    t_list = []
    # 循环创建线程
    for i in range(5):
        t = Thread(target=func1, args=('t{}'.format(i),))
        t.start()
        t_list.append(t)
# 等待线程结束
# for t in t_list:
# t.join()

注意:

  1. 加锁还可以使用with 效果一样
  2. 必须使用同一把锁
  3. 如果使用锁,程序会变成串行,因此应该是在适当的地再加锁
    线程调度本质上是不确定的,因此,在多线程程序中错误地使用锁机制可能会导致随机数
    据损坏或者其他的异常行为,我们称之为竞争条件。为了避免竞争条件,最好只在临界区(对
    临界资源进行操作的那部分代码)使用锁

对于全局变量,在多线程中要格外小心,否则容易造成数据错乱的情况发生,那对非全局变量是否要加锁呢?

我们来看看下面两段代码:

代码1:

# -*- coding: UTF-8 -*-
# 文件名:No_global_a.py

import threading
import time


class MyThread(threading.Thread):
    # 重写 构造方法
    def __init__(self, num, sleepTime):
        threading.Thread.__init__(self)
        self.num = num
        self.sleepTime = sleepTime

    def run(self):
        self.num += 1
        time.sleep(self.sleepTime)
        print('线程(%s),num=%d' % (self.name, self.num))


if __name__ == '__main__':
    mutex = threading.Lock()
    t1 = MyThread(100, 5)
    t1.start()
    t2 = MyThread(200, 1)
    t2.start()

运行结果:

线程(Thread-2),num=201
线程(Thread-1),num=101

Process finished with exit code 0

代码2:

# -*- coding: UTF-8 -*-
# 文件名:No_global_b.py

import threading
from time import sleep


def test(sleepTime):
    num = 1
    sleep(sleepTime)
    num += 1
    print('---(%s)--num=%d' % (threading.current_thread(), num))


t1 = threading.Thread(target=test, args=(5,))
t2 = threading.Thread(target=test, args=(1,))

t1.start()
t2.start()

运行结果:

---(<Thread(Thread-2, started 9872)>)--num=2
---(<Thread(Thread-1, started 2340)>)--num=2

Process finished with exit code 0

我们看到运行结果,与程序上的对比,实际上对于局部变量,是独立非共享的程序,每一个线程拿到的值,是相对独立的,在各自的线程内。所以运行的结果,在第二段代码上非常明显,不同的暂停时间上,运算num += 1结果是一致的。

在多线程开发中,全局变量是多个线程都共享的数据,而局部变量等是各自线程的,是非共享的

死锁

在多线程程序中,死锁问题很大一部分是由于线程同时获取多个锁造成的。

在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。

尽管死锁很少发生,但一旦发生就会造成应用的停止响应。看代码

代码:

# -*- coding: UTF-8 -*-
# 文件名:No_global_b.py

import threading
import time

class MyThread1(threading.Thread):
    def run(self):
        if mutexA.acquire():
            print(self.name+'----do1---up----')
            time.sleep(1)

            if mutexB.acquire():
                print(self.name+'----do1---down----')
                mutexB.release()
            mutexA.release()

class MyThread2(threading.Thread):
    def run(self):
        if mutexB.acquire():
            print(self.name+'----do2---up----')
            time.sleep(1)
            if mutexA.acquire():
                print(self.name+'----do2---down----')
                mutexA.release()
            mutexB.release()

mutexA = threading.Lock()
mutexB = threading.Lock()

if __name__ == '__main__':
    t1 = MyThread1()
    t2 = MyThread2()
    t1.start()
    t2.start()

看运行结果:

Thread-1----do1---up----
Thread-2----do2---up----

Process finished with exit code -1

此时已经进入死锁状态,命令行可以可以使用ctrl-z退出,

当我们执行线程t1与t2时候,我们的程序,第一步就是对双方所需占用的资源上锁,我们可以获取到上锁后的一部分信息,至此程序运转继续,等待对方的资源解锁,但是解锁操作无法完成,因为解锁条件就是对方完成解锁才能解锁。

产生死锁的四个必要条件:

(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

在这里插入图片描述

代码:

# -*- coding: UTF-8 -*-
# 文件名:mutu_a.py

from threading import Thread,Lock
from time import sleep

class Task1(Thread):
    def run(self):
        while True:
            if lock1.acquire():
                print("------Task 1 -----")
                sleep(0.5)
                lock2.release()

class Task2(Thread):
    def run(self):
        while True:
            if lock2.acquire():
                print("------Task 2 -----")
                sleep(0.5)
                lock3.release()

class Task3(Thread):
    def run(self):
        while True:
            if lock3.acquire():
                print("------Task 3 -----")
                sleep(0.5)
                lock1.release()

#使用Lock创建出的锁默认没有“锁上”
lock1 = Lock()
#创建另外一把锁,并且“锁上”
lock2 = Lock()
lock2.acquire()
#创建另外一把锁,并且“锁上”
lock3 = Lock()
lock3.acquire()

t1 = Task1()
t2 = Task2()
t3 = Task3()

t1.start()
t2.start()
t3.start()

可以使用互斥锁完成多个任务,有序的进程工作,这就是线程的同步,按照指定的顺序执行指定的任务。

生产者与消费者模式

线程间的通信

Semaphore

​ 我们都知道在加锁的情况下,程序就变成了串行,也就是单线程,而有时,我们在不用考虑数据安全时,为了避免业务开启过多的线程时。我们就可以通过信号量(Semaphore)来设置指定个数的线程。举个简单例子:车站有3 个安检口,那么同时只能有3 个人安检,别人来了,只能等着别人安检完才可以过。

# -*- coding: UTF-8 -*-
# 文件名:Semaphore.py

from threading import Thread, BoundedSemaphore
from time import sleep


def an_jian(num):
    semapshore.acquire()
    print('第{}个人安检完成!'.format(num))
    sleep(2)
    semapshore.release()


if __name__ == '__main__':
    semapshore = BoundedSemaphore(3)
    for i in range(20):
        thread = Thread(target=an_jian, args=(i,))
        thread.start()

Queue

​ 从一个线程向另一个线程发送数据最安全的方式可能就是使用queue 库中的队列了。创建一个被多个线程共享的Queue 对象,这些线程通过使用put() 和get() 操作来向队列中添加或者删除元素。Queue 对象已经包含了必要的锁,所以你可以通过它在多个线程间多安全地共享数据。

​ Python 包括FIFO(先入先出)队列Queue,LIFO(后入先出)队列LifoQueue,和优先
级队列PriorityQueue。Queue 中包含以下方法:

• Queue.qsize() 返回队列的大小
• Queue.empty() 如果队列为空,返回True,反之False
• Queue.full() 如果队列满了,返回True,反之False
• Queue.full 与maxsize 大小对应
• Queue.get([block[, timeout]])获取队列,timeout 等待时间
• Queue.get_nowait() 相当Queue.get(False)
• Queue.put(item) 写入队列,timeout 等待时间
• Queue.put_nowait(item) 相当Queue.put(item, False)
• Queue.taskdone() 在完成一项工作之后,Queue.taskdone()函数向任务已经完成的队列发送一个信号
• Queue.join() 实际上意味着等到队列为空,再执行别的操作

​ Python的Queue模块中提供了同步的、线程安全的队列类,包括FIFO(先入先出)队列Queue,LIFO(后入先出)队列LifoQueue,和优先级队列PriorityQueue。这些队列都实现了锁原语(可以理解为原子操作,即要么不做,要么就做完),能够在多线程中直接使用。可以使用队列来实现线程间的同步。

​ 用FIFO队列实现上述生产者与消费者问题的代码如下:

# -*- coding: UTF-8 -*-
# 文件名:Queue_b.py
import threading
import time

from queue import Queue


class Producer(threading.Thread):
    def run(self):
        global queue
        count = 0
        while True:
            if queue.qsize() < 1000:
                for i in range(100):
                    count = count + 1
                    msg = '生成产品' + str(count)
                    queue.put(msg)
                    print(msg)
            time.sleep(0.5)


class Consumer(threading.Thread):
    def run(self):
        global queue
        while True:
            if queue.qsize() > 100:
                for i in range(3):
                    msg = self.name + '消费了 ' + queue.get()
                    print(msg)
            time.sleep(1)


if __name__ == '__main__':
    queue = Queue()

    for i in range(500):
        queue.put('初始产品' + str(i))
    for i in range(2):
        p = Producer()
        p.start()
    for i in range(5):
        c = Consumer()
        c.start()

  1. 对于Queue,在多线程通信之间扮演重要的角色
  2. 添加数据到队列中,使用put()方法
  3. 从队列中取数据,使用get()方法
  4. 判断队列中是否还有数据,使用qsize()方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MaBftfUx-1596616396652)(/imgs/生产者与消费者.jpg)]

​ 在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。

# -*- coding: UTF-8 -*-
# 文件名:Queue_c.py

from queue import Queue
from threading import Thread
from time import sleep
from random import randint


def producer():
    num = 1
    while True:
        if queue.qsize() < 5:
            print(f'生产:{num}号加菲猫')
            queue.put(f'加菲猫{num}号')
            num += 1
        else:
            print('加菲猫满仓了,等待来人胜任领!')
        sleep(1)


def consumer():
    while True:
        print('获取:{}'.format(queue.get()))
        sleep(randint(1, 3))


if __name__ == '__main__':
    queue = Queue()
    t = Thread(target=producer)
    t.start()
    c = Thread(target=consumer)
    c.start()
    c2 = Thread(target=consumer)
    c2.start()

​ 生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。

​ 上面的代码如果不加以人工干预,本代码将会一直执行下去。从输出结果来看,除了最初还有部分剩余产品外,后面只要产品生产出来后就被消费了。这也是可以解释的,因为消费者消费产品的速度要快于生产者生产产品的速度。

在这里插入图片描述

事件Event

​ 线程的一个关键特性是每个线程都是独立运行且状态不可预测。如果程序中的其 他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就会变得非常棘手。为了解决这些问题,我们需要使用threading库中的Event对象。 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在 初始情况下,Event对象中的信号标志被设置为假。如果有线程等待一个Event对象, 而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行

Event()可以创建一个事件管理标志,该标志(event)默认为False,event 对象主要有四种方法可以调用:
event.wait(timeout=None):调用该方法的线程会被阻塞,如果设置了timeout 参数,超时后,线程会停止阻塞继续执行;
event.set():将event 的标志设置为True,调用wait 方法的所有线程将被唤醒;
event.clear():将event 的标志设置为False,调用wait 方法的所有线程将被阻塞;
event.is_set():判断event 的标志是否为True。
# -*- coding: UTF-8 -*-
# 文件名:Event_a.py

from threading import Thread, Event
from time import sleep
from random import randint

state = 0


def door():
    global state
    while True:
        if even.is_set():
            print('门开着,可以通行~')
            sleep(1)
        else:
            print('门关了~请刷卡!')
            state = 0
            even.wait()
        if state > 3:
            print('超过3 秒,门自动关门')
            even.clear()
        state += 1
        sleep(1)


def person():
    global state
    n = 0
    while True:
        n += 1
        if even.is_set():
            print('门开着:{}号进入'.format(n))
        else:
            even.set()
            state = 0
            print('门关着,{}号人刷卡进门'.format(n))
        sleep(randint(1, 10))


if __name__ == '__main__':
    even = Event()
    even.set()
    d = Thread(target=door)
    d.start()
    p = Thread(target=person)
    p.start()

异步

异步

  • 为完成某个任务,不同程序单元之间过程中无需通信协调,也能完成任务的方式。
  • 不相关的程序单元之间可以是异步的。
  • 例如,爬虫下载网页。调度程序调用下载程序后,即可调度其他任务,而无需与该下载任务保持通信以协调行为。不同网页的下载、保存等操作都是无关的,也无需相互通知协调。这些异步操作的完成时刻并不确定。异步意味着无序

如果在某程序的运行时,能根据已经执行的指令准确判断它接下来要进行哪个具体操作,那它是同步程序,反之则为异步程序。(无序与有序的区别)

​ 同步/异步、阻塞/非阻塞并非水火不容,要看讨论的程序所处的封装级别。例如购物程序在处理多个用户的浏览请求可以是异步的,而更新库存时必须是同步的

# -*- coding: UTF-8 -*-
# 文件名:asyn_a.py

from multiprocessing import Pool
import time
import os


def test():
    print("---进程池中的进程---pid=%d,ppid=%d--" % (os.getpid(), os.getppid()))
    for i in range(3):
        print("----%d---" % i)
        time.sleep(1)
    return "hahah"


def test2(args):
    print("---callback func--pid=%d" % os.getpid())
    print("---callback func--args=%s" % args)




if __name__ == '__main__':
    # 创建进程池及制定数量
    pool = Pool(3)
    # 非阻塞支持回调
    pool.apply_async(func=test, callback=test2)

    time.sleep(5)

    print("----主进程-pid=%d----" % os.getpid())

看运行结果

---进程池中的进程---pid=7744,ppid=1592--
----0---
----1---
----2---
---callback func--pid=1592
---callback func--args=hahah
----主进程-pid=1592----

Process finished with exit code 0

我们看到,异步调用实际想要同时结束进程,但是线程池中的其他进程有需要执行任务,等待执行任务完成结束去与其他进程回合,这里的参数通过回调,直至程序结束。

协程

协程(coroutine),又称为微线程,纤程。(协程是一种用户态的轻量级线程)

作用:在执行A 函数的时候,可以随时中断,去执行B 函数,然后中断继续执行A 函数(可以自动切换),单着一过程并不是函数调用(没有调用语句),过程很像多线程,然而协程只有一个线程在执行

在这里插入图片描述

​ 对于单线程下,我们不可避免程序中出现io 操作,但如果我们能在自己的程序中(即用户程序级别,而非操作系统级别)控制单线程下的多个任务能在一个任务遇到io 阻塞时就将寄存器上下文和栈保存到其他地方,切换到另外一个任务去计算。在任务切回来的时候,恢复先前保存的寄存器上下文和栈,这样就保证了该线程能够最大限度地处于就绪态,即随时都可以被cpu 执行的状态,相当于我们在用户程序级别将自己的io 操作最大限度地隐藏起来,从而可以迷惑操作系统,让其看到:该线程好像是一直在计算,io 比较少,从而更多的将cpu的执行权限分配给我们的线程(注意:线程是CPU 控制的,而协程是程序自身控制的)

协作的标准

必须在只有一个单线程里实现并发
 修改共享数据不需加锁
 用户程序里自己保存多个控制流的上下文栈
 一个协程遇到IO 操作自动切换到其它协程

在这里插入图片描述

​ 由于自身带有上下文和栈,无需线程上下文切换的开销,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级;无需原子操作的锁定及同步的开销;方便切换控制流,简化编程模型单线程内就可以实现并发的效果,最大限度地利用cpu,且可扩展性高,成本低(注:一个CPU 支持上万的协程都不是问题。所以很适合用于高并发处理)

他的缺点:无法利用多核资源:协程的本质是个单线程,它不能同时将单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU 上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu 密集型应用。进行阻塞(Blocking)操作(如IO 时)会阻塞掉整个程序

greenlet

# -*- coding: UTF-8 -*-
# 文件名:greenlet_a.py

from greenlet import greenlet


def attack(name):
    print(f'{name} :我要买包!')  # 2
    gree_b.switch('吕布')  # 3
    print(f'{name} :我要去学编程!')  # 6
    gree_b.switch()  # 7


def player(name):
    print(f'{name} :买买买!! ')  # 4
    gree_a.switch()  # 5
    print(f'{name} :一定去马士兵教育!!!!')  # 8


gree_a = greenlet(attack)
gree_b = greenlet(player)
gree_a.switch('貂蝉')  # 可以在第一次switch 时传入参数,以后都不需要#1

Gevent 模块

​ Gevent 是一个第三方库,可以轻松通过gevent 实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet,它是以C 扩展模块形式接入Python 的轻量级协程。Greenlet 全部运行在主程序操作系统进程的内部,但他们被协作式地调度。

安装模块

pip install gevent

代码:

# -*- coding: UTF-8 -*-
# 文件名:gevent_a.py

import gevent


def gf(name):
    print(f'{name} :我要买包!')  # 2
    gevent.sleep(2)  # 3
    print(f'{name} :我要去学编程!')  # 6


def bf(name):
    print(f'{name} :买买买!! ')  # 4
    gevent.sleep(2)
    print(f'{name} :一定去马士兵教育!!!!')  # 8


geven_a = gevent.spawn(gf, '小乔')
geven_b = gevent.spawn(bf, name='周瑜')
gevent.joinall([geven_a, geven_b])

注意:上例gevent.sleep(2)模拟的是gevent 可以识别的io 阻塞;

而time.sleep(2)或其他的阻塞,gevent 是不能直接识别的加入一行代码monkey.patch_all(),这行代码需在time,socket 模块之前。

async io 异步IO
asyncio 是python3.4 之后的协程模块,是python 实现并发重要的包,这个包使用事件循环驱动实现并发。事件循环是一种处理多并发量的有效方式,在维基百科中它被描述为「一种等待程序分配事件或消息的编程架构」,我们可以定义事件循环来简化使用轮询方法来监控事件,通俗的说法就是「当A 发生时,执行B」。事件循环利用poller 对象,使得程序员不用控制任务的添加、删除和事件的控制。事件循环使用回调方法来知道事件的发生。

在这里插入图片描述

看代码

# -*- coding: UTF-8 -*-
# 文件名:asyn_b.py

import asyncio


@asyncio.coroutine  # python3.5 之前
def func_a():
    for i in range(5):
        print('协程——a!!')
        yield from asyncio.sleep(1)


async def func_b():  # python3.5 之后
    for i in range(5):
        print('协程——b!!!')
        await asyncio.sleep(2)


# 创建协程对象
asy_a = func_a()
asy_b= func_b()
# 获取事件循环
loop = asyncio.get_event_loop()
# 监听事件循环
loop.run_until_complete(asyncio.gather(asy_a, asy_b))
# 关闭事件
loop.close()

1 @asyncio.coroutine 协程装饰器装饰
2 asyncio.sleep() 可以避免事件循环阻塞
3 get_event_loop() 获取事件循环
4 Loop.run_until_complete() 监听事件循环
5 gather() 封装任务
6 await 等于yield from 就是在等待task 结果
# -*- coding: UTF-8 -*-
# 文件名:asyn_c.py

import asyncio


async def compute(x, y):
    print(f"compute: {x}+{y} ...")
    await asyncio.sleep(1)
    return x + y


async def print_sum(x, y):
    result = await compute(x, y)
    print(f"{x}+{y}={result}")


loop = asyncio.get_event_loop()
loop.run_until_complete(print_sum(1, 2))
loop.close()

总结

串行、并行与并发的区别
在这里插入图片描述

  • 并行:指的是任务数小于等于 cpu核数,即任务真的是一起执行的
  • 并发:指的是任务数多余 cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)

进程与线程的区别

在这里插入图片描述

1.线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位; 
2.一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
3.进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号),某进程内的线程在其它进程不可见;
4.调度和切换:线程上下文切换比进程上下文切换要快得多。
有一个老板想要开个工厂进行生产某件商品(例如:手机)他需要花一些财力物力制作一条生产线,这个生产线上有很多的器件以及材料这些所有的为了能够生产手机而准备的资源称之为:进程只有生产线是不能够进行生产的,所以老板的找个工人来进行生产,这个工人能够利用这些材料最终一步步的将手机做出来,这个来做事情的工人称之为:线程这个老板为了提高生产率,想到 3种办法:

1.在这条生产线上多招些工人,一起来做手机,这样效率是成倍増长,即单进程多线程方式

2.老板发现这条生产线上的工人不是越多越好,因为一条生产线的资源以及材料毕竟有限,所以老板又花了些财力物力购置了另外一条生产线,然后再招些工人这样效率又再一步提高了,即多进程多线程方式

3.老板发现,现在已经有了很多条生产线,并且每条生产线上已经有很多工人了(即程序是多进程的,每个进程中又有多个线程),为了再次提高效率,老板想了个损招,规定:如果某个员工在上班时临时没事或者再等待某些条件(比如等待另一个工人生产完谋道工序之后他才能再次工作),那么这个员工就利用这个时间去做其它的事情,那么也就是说:如果一个线程等待某些条件,可以充分利用这个时间去做其它事情,其实这就是:协程方式

进程:拥有自己独立的堆和栈,既不共享堆,也不共享栈,进程由操作系统调度;进程切换需要的资源很最大,效率很低
线程:拥有自己独立的栈和共享的堆,共享堆,不共享栈,标准线程由操作系统调度;线程切换需要的资源一般,效率一般(当然了在不考虑 GIL的情况下)
协程:拥有自己独立的栈和共享的堆,共享堆,不共享栈,协程由程序员在协程的代码里显示调度;协程切换任务资源很小,效率高
多进程、多线程根据 cpu核数不一样可能是并行的,但是协程是在一个线程中所以是并发
选择技术考虑的因素:切换的效率、数据共享的问题、数据安全、是否需要并发

猜你喜欢

转载自blog.csdn.net/qq_42475194/article/details/107780889