文章目录

  • 1. 认识多任务编程
  • 2. 多进程编程
  • 2.1 认识进程
  • 2.2 创建子进程
  • 2.2.1 方法一:fork 创建子进程
  • 2.2.2 方法二:Process 创建子进程
  • 2.2.3 方法三:Pool 创建子进程
  • 2.3 进程间通信
  • 3. 多线程编程
  • 3.1 认识线程
  • 3.2 创建子线程
  • 3.3 线程间资源管理
  • 3.3.1 全局解释锁 GIL
  • 3.3.2 线程同步 --> 线程锁
  • 3.3.3 死锁
  • 4. 协程
  • 4.1 认识协程
  • 4.2 协程实现方法
  • 方法一:yield 实现方法——生产者消费者模式
  • 方法二:gevent 实现方法


1. 认识多任务编程

什么是多任务?

多任务是指操作系统可以同时运行多个任务。例如,你一边在浏览器上网,一边在听MP3,一边在用 Word 赶作业,这就是多任务,至少同时有3个任务正在运行。还有很多任务悄悄地在后台同时运行着,只是桌面上没有显示而已。

单核 CPU 如何实现多任务?

操作系统轮流让各个任务交替执行,每个任务执行0.01秒,这样反复执行下去。 表面上看,每个任务交替执行,但CPU的执行速度实在是太快了,感觉就像所有任务都在同时执行一样。

Python任务多开 python多任务编程_python


多核 CPU 如何实现多任务?

真正的并发执行多任务只能在多核CPU上实现,但是,由于任务数量远远多 于CPU的核心数量,所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。

Python任务多开 python多任务编程_子进程_02

2. 多进程编程

2.1 认识进程

程序:编写完毕的代码,在没有运行的时候为程序
进程:正在运行着的代码,此外,进程除了包含代码以外,还需要运行的环境等,所以和程序是有区别的

进程的五种状态:

Python任务多开 python多任务编程_多线程_03

2.2 创建子进程
2.2.1 方法一:fork 创建子进程

Python任务多开 python多任务编程_多线程_04

代码示例:

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

import os
import time

# 定义一个全局变量 money
money = 100
print("当前进程的pid:", os.getpid())
print("当前进程的父进程pid:", os.getppid())

p = os.fork()
# 子进程返回的是0
if p == 0:
    money = 200
    print("子进程返回的信息, money=%d" %(money))
# 父进程返回的是子进程的pid
else:
    print("创建子进程%s, 父进程是%d, money=%d" %(p,  os.getppid(), money))

【小结】

  • 执行到os.fork()时,操作系统会创建一个新的进程复制父进程的所有信息(数据,包括全局变量)到子进程中,数据互不影响
  • 普通的函数调用,调用一次,返回一次,但是 fork() 调用一次,返回两次
  • 父进程和子进程都会从 fork() 函数中得到一个返回值,父进程返回值是0,子进程中返回父进程的 id 号(>0的)
2.2.2 方法二:Process 创建子进程

因为 Windows 没有 fork 调用,而Python是跨平台的,所以利用multiprocessing 模块实现跨平台版本的多进程。multiprocessing 模块提供一个 Process 类来代表一个进程对象。

Process 类的使用:

from multiprocessing import Process

Process([group [, target [, name [, args [, kwargs]]]]]) 
	- target:表示这个进程实例所调用的对象; 
	- args:表示调用对象的参数,为元组数据类型; 
	- kwargs:表示调用对象的关键字参数,为字典; 
	- name:为当前进程实例的别名; 
	- group:大多数情况下用不到;

Process 类常用方法:

my_process = Process(target=my_func)
	- my_process.is_alive():判断进程实例是否还在执行;
	- my_process.join([timeout]):是否等待进程实例执行结束,或等待多少秒; 
	- my_process.start():启动进程实例(创建子进程); 
	- my_process.run():如果没有给定target参数,当这个对象调用 start() 方法时,就将执行对象中的 run() 方法; 
	- my_process.terminate():不管任务是否完成,立即终止;
  1. Process 创建子进程——实例化对象,代码示例
