多任务编程

多任务

就是同一时间 内执行 多个任务

分为

  • 并发 多个任务在一段时间内交替去执行,但是任务的切换速度快
  • 并行 多个任务在一段时间内同时执行(任务数少于等于 cpu 核数)
  • 串⾏:⼀个任务完完整整的执⾏完了再执⾏下⼀个
进程

进程:⼀个程序运⾏起来后,代码+⽤到的资源称之为进程,它是操作系统分配资源的基本单
元。

Process 进程类的说明

Process([group [, target [, name [, args [, kwargs]]]]])
group:指定进程组,目前只能使用 None
target:执行的目标任务名
name:进程名字

执行的任务带参数时候使用
args:以元组方式给执行任务传参(顺序一致)
kwargs:以字典方式给执行任务传参(键对应就行)

Process创建的实例对象的常用方法:
start():启动子进程实例(创建子进程)
join():等待子进程执行结束
terminate():不管任务是否完成,立即终止子进程

Process创建的实例对象的常用属性:
name:当前进程的别名,默认为Process-N,N为从1开始递增的整数

#!/usr/bin/python
# -*- coding:UTF-8 -*-

import multiprocessing
import time


# 带有参数的任务
def task(count):
    for i in range(count):
        print("任务执行中..")
        time.sleep(0.2)
    else:
        print("任务执行完成")


if __name__ == '__main__':
    # 创建子进程

    # args: 以元组的方式给任务传入参数,注意顺序
    # sub_process = multiprocessing.Process(target=task, args=(5,))

    # kwargs: 表示以字典方式传入参数,顺序无关,只要键对应
    sub_process = multiprocessing.Process(target=task, kwargs={"count": 3})
    sub_process.start()

进程的注意点介绍

  1. 进程之间不共享全局变量。(创建子进程会对主进程资源进行拷贝,也就是说子进程是主进程的一个副本,之所以进程之间不共享全局变量,是因为操作的不是同一个进程里面的全局变量,只不过不同进程里面的全局变量名字相同而已。)
  2. 主进程会等待所有的子进程执行结束再结束。
  3. 进程之间执行是无序的,它是由操作系统调度决定的,操作系统调度哪个进程,哪个进程就先执行,没有调度的进程不能执行。

保证主进程正常退出不等待子线程结束两种方法:

 1. sub_process.daemon = True  # 守护主进程就是主进程退出子进程销毁不再执行 
 2. sub.process.terminate()   # 子进程销毁
线程

线程: CPU调度的基本单位。

多线程是并发的, CPU 来调度执行,也是依靠GIL 锁。

线程类 Thread 参数说明

Thread([group [, target [, name [, args [, kwargs]]]]])

  • group:线程组,目前只能使用None target: 执行的目标任务名
  • args: 以元组的方式给执行任务传参
  • kwargs: 以字典方式给执行任务传参
  • name: 线程名,一般不用设置
threading.enumerate()  # 枚举所有活动的线程
threading.active_count()  # 活动线程数
threading.current_thread()  # 当前的线程

线程的注意点介绍

  1. 线程之间执行是无序的(线程之间执行是无序的,它是由cpu调度决定的 ,cpu调度哪个线程,哪个线程就先执行,没有调度的线程不能执行。)
  2. 主线程会等待所有的子线程执行结束再结束
  3. 线程之间共享全局变量,所以操作数据可能出现错误问题
    线程同时操作解决方法:线程同步: 保证同一时刻只能有一个线程去操作全局变量 同步: 就是协同步调,按预定的先后次序进行运行。

设置守护主线程有两种方式:

threading.Thread(target=func, daemon=True)  # 创建线程的时候就设置守护主线程
线程对象.setDaemon(True)  # 线程开始前设置好守护主线程
 

线程同步

