Python篇:多任务编程——进程,线程,协程详解

文章目录

  • 多任务编程
    • 概念
    • 单核CPU如何实现“多任务”
    • 多核CPU如何实现“多任务”
  • 多进程编程
    • 进程的创建
      • 进程 VS 程序
      • 进程的五状态模型
      • 创建⼦进程
      • 多进程修改全局变量
    • 多进程编程
      • 多进程编程方法1: 实例化对象
      • 多进程编程方法2: 创建子类
    • 进程池
      • 为什么需要进程池Pool?
      • 实现方式
    • 进程间通信
      • 目的
    • 方式
    • 消息队列
    • 代码实现
  • 多线程编程
    • 什么是线程?
    • 线程和进程各自的区别和优劣
    • 线程分类
    • 线程的⼏种状态
    • 多线程编程实现方法
      • 方法一:实例化对象
        • 项目案例: IP地址归属地批量查询任务
      • 方法二:创建子类
        • 项目案例: IP地址归属地批量查询任务
        • 项目案例: 基于多线程的批量主机存活探测
    • 共享全局变量
    • 共享全局变量:如何解决线程不安全问题(使用全局解释器锁(GIL))
    • 线程同步(线程锁)
    • 死锁
  • 协程
    • 概念
    • 协程优势
    • 实现方法
      • 方法一: yield实现
      • 方法二: gevent实现协程
        • 协程实现ip归属地查询并存入数据库
  • 总结

多任务编程

概念

操作系统可以同时运⾏多个任务。打个 ⽐⽅,你⼀边在⽤浏览器上⽹,⼀边在听MP3,⼀边在⽤Word赶作业,这就是多任务,⾄少同时有3个任务正在运⾏。还有很多任务悄悄地在后台同时运 ⾏着,只是桌⾯上没有显示⽽已。

单核CPU如何实现“多任务”

操作系统轮流让各个任务交替执⾏,每个任务执⾏0.01秒,这样反复执⾏下去。 表⾯上看,每个任务交替执⾏,但CPU的执⾏速度实在是太快了,感觉就像所有任务都在同时执⾏⼀样。
Python篇:多任务编程——进程,线程,协程详解_第1张图片

多核CPU如何实现“多任务”

真正的并⾏执⾏多任务只能在多核CPU上实现,但是,由于任务数量远远多 于CPU的核⼼数量,所以,操作系统也会⾃动把很多任务轮流调度到每个核 ⼼上执⾏。
Python篇:多任务编程——进程,线程,协程详解_第2张图片

多进程编程

进程的创建

进程 VS 程序

  • 编写完毕的代码,在没有运⾏的时候,称之为程序
  • 正在运⾏着的代码,就成为进程

注意: 进程,除了包含代码以外,还有需要运⾏的环境等,所以和程序是有区别的

进程的五状态模型

Python篇:多任务编程——进程,线程,协程详解_第3张图片

创建⼦进程

Python的os模块封装了常⻅的系统调⽤,其中就包括fork,可以在Python程 序中轻松创建⼦进程

Python篇:多任务编程——进程,线程,协程详解_第4张图片

# encoding=utf-8
"""
Date:2019-07-21 09:48
User:LiYu
Email:[email protected]

"""
import os

print('当前进程:', os.getpid())
print('当前进程的父进程:', os.getppid())
print('开始创建子进程...')

p = os.fork()

if p == 0:
    print('这是子进程,id是:%s,父进程id是:%s' % (os.getpid(), os.getppid()))
else:
    print('这是当前进程,id是:%s,子进程id是:%s' % (os.getpid(), p))

Python篇:多任务编程——进程,线程,协程详解_第5张图片
执⾏到os.fork()时,操作系统会创建⼀个新的进程复制⽗进程的所有信息到⼦进程中

普通的函数调⽤,调⽤⼀次,返回⼀次,但是fork()调⽤⼀次,返回两次

⽗进程和⼦进程都会从fork()函数中得到⼀个返回值,⼦进程返回是0,⽽⽗进程中返回⼦进程的 id号

多进程修改全局变量

多进程中,每个进程中所有数据(包括全局变量)都各有拥有⼀份,互不影响

多进程编程