"""
Process([group [, target [, name [, args [, kwargs]]]]])
	target:表示这个进程实例所调⽤对象;
	args:表示调⽤对象的位置参数元组;
	kwargs:表示调⽤对象的关键字参数字典;
	name:为当前进程实例的别名;
	group:⼤多数情况下⽤不到;


Process类常用方法:
	is_alive():	判断进程实例是否还在执⾏;
	join([timeout]):	是否等待进程实例执⾏结束,或等待多少秒;
	start():		启动进程实例(创建⼦进程);
	run():		如果没有给定target参数,对这个对象调⽤start()⽅法时,
	           		就将执 ⾏对象中的run()⽅法;
	terminate():	不管任务是否完成,⽴即终⽌;

"""

from multiprocessing import Process
import time

def task1():
    print("正在听音乐......")
    time.sleep(1)

def task2():
    print("正在编程......")
    time.sleep(0.5)

def no_multi():
    task1()
    task2()

def use_multi():
    p1 = Process(target=task1)  # 实例化对象
    p2 = Process(target=task2)
    p1.start()
    p2.start()
    p1.join()
    p2.join()

    # join 的逻辑:
    '''
    p.join(): 阻塞当前进程,
    当 p1.start()之后, p1就提示主进程,需要等待p1进程执行结束才能向下执行, 
    那么主进程就乖乖等着,等待p1,p2执行完,
    也就是use_multi()执行完,再计算 end_time
    这才符合时间计算的逻辑
    '''
    # [process.join() for process in processes]

if __name__ == '__main__':
    # 主进程
    start_time= time.time()
    # no_multi()
    use_multi()
    end_time = time.time()
    print(end_time-start_time)

执行结果:

Python任务多开 python多任务编程_多进程_05

  1. Process 创建子进程——继承创建子类,代码示例
"""
创建子类, 继承的方式
"""
from multiprocessing import  Process
import time

class MyProcess(Process):
    """
    创建自己的进程, 父类是 Process
    """
    def __init__(self, music_name):
        super(MyProcess, self).__init__()  # 继承父类中的 init 函数
        self.music_name = music_name

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

# 开启进程: p.start()  <======> p.run()
if __name__ == '__main__':
    for i in range(10):
        p = MyProcess("音乐%d" %(i))
        p.start()  # 执行 run() 函数中的内容

执行结果:

Python任务多开 python多任务编程_多进程_06

2.2.3 方法三:Pool 创建子进程

为什么需要进程池 Pool?

  • 当被操作对象数目不大时,可以直接利用 multiprocessing 中的 Process 动态成生多个进程,十几个还好,但如果是上百个,上千个目标,手动的去限制进程数量却太过繁琐,此时可以发挥进程池的功效。
  • Pool 可以提供指定数量的进程供用户调用,当有新的请求提交到pool=中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到规定最大值,那么该请求就会等待,直到池中有进程结束,才会创建新的进程来它。更好的管理进程数与 CPU 核心数之间的关系,更好的实现“真正的多任务”

代码示例:

from multiprocessing import Process


def is_prime(num):
    """判断素数"""
    if num == 1:
        return False
    for i in range(2, num):
        if num % i == 0:
            return False
    else:
        return True


def task(num):
    if is_prime(num):
        print("%d是素数" % (num))


# 判断1000-1200之间所有的素数
def use_mutli():
    ps = []
    # 不要开启太多进程, 创建子进程会耗费时间和空间(内存);
    for num in range(1, 10000):
        # 实例化子进程对象
        p = Process(target=task, args=(num,))
        # 开启子进程
        p.start()
        # 存储所有的子进程对象
        ps.append(p)

    # 阻塞子进程, 等待所有的子进程执行结束, 再执行主进程以计算运行时间;
    [p.join() for p in ps]