if __name__ == '__main__':
    # 创建两个线程
    first_thread = threading.Thread(target=sum_num1)
    second_thread = threading.Thread(target=sum_num2)

    # 启动线程
    first_thread.start()
    # 主线程等待第一个线程执行完成以后代码再继续执行,让其执行第二个线程
    # 线程同步: 一个任务执行完成以后另外一个任务才能执行,同一个时刻只有一个任务在执行
    first_thread.join()
    # 启动线程
    second_thread.start()

互斥锁
对共享数据进行锁定,保证同一时刻只能有一个线程去操作。

# 每个线程里面都创建锁
mutex = threading.Lock()

# 上锁
mutex.acquire()

...这里编写代码能保证同一时刻只能有一个线程去操作, 对共享数据进行锁定...

# 释放锁
mutex.release()

死锁 (线程因为 多个线程互相调用对方的数据导致一直阻塞,造成程序的停滞响应)

一个程序运行后至少有一个进程,一个进程默认有一个线程,进程里面可以创建多个线程,线程是依附在进程里面的,没有进程就没有线程。

线程 进程对比:

进程优缺点:

  • 优点:可以用多核
  • 缺点:资源开销大

线程优缺点:

  • 优点:资源开销小
  • 缺点:不能使用多核

进程和线程都是完成多任务的一种方式
多进程要比多线程消耗的资源多,但是多进程开发比单进程多线程开发稳定性要强,某个进程挂掉不会影响其它进程。
多进程可以使用cpu的多核运行,所以与数据相关的操作使用进程,计算能力强。
多线程可以共享全局变量。
线程不能单独执行必须依附在进程里面

协程

协程,⼜称微线程,纤程,英⽂名Coroutine。是python个中另外⼀种实现多任务的⽅式,只不过⽐
线程更⼩占⽤更⼩执⾏单元。 为啥说它是⼀个执⾏单元,因为它⾃带CPU上下⽂。这样只要在合适的时
机, 我们可以把⼀个协程切换到另⼀个协程。 这个切换没有线程的切换开销。

协程的优点

  • 比线程完成要节省资源
  • 没有多线程的锁机制,因为只有一个线程,在协程中操作共享资源不加锁,所以执行效率比多线程高很多。