Windows没有fork调⽤,由于Python是跨平台的, multiprocessing模块就是跨平台版本的多进程模块。multiprocessing模块提供了⼀个Process类来代表⼀个进程对象。

Process([group [, target [, name [, args [, kwargs]]]]]) 
	target:表示这个进程实例所调⽤对象; 
	args:表示调⽤对象的位置参数元组; 
	kwargs:表示调⽤对象的关键字参数字典; 
	name:为当前进程实例的别名; 
	group:⼤多数情况下⽤不到;
	
Process类常⽤⽅法:
	is_alive():判断进程实例是否还在执⾏;
	join([timeout]):是否等待进程实例执⾏结束,或等待多少秒; 
	start():启动进程实例(创建⼦进程); 
	run():	如果没有给定target参数,对这个对象调⽤start()⽅法时,就将执⾏对象中的run()⽅法; 
	terminate():不管任务是否完成,⽴即终⽌;

Process类常⽤属性: 
	name:当前进程实例别名,默认Process-N,N为从1开始计数;
	pid:当前进程实例的PID值;

多进程编程方法1: 实例化对象

Python篇:多任务编程——进程,线程,协程详解_第6张图片

# encoding=utf-8
"""
Date:2019-07-21 10:15
User:LiYu
Email:[email protected]

"""
from multiprocessing import Process
import time


def task1():
    print('任务1')
    time.sleep(1)


def task2():
    print('任务2')
    time.sleep(0.5)


def no_multi():
    for i in range(3):
        task1()
    for i in range(5):
        task2()


def use_multi():
    processess = []
    for i in range(3):
        p = Process(target=task1)
        p.start()
        processess.append(p)

    for i in range(5):
        p = Process(target=task2)
        p.start()
        processess.append(p)

    [processe.join() for processe in processess]


if __name__ == '__main__':
    startTime = time.time()
    # no_multi()
    use_multi()
    endTime = time.time()
    print(endTime - startTime)

多进程编程方法2: 创建子类

Python篇:多任务编程——进程,线程,协程详解_第7张图片

# encoding=utf-8
"""
Date:2019-07-21 11:34
User:LiYu
Email:[email protected]

"""
from multiprocessing import Process
import time


class MyProcess(Process):
    def __init__(self, music_name):
        super(MyProcess, self).__init__()
        self.music_name = music_name

    def run(self):
        """重写run方法,内容是要执行的任务"""
        print("听音乐%s" % self.music_name)
        time.sleep(1)


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

进程池

为什么需要进程池Pool?

  • 当被操作对象数目不大时,可以直接利用multiprocessing中的Process动态成生多个进程,十几个还好,但如果是上百个,上千个目标,手动的去限制进程数量却又太过繁琐,此时可以发挥进程池的功效。
  • Pool可以提供指定数量的进程供用户调用,当有新的请求提交到pool中时,如果池还没有满,那么就会分配一个进程用来执行该请求;但如果池中的进程数已经达到规定最大值,那么该请求就会等待,直到池中所有进程结束,才会分配进程来执行该请求。
    Python篇:多任务编程——进程,线程,协程详解_第8张图片

实现方式

方法一:
Python篇:多任务编程——进程,线程,协程详解_第9张图片
方法二:
Python篇:多任务编程——进程,线程,协程详解_第10张图片

进程间通信

目的

Python篇:多任务编程——进程,线程,协程详解_第11张图片

方式

Python篇:多任务编程——进程,线程,协程详解_第12张图片

消息队列

可以使⽤multiprocessing模块的Queue实现多进程之间的数据传递,Queue 本身是⼀个消息列队程序。

Queue.qsize():	返回当前队列包含的消息数量; 
Queue.empty():	如果队列为空,返回True,反之False ; 
Queue.full():	如果队列满了,返回True,反之False; 
Queue.get([block[, timeout]]):获取队列中的⼀条消息,然后将其从列队中移除,block默认值为True;

Queue.get_nowait():相当Queue.get(False);
Queue.put(item,[block[, timeout]]):将item消息写⼊队列,block默认值 为True;
Queue.put_nowait(item):相当Queue.put(item, False)

代码实现

# encoding=utf-8
"""
Date:2019-07-21 13:38
User:LiYu
Email:[email protected]

"""
import time
import os
from multiprocessing import Process, Queue