# 判断1000-1200之间所有的素数
def no_mutli():
    for num in range(1, 100000):
        task(num)


def use_pool():
    """使用进程池"""
    from multiprocessing import Pool
    from multiprocessing import cpu_count  # 4个
    p = Pool(cpu_count())
    p.map(task, list(range(1, 100000)))
    p.close()  # 关闭进程池
    p.join()  # 阻塞,等待所有的子进程执行结束,再执行主进程;

if __name__ == '__main__':
    import time

    start_time = time.time()
    # 数据量大小         # 1000-1200             # 1-10000                 # 1-100000
    # no_mutli()        # 0.0077722072601       # 1.7887046337127686      # 90.75180315971375
    # use_mutli()       # 1.806459665298462
    use_pool()          # 0.15455389022827148   # 1.2682361602783203      # 35.63375639915466
    end_time = time.time()
    print(end_time - start_time)
2.3 进程间通信
多进程中,每个进程中所有数据(包括全局变量)都各有拥有一份,互不影响。

进程通信的目的:

  • 数据传输
  • 共享数据
  • 通知事件
  • 资源共享
  • 进程控制

进程通信的方式:【重要】

  • 管道
  • 信号
  • 消息队列
  • 信号量
  • 套接字

3. 多线程编程

3.1 认识线程

线程(thread)是CPU能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。

每个进程至少有一个线程,即进程本身。进程可以启动多个线程。操作系统像并行“进程”一样执行这些线程。

Python任务多开 python多任务编程_python_07


线程与进程的区别:【重要】

  • 进程是资源分配的最小单位,线程是CPU调度的单位。
  • 一个程序至少有一个进程,一个进程至少有一个线程
  • 进程有自己的独立地址空间,线程是共享进程中的数据的,使用相同的地址空间
  • 进程之间的通信需要以通信的方式(IPC)进行。线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,难点在于处理好同步与互斥。
3.2 创建子线程
  1. Thread 创建子线程——实例化对象,代码示例
"""
通过实例化对象的方式实现多线程
"""
import time
import threading
def task():
    """当前要执行的任务"""
    print("听音乐........")
    time.sleep(1)

if __name__ == '__main__':
    start_time = time.time()

    threads = []
    for count in range(5):
        t = threading.Thread(target=task)
        # 让线程开始执行任务
        t.start()
        threads.append(t)

    # 等待所有的子线程执行结束, 再执行主线程;
    [thread.join() for thread in threads]

    end_time = time.time()
    print(end_time-start_time)

执行结果:

Python任务多开 python多任务编程_子进程_08


多线程原理分析:

  • 多线程程序的执行顺序是不确定的。
  • 当执行到sleep语句时,线程将被阻塞(Blocked),到sleep结束后,线程进入就绪(Runnable)状态,等待调度。而线程调度将自行选择以个线程执行。
  • 代码中只能保证每个线程都运行完整个run函数,但是线程的启动顺序、 run函数中每次循环的执行顺序都不能确定。
  1. Thread 创建子线程——继承创建子类,代码示例
"""
继承创建子类

***项目案例: 基于多线程的批量主机存活探测***
项目描述:如果要在本地网络中确定哪些地址处于活动状态或哪些计算机处于活动状态,
		则可以使用此脚本。我们将依次ping地址, 每次都要等几秒钟才能返回值。
		这可以在Python中编程,在IP地址的地址范围内有一个for循环和一个 os.popen(“ping -q -c2”+ ip)。 
项目瓶颈:没有线程的解决方案效率非常低,因为脚本必须等待每次ping。 

"""
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)
        #  返回值如果为0, 代表命令正确执行,没有报错; 如果不为0, 执行报错;
        result = os.system(cmd)

        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()
3.3 线程间资源管理
3.3.1 全局解释锁 GIL