其原理是当一个任务函数遇到IO(指的是input output 输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的任务函数执行,等到IO操作完成,再在适当的时候切换回来继续执行。
由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有任务函数在运行,而不是等待IO,得以实现多任务,提高程序执行效率。

安装:
pip3 install gevent

多个协程是依次执行的
import gevent

例子

import gevent
import time
import threading

from gevent import monkey

# 打补丁操作,让协程识别耗时操作(time.sleep, 网络请求, socket里面的接收数据, socket等待接受客户端的连接请求)
monkey.patch_all()


# 定义函数任务
def task(count):
    for i in range(count):
        print("当前遍历的数据为:", i)
        # 获取当前协程
        print("当前线程为:", gevent.getcurrent())
        # 让协程执行任务时有耗时操作,协程会根据耗时操作切换到其它协程去执行
        # 默认情况下,time.sleep 对于协程来说不是耗时操作
        # time.sleep(0.2)
        # 协程里面的耗时操作,会根据耗时操作切换到其它协程去执行
        # gevent.sleep(0.2)

        # 由于已经打上补丁,协程能够识别是耗时操作,遇到耗时操作可以切换到其它协程执行代码
        time.sleep(0.2)

if __name__ == '__main__':
    # 创建两个协程
    g1 = gevent.spawn(task, 2)
    g2 = gevent.spawn(task, 3)

    # 默认情况下: 主线程不会等待协程执行结束再结束

    # 解决办法:让当前线程等待协程执行结束
    # g1.join()
    # g2.join()

    # 等待协程执行结束的第二种方式
    # gevent.joinall([g1, g2])

    # 验证使用gevent利用程完成多任务没有创建线程
    result = threading.active_count()
    print("当前进程的活动线程的数量为:", result)
    # 扩展: 主线程有耗时操作,可以让协程切换执行
    while True:
        time.sleep(1)

## output
g = gevent.spawn(test, 1, 2, 3) # 参数直接放在后面
g.join()
扩展

多进程多线程存在的意义:

多线程:主要是解决了IO阻塞时,线程会切换,虽然切换线程也会消耗资源,但相⽐较等待
IO,节约了很多的时间。单核计算机时代,同时只能有⼀个线程执⾏,多线程的设计在IO操作
时,很提升效率。

多进程:现在的计算机都是多核计算机,多进程的设计提⾼了CPU的使⽤率,多个任务同时执行,加快了程序的效率。

进程和线程如何工作:
⼀个进程中可以有多个线程,每个进程都有⼀把GIL锁(GIL实际存在于C-Python解释器),
线程要想执⾏,就得先获取这把锁,拿到锁的线程开始执⾏,满⾜⼀定条件后,释放GIL锁,所
有线程⼜开始竞争这个GIL锁。

阻塞 非阻塞 同步 异步

  1. 同步:多个任务之间有先后顺序执⾏,⼀个执⾏完下个才能执行。(串行)
  2. 异步:多个任务之间没有先后顺序,可以同时执⾏,有时候⼀个任务可能要在必要的时候获取另⼀
    个同时执行的任务的结果,这个就叫回调!
  3. 阻塞:如果卡住了调用者,调⽤者不能继续往下执行,就是说调⽤者阻塞了。
  4. 非阻塞:如果不会卡住,可以继续执行,就是说非阻塞的

GIL 与 互斥锁
GIL锁,Global Interpreter Lock(全局解释器锁),它不是Python的特性,是 cpython解释器⾥的特性。就是线程在执⾏前,必须得获取到这个GIL锁,才能执⾏,也就是保证了进程中,同时只有⼀个线程在执⾏

GIL 存在的原因

  • 单核计算机时代,计算机的算⼒有限,同⼀时刻只有⼀个线程执⾏,GIL的设计保证同时只有⼀个线程在运⾏且在IO操作时有效的提⾼了计算效率。
  • 多核计算机时代,GIL锁却严重影响了Python的效率。多核情况下,单进程多线程,⽐单核情况下,效率差很多,原因是单核下多线程,每次释放GIL,唤醒的那个线程都能获取到GIL锁,所以能够⽆缝执⾏,但多核下,CPU0释放GIL后,其他CPU上的线程都会进⾏竞争,但GIL可能会⻢上⼜被
    CPU0拿到,导致其他⼏个CPU上被唤醒后的线程会醒着等待到切换时间后⼜进⼊待调度状态,这样会造成线程颠簸(thrashing),导致效率更低 。
  • python下想要充分利⽤多核CPU,就⽤多进程,原因是什么呢?原因是:每个进程有各⾃独⽴的
    GIL,互不⼲扰,这样就可以真正意义上的并⾏执⾏,(多线程并不是真正意义上的并⾏执⾏,只能算
    并发执⾏)所以在python中,多进程的执⾏效率优于多线程(仅仅针对多核CPU⽽⾔)。

GIL释放条件

  • 线程执⾏完
  • IO操作
  • 阙值:Python2–ticks计数(100次操作);Python3–计时器(执⾏时间达到阈值后,当
    前线程释放GIL

互斥锁
GIL锁存在的⽬的,就有保证了同⼀时刻只有⼀个线程在运⾏,⼀定程度上做到了线程安全。但不能完
全保证线程安全,因为多线程共享全局变量,在多个线程⼏乎同时操作全局变量是,可能会导致数据
紊乱(因为GIL锁的释放条件,⼀个线程可能还没有计算完,就被其他线程抢去执⾏),此时引⼊互斥
锁的,保证该线程执⾏完再让其他的线程执⾏。

协程
协程,⼜称微线程,纤程,英⽂名Coroutine。是python个中另外⼀种实现多任务的⽅式,只不过⽐
线程更⼩占⽤更⼩执⾏单元。 为啥说它是⼀个执⾏单元,因为它⾃带CPU上下⽂。这样只要在合适的时
机, 我们可以把⼀个协程切换到另⼀个协程。 这个切换没有线程的切换开销。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值