# 继承的方法同理,任务放到run里,数据加到实例属性里面
def task1(queue):
    for i in range(10):
        queue.put(i)
        time.sleep(0.1)
        print('传递消息:', i)
        print(os.getpid())


def task2(queue):
    while True:
        time.sleep(0.1)
        result = queue.get()
        print('收到消息:', result)
        print(os.getpid())


if __name__ == '__main__':
    print(os.getpid())
    q = Queue()
    p1 = Process(target=task1, args=(q,))
    p2 = Process(target=task2, args=(q,))
    p1.start()
    p2.start()
    p1.join()
    p2.join()

Python篇:多任务编程——进程,线程,协程详解_第13张图片

多线程编程

什么是线程?

线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
Python篇:多任务编程——进程,线程,协程详解_第14张图片
每个进程至少有一个线程,即进程本身。进程可以启动多个线程。操作系统像并行“进程”一样执行这些线程。
Python篇:多任务编程——进程,线程,协程详解_第15张图片

线程和进程各自的区别和优劣

  • 进程是资源分配的最小单位,线程是程序执行的最小单位。
  • 进程有自己的独立地址空间。线程是共享进程中的数据的,使用相同的地址空间.
  • 进程之间的通信需要以通信的方式(IPC)进行。线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,难点: 处理好同步与互斥。

线程分类

有两种不同的线程:

  • 内核线程
  • 用户空间线程或用户线程

内核线程是操作系统的一部分,而内核中没有实现用户空间线程。

线程的⼏种状态

Python篇:多任务编程——进程,线程,协程详解_第16张图片

多线程编程实现方法

python的thread模块是⽐较底层的模块,python的threading 模块是对thread做了⼀些包装的,可以更加⽅便的被使⽤
Python篇:多任务编程——进程,线程,协程详解_第17张图片

方法一:实例化对象

Python篇:多任务编程——进程,线程,协程详解_第18张图片
分析:

  • 多线程程序的执⾏顺序是不确定的。
  • 当执⾏到sleep语句时,线程将被阻塞(Blocked),到sleep结束后,线程进⼊就绪(Runnable)状态,等待调度。⽽线程调度将⾃⾏选择⼀个线程执⾏。
  • 代码中只能保证每个线程都运⾏完整个run函数,但是线程的启动顺序、 run函数中每次循环的执⾏顺序都不能确定。

项目案例: IP地址归属地批量查询任务

# encoding=utf-8
"""
Date:2019-07-21 14:18
User:LiYu
Email:[email protected]

"""
import random
import threading
import time

from sqlalchemy import create_engine, Column, String, Integer
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
import requests

content = []
engine = create_engine("mysql+pymysql://root:[email protected]/pymysql",
                       encoding='utf8',
                       # echo=True
                       )
Session = sessionmaker(bind=engine)
session = Session()
Base = declarative_base()


class AddrForIp(Base):
    __tablename__ = 'ipInfo'
    id = Column(Integer, primary_key=True, autoincrement=True)
    ip = Column(String(30), unique=True)
    city = Column(String(20), nullable=False)
    country = Column(String(30), nullable=False)

    def __repr__(self):
        return '%s, %s, %s, %s\n' % (self.id, self.ip, self.city, self.country)


def get_addr(ip):
    url = 'http://ip-api.com/json/%s' % ip
    page_content = requests.get(url).text

    import json
    dict_data = json.loads(page_content)
    content.append([ip, dict_data['city'], dict_data['country']])
    time.sleep(random.random())


if __name__ == '__main__':
    pList = []
    tList = []
    for i in range(1, 255):
        ip = '1.1.1.%s' % str(i)
        t = threading.Thread(target=get_addr, args=(ip,))
        t.start()
        tList.append(t)
    [t.join() for t in tList]
    # print(content)

    Base.metadata.create_all(engine)
    for i in content:
        p = AddrForIp(ip=i[0], city=i[1], country=i[2])
        pList.append(p)
    session.add_all(pList)
    session.commit()

方法二:创建子类

Python篇:多任务编程——进程,线程,协程详解_第19张图片

项目案例: IP地址归属地批量查询任务

Python篇:多任务编程——进程,线程,协程详解_第20张图片

项目案例: 基于多线程的批量主机存活探测