多线程间共享全局变量
优点: 在一个进程内的所有线程共享全局变量,能够在不使用其他方式的前提下完成多线程之间的数据共享(这点要比多进程要好)
缺点: 线程是对全局变量随意遂改可能造成多线程之间对全局变量的混乱(即线程非安全)

共享全局变量:如何解决线程不安全问题?
GIL(global interpreter lock):python解释器中任意时刻都只有一个线程在执行

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

3.3.2 线程同步 --> 线程锁

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

同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。
"同"字从字面上容易理解为⼀起动作 其实不是,
"同"字应是指协同、协助、互相配合。

Python任务多开 python多任务编程_多线程_09


如何实现线程同步–> 线程锁,代码示例:

money = 0  # 全局变量

def add():
    for i in range(1000000):
        global money
        lock.acquire()  # 上锁
        money += 1  # 该语句包括:+1 和赋值两部分,所以需保证该语句被一个线程完整执行
        lock.release()  # 解锁
        
def reduce():
    for i in range(1000000):
        global money
        lock.acquire()
        money -= 1
        lock.release()
        
if __name__ == '__main__':
    from threading import  Thread, Lock
    # 创建线程锁
    lock = Lock()  
    t1 = Thread(target=add)
    t2 = Thread(target=reduce)
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print(money)
3.3.3 死锁

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

Python任务多开 python多任务编程_python_10


代码示例:

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

import time
import threading


class Account(object):
    def __init__(self, id, money, lock):
        self.id = id
        self.money = money
        self.lock = lock

    def reduce(self, money):
        self.money -= money

    def add(self, money):
        self.money += money


def transfer(_from, to, money):
    if _from.lock.acquire():
        _from.reduce(money)

        time.sleep(1)
        if to.lock.acquire():
            to.add(money)
            to.lock.release()
        _from.lock.release()


if __name__ == '__main__':
    a = Account('a', 1000, threading.Lock())  # 900
    b = Account('b', 1000, threading.Lock())  # 1100

    t1 = threading.Thread(target=transfer, args=(a, b, 200))
    t2 = threading.Thread(target=transfer, args=(b, a, 100))
    t1.start()
    t2.start()
    print(a.money)
    print(b.money)

执行结果:

Python任务多开 python多任务编程_多进程_11

4. 协程

4.1 认识协程

协程,又称微线程,纤程。英文名Coroutine。比线程更小的执行单元,自带 CPU上下文。协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。

进程、线程、协程间的关系:

Python任务多开 python多任务编程_子进程_12


协程的优势:

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

代码示例:

import time

def consumer():
	r = ''
	while True:
		n = yield r
		if not n:
			return 
		print("[Consumer consuming %s]" % n)
		time.sleep(1)
		r = '200 OK'

def producer(c):
	c.__next__()
	n = 0
	while n < 5:
		n += 1
		print("[Producer producing %s]" % n)
		r = c.send(n)
		print("[Producer Consumer return %s]" % r)
	c.close()

if __name__ == '__main__':
	c = consumer()
	producer(c)

执行结果:

Python任务多开 python多任务编程_子进程_13

方法二:gevent 实现方法

代码示例:

import gevent

import requests
import json

from gevent import monkey
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from threading import Thread

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:
        # 默认返回的是字符串
        """
        {"as":"AS174 Cogent Communications","city":"Beijing","country":"China","countryCode":"CN","isp":"China Unicom Shandong Province network","lat":39.9042,"lon":116.407,"org":"NanJing XinFeng Information Technologies, Inc.","query":"114.114.114.114","region":"BJ","regionName":"Beijing","status":"success","timezone":"Asia/Shanghai","zip":""}
        """
        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:westos@172.25.254.123/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)

    # gevent 实现协程
    gevents = [gevent.spawn(task, '1.1.1.' + str(ip + 1)) for ip in range(10)]
    gevent.joinall(gevents)
    print("执行结束....")