项目描述: 如果要在本地网络中确定哪些地址处于活动状态或哪些计算机处于活动状态,则可以使用此脚本。我们将依次ping地址, 每次都要等几秒钟才能返回值。这可以在Python中编程,在IP地址的地址范围内有一个for循环和一个os.popen(“ping -q -c2”+ ip)。

项目瓶颈: 没有线程的解决方案效率非常低,因为脚本必须等待每次ping。
Python篇:多任务编程——进程,线程,协程详解_第21张图片

"""
创建子类
"""


from threading import  Thread
class GetHostAliveThread(Thread):
    """
    创建子线程, 执行的任务:判断指定的IP是否存活
    """
    def __init__(self, ip):
        super(GetHostAliveThread, self).__init__()
        self.ip = ip
    def run(self):
        # # 重写run方法: 判断指定的IP是否存活
        # """
        # >>> # os.system()  返回值如果为0, 代表命令正确执行,没有报错; 如果不为0, 执行报错;
        # ...
        # >>> os.system('ping -c1 -w1 172.25.254.49 &> /dev/null')
        # 0
        # >>> os.system('ping -c1 -w1 172.25.254.1 &> /dev/null')
        # 256
        # """
        import os
        # 需要执行的shell命令
        cmd = 'ping -c1 -w1 %s &> /dev/null' %(self.ip)
        result = os.system(cmd)
        #  返回值如果为0, 代表命令正确执行,没有报错; 如果不为0, 执行报错;
        if result != 0:
            print("%s主机没有ping通" %(self.ip))
if __name__ == '__main__':
    print("打印172.25.254.0网段没有使用的IP地址".center(50, '*'))
    for i in range(1, 255):
        ip = '172.25.254.' + str(i)
        thread = GetHostAliveThread(ip)
        thread.start()

Python篇:多任务编程——进程,线程,协程详解_第22张图片

共享全局变量

优点: 在⼀个进程内的所有线程共享全局变量,能够在不使⽤其他⽅式的前提下完成多线程之间的数据共享(这点要⽐多进程要好)

缺点: 线程是对全局变量随意遂改可能造成多线程之间对全局变量 的混乱(即线程⾮安全)

共享全局变量:如何解决线程不安全问题(使用全局解释器锁(GIL))

GIL(global interpreter lock): python解释器中任意时刻都只有一个线程在执行;

Python代码的执行由Python 虚拟机(也叫解释器主循环,CPython版本)来控制,Python 在设计之初就考虑到要在解释器的主循环中,同时只有一个线程在执行,即在任意时刻,只有一个线程在解释器中运行。对Python 虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。
Python篇:多任务编程——进程,线程,协程详解_第23张图片
Python篇:多任务编程——进程,线程,协程详解_第24张图片

线程同步(线程锁)

**线程同步:**即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作.

同步就是协同步调,按预定的先后次序进⾏运⾏。如:你说完,我再说。
"同"字从字⾯上容易理解为⼀起动作,其实不是,
"同"字应是指协同、协助、互相配合。
Python篇:多任务编程——进程,线程,协程详解_第25张图片
Python篇:多任务编程——进程,线程,协程详解_第26张图片

死锁

在线程间共享多个资源的时候,如果两个线程分别占有⼀部分资源并且同时 等待对⽅的资源,就会造成死锁。
Python篇:多任务编程——进程,线程,协程详解_第27张图片
Python篇:多任务编程——进程,线程,协程详解_第28张图片

协程

概念

协程,又称微线程,纤程。英文名Coroutine。协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。
Python篇:多任务编程——进程,线程,协程详解_第29张图片

协程优势

  • 执行效率极高,因为子程序切换(函数)不是线程切换,由程序自身控制,
  • 没有切换线程的开销。所以与多线程相比,线程的数量越多,协程性能的优势越明显。
  • 不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在控制共享资源时也不需要加锁,因此执行效率高很多。

实现方法

方法一: yield实现

Python篇:多任务编程——进程,线程,协程详解_第30张图片
Python篇:多任务编程——进程,线程,协程详解_第31张图片

方法二: gevent实现协程

基本思想:
当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。
Python篇:多任务编程——进程,线程,协程详解_第32张图片
@timeit功能同join()函数

同时还需要打补丁(monkey),值得注意的是补丁包需要单独导入

协程实现ip归属地查询并存入数据库

import gevent
import requests
import json
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from gevent import monkey
# 打补丁
monkey.patch_all()


def task(ip):
    """获取指定IP的所在城市和国家并存储到数据库中"""
    # 获取网址的返回内容
    url = 'http://ip-api.com/json/%s' % (ip)
    try:
        response = requests.get(url)
    except Exception as e:
        print("网页获取错误:", e)
    else:
        # 默认返回的是字符串
        contentPage = response.text
        # 将页面的json字符串转换成便于处理的字典;
        data_dict = json.loads(contentPage)
        # 获取对应的城市和国家
        city = data_dict.get('city', 'null')  # None
        country = data_dict.get('country', 'null')

        print(ip, city, country)
        # 存储到数据库表中ips
        ipObj = IP(ip=ip, city=city, country=country)
        session.add(ipObj)
        session.commit()


if __name__ == '__main__':
	# 连接数据库
    engine = create_engine("mysql+pymysql://root:[email protected]/pymysql",
                           encoding='utf8',
                           # echo=True
                           )
    # 创建缓存对象
    Session = sessionmaker(bind=engine)
    session = Session()
    # 声明基类
    Base = declarative_base()


    class IP(Base):
        __tablename__ = 'ips'
        id = Column(Integer, primary_key=True, autoincrement=True)
        ip = Column(String(20), nullable=False)
        city = Column(String(30))
        country = Column(String(30))

        def __repr__(self):
            return self.ip


    # 创建数据表
    Base.metadata.create_all(engine)
    # 使用协程
    gevents = [gevent.spawn(task, '1.1.1.' + str(ip + 1)) for ip in range(10)]
    gevent.joinall(gevents)
    print("执行结束....")

总结

Python篇:多任务编程——进程,线程,协程详解_第33张图片

  1. 线程与进程的区别?
    区别:
    1)是否占有资源问题
    2)创建或撤销一个进程所需要的开销比创建或撤销一个线程所需要的开销大。
    3)进程为重量级组件,线程为轻量级组件
    多进程: 在操作系统中能同时运行多个任务(程序)
    多线程: 在同一应用程序中有多个功能流同时执行

  2. 进程间内存是否共享?
    进程间有独立内存空间,不共享

  3. 进程间通信方式?
    管道、信号、消息队列、信息量、套接字

  4. 多线程有几种实现方法,都是什么?
    1)通过threding模块实例化
    2)子类继承实现

  5. GIL锁是怎么回事?
    python的多线程环境下,每执行完100条指令后(称为“软时钟”)会触发一次“python级线程调度”,所谓的“python级线程调度”,指的是线程A释放GIL,线程B获得GIL,从而掌握了对解释器的“执行大权”。GIL实际上是再普通不过的线程锁,获得、释放GIL就是加解锁操作,像win32下是WaitForSingleObject和SetEvent,而solaris下则是mutex_lock和mutex_unlock。所以,“python级线程调度”本质上是GIL的锁争用,从某种意义上也可以说GIL锁被顺带用作“python级线程调度”。

  6. python中是否线程安全?如何解决线程安全?
    多线程环境中,共享数据同一时间只能有一个线程来操作,这种情况成为线程安全。
    当对全局资源存在写操作时,如果不能保证写入过程的原子性,会出现脏读脏写的情况,即线程不安全。Python的GIL只能保证原子操作的线程安全,因此在多线程编程时我们需要通过加锁来保证线程安全

  7. 什么叫死锁?
    在线程间共享多个资源的时候,如果两个线程分别占有⼀部分资源并且同时 等待对⽅的资源,就会造成死锁。

  8. 什么是协程?常用的协程模块有哪些?
    协程,又称微线程,纤程。英文名Coroutine。协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。
    常用的模块有:
    greenlet,此模块是通过yield封装实现的
    gevent实现了自动识别io操作,自动进行协程的切换调度。是一种更高级的封装。

  9. 协程中的join是用来做什么用的?它是如何发挥作用的?
    阻塞等待调用join方法的协程任务执行完毕,然后继续往后执行。
    join产生阻塞,gevent识别到阻塞后,自动切换任务,只要该协程任务没有完成,join会一直产生阻塞,从而使gevent不停的切换到该协程任务上执行。

你可能感兴趣的:(Python笔记)