Python多任务编程笔记——学过操作系统之后会更加轻松

一、多任务的介绍

1. 多任务的概念

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

eg: 现在电脑安装的操作系统都是多任务操作系统,可以同时运行着多个软件。

2. 多任务的执行方式
  • 并发:宏观上同时进行,微观上交替进行
  • 并行:多核CPU可以实现多个任务微观上也同时进行
3. 多任务的实现方式
  • 使用进程
  • 使用线程
  • 使用协程

二、进程

1. 进程的介绍

在Python程序中,想要实现多任务可以使用进程来完成,进程是实现多任务的一种方式。

2. 进程的概念

一个正在运行的程序或者软件就是一个进程,它是操作系统进行资源分配的基本单位,也就是说每启动一个进程,操作系统都会给其分配一定的运行资源(内存资源)保证进程的运行。

引进线程后,线程是系统调用的基本单位,而进程仍是资源分配的基本单位

3.多进程的使用
1. 导入进程包
   - import multiprocessing
2. 创建子进程并指定执行的任务
   - 进程对象 = multiprocessing.Process (target=任务名)
3. 启动进程执行任务
   - 进程对象.start()

进程对象一般用:“任务名_process”

(1)Process进程类的说明

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

  • group:指定进程组,目前只能使用None
  • target:执行的目标任务名,这里指函数名(方法名)
  • name:进程名字,一般不用设置
  • args:以元组方式给执行任务传参
  • kwargs:以字典方式给执行任务传参

元组方式传参(args): 元组方式传参一定要和参数的顺序保持一致。

字典方式传参(kwargs): 字典方式传参字典中的key一定要和参数名保持一致。

Process创建的实例对象的常用方法:

  • start():启动子进程实例(创建子进程)
  • join():等待子进程执行结束
  • terminate():不管任务是否完成,立即终止子进程

Process创建的实例对象的常用属性:

name:当前进程的别名,默认为Process-N,N为从1开始递增的整数

(2)多进程完成多任务的代码
import multiprocessing
import time

# 跳舞任务
def dance():
    for i in range(3):
        print("跳舞中")
        time.sleep(0.2)  # 每循环一次停留0.2秒,让结果更加清晰

# 唱歌任务
def sing(x1, x2):
    for i in range(3):
        print("%s和%s唱歌中" % (x1, x2))
        time.sleep(0.2)
        
# 思考
def think(name,age):
    for i in range(3):
        print("%s思考小明%d岁了" % (name, age))
        time.sleep(0.2)

if __name__ == "__main__":  # 若是在当前文件下运行
    # 创建子进程 dance方法不需要加括号
    dance_process = multiprocessing.Process(target=dance)
    # 创建子进程,若需要传参则使用args=(参数)以元组的方式传参
    sing_process = multiprocessing.Process(target=sing, args=('Tom', 'Jack'))
    # 创建子进程,若需要传参则使用kwargs={参数}以字典的方式传参
    think_process = multiprocessing.Process(target=think, kwargs={'name':'Tom', 'age':20})

    # 启动进程
    dance_process.start()
    sing_process.start()
    think_process.start()

执行结果:

跳舞中
Tom思考小明20岁了
Tom和Jack唱歌中
跳舞中
Tom和Jack唱歌中
Tom思考小明20岁了
跳舞中
Tom思考小明20岁了
Tom和Jack唱歌中
4.获取进程编号
(1)获取进程编号的目的

获取进程编号的目的是验证主进程和子进程的关系,可以得知子进程是由那个主进程创建出来的。

(2)获取进程编号的两种操作 os.getpid() os.getppid()
  • 获取当前进程编号:(导入os模块)
 os.getpid()
  • 获取当前父进程编号:
os.getppid()

比如3.2的代码,dance、sing、think三个进程都是由主进程创建的,在三个方法中查询父进程编号则三个父进程编号相同,并且三个进程编号各不相同

三、线程

1. 线程的概念
  • 线程是cpu调度的基本单位,每个进程至少都有一个线程,而这个线程就是我们通常说的主线程
  • 线程的执行需要cpu调度来完成。
  • 同属一个进程的多线程共享进程的全部资源
  • 线程不能够独立执行,必须依存在进程中
2. 多线程的使用(类似多进程)
1. 导入线程模块
   - import threading
2. 创建子线程并指定执行的任务
   - 线程对象 = threading.Thread(target=任务名)
3. 启动线程执行任务
   - 线程对象.start()

线程对象一般用:“任务名_thread”

(1)Thread线程类的说明

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

  • group: 线程组,目前只能使用None
  • target: 执行的目标任务名
  • args: 以元组的方式给执行任务传参
  • kwargs: 以字典方式给执行任务传参
  • name: 线程名,一般不用设置
(2)获取线程信息 threading.current_thread()
threading.current_thread()

三、线程和进程的注意点

1.进程(线程)之间执行是无序的
  • 进程之间执行也是无序的,它是由操作系统调度决定的,操作系统调度哪个进程,哪个进程就先执行,没有调度的进程不能执行。
  • 线程之间执行是无序的,它是由cpu调度决定的 ,cpu调度哪个线程,哪个线程就先执行,没有调度的线程不能执行。
2.主进程(线程)会等待所有的子进程(子线程)执行结束再结束
(1)守护主进程
  • 为了保证子进程能够正常的运行,主进程会等所有的子进程执行完成以后再销毁,设置守护主进程,主进程退出子进程直接销毁,子进程的生命周期依赖与主进程,不让主进程再等待子进程去执行。

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

①设置守护主进程方式:

子进程对象.daemon = True

②在主进程退出之前让子进程销毁

​ 销毁子进程方式:

子进程对象.terminate()
(2)守护主线程

如果想要主线程退出子线程销毁,可以设置守护主进程(主线程)

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

线程对象.setDaemon(True)
线程对象 = threading.Thread(target=show_info, daemon=True)
3.共享全局变量
(1)进程之间不共享全局变量
  • 创建子进程是把主进程资源进行拷贝,也就是说子进程是主进程的一个副本,主进程和子进程是相互独立的。
  • 之所以进程之间不共享全局变量,是因为操作的不是同一个进程里面的全局变量,只不过不同进程里面的全局变量名字相同而已
(2)线程之间共享全局变量

线程之间共享全局变量可能会导致数据出现错误问题,可以使用互斥锁或者线程同步方式来解决这个问题。

同步即线程之间有相互制约关系,即先后顺序

4.优缺点对比
  • 进程优缺点:
    • 优点:可以用多核
    • 缺点:资源开销大
  • 线程优缺点:
    • 优点:资源开销小
    • 缺点:不能使用多核

多进程开发比单进程多线程开发稳定性要强

5.进程是操作系统资源分配的基本单位,线程是CPU调度的基本单位

四、线程同步

1.线程等待(join)
第一个执行的线程.start()
第一个执行的线程.join() # 即第一个任务完成之后才能进行第二个任务
第二个执行的线程.start()
2.互斥锁,类似操作系统的PV操作
(1)互斥锁的概念
  • 互斥锁: 对共享数据进行锁定,保证同一时刻只能有一个线程去操作。
  • 使用互斥锁的好处确保某段关键代码只能由一个线程从头到尾完整地去执行。但是使用互斥锁会影响代码的执行效率,多任务改成了单任务执行。还需要注意:互斥锁如果没有使用好容易出现死锁的情况
(2)互斥锁的使用

threading模块中定义了Lock变量,这个变量本质上是一个函数,通过调用这个函数可以获取一把互斥锁。

互斥锁使用步骤:

# 创建锁
mutex = threading.Lock()

# 上锁
mutex.acquire()

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

# 释放锁
mutex.release()

注意点:

  • acquire和release方法之间的代码同一时刻只能有一个线程去操作
  • 如果在调用acquire方法的时候 其他线程已经使用了这个互斥锁,那么此时acquire方法会堵塞,直到这个互斥锁释放后才能再次上锁。

五、死锁

1.死锁的概念

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

2.避免死锁

在合适的地方释放锁

这只是我在学人工智能时讲的,死锁更多的知识可以看操作系统这本书,我之前在操作系统中学的更加详细

六、协程

1.协程的概念

协程,又称微线程,纤程。英文名Coroutine。协程是python个中另外一种实现多任务的方式。只不过比线程更小占用更小执行单元,即需要更少的资源。

通俗的理解:

在一个线程中的某个函数,可以在任何地方保存当前函数的一些临时变量等信息,然后切换到另外一个函数中执行,注意不是通过调用函数的方式做到的,并且切换的次数以及什么时候再切换到原来的函数由开发者自己确定

2.协程的优点

最大的优势就是协程极高的执行效率。因为函数切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多协程的性能优势就越明显。

第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

3.gevent——一个第三方库。

Python中仅提供了对协程的基本支持,但是不完全。而第三方的gevent为Python提供了比较完善的协程支持。

其原理是当一个任务函数遇到IO(指的是input output 输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的任务函数执行,等到IO操作完成,再在适当的时候切换回来继续执行。

由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有任务函数在运行,而不是等待IO,得以实现多任务,提高程序执行效率。

安装
pip3 install gevent
1. gevent的使用
import gevent

def f(n):
    for i in range(n):
        print(gevent.getcurrent(), i)

g1 = gevent.spawn(f, 5)
g2 = gevent.spawn(f, 5)
g3 = gevent.spawn(f, 5)
g1.join()
g2.join()
g3.join()

运行结果

<Greenlet at 0x10e49f550: f(5)> 0
<Greenlet at 0x10e49f550: f(5)> 1
<Greenlet at 0x10e49f550: f(5)> 2
<Greenlet at 0x10e49f550: f(5)> 3
<Greenlet at 0x10e49f550: f(5)> 4
<Greenlet at 0x10e49f910: f(5)> 0
<Greenlet at 0x10e49f910: f(5)> 1
<Greenlet at 0x10e49f910: f(5)> 2
<Greenlet at 0x10e49f910: f(5)> 3
<Greenlet at 0x10e49f910: f(5)> 4
<Greenlet at 0x10e49f4b0: f(5)> 0
<Greenlet at 0x10e49f4b0: f(5)> 1
<Greenlet at 0x10e49f4b0: f(5)> 2
<Greenlet at 0x10e49f4b0: f(5)> 3
<Greenlet at 0x10e49f4b0: f(5)> 4

可以看到,3个greenlet是依次运行而不是交替运行

2. gevent切换执行
import gevent

def f(n):
    for i in range(n):
        print(gevent.getcurrent(), i)
        #用来模拟一个耗时操作,注意不是time模块中的sleep
        gevent.sleep(1)

g1 = gevent.spawn(f, 5)
g2 = gevent.spawn(f, 5)
g3 = gevent.spawn(f, 5)
g1.join()
g2.join()
g3.join()

运行结果

<Greenlet at 0x7fa70ffa1c30: f(5)> 0
<Greenlet at 0x7fa70ffa1870: f(5)> 0
<Greenlet at 0x7fa70ffa1eb0: f(5)> 0
<Greenlet at 0x7fa70ffa1c30: f(5)> 1
<Greenlet at 0x7fa70ffa1870: f(5)> 1
<Greenlet at 0x7fa70ffa1eb0: f(5)> 1
<Greenlet at 0x7fa70ffa1c30: f(5)> 2
<Greenlet at 0x7fa70ffa1870: f(5)> 2
<Greenlet at 0x7fa70ffa1eb0: f(5)> 2
<Greenlet at 0x7fa70ffa1c30: f(5)> 3
<Greenlet at 0x7fa70ffa1870: f(5)> 3
<Greenlet at 0x7fa70ffa1eb0: f(5)> 3
<Greenlet at 0x7fa70ffa1c30: f(5)> 4
<Greenlet at 0x7fa70ffa1870: f(5)> 4
<Greenlet at 0x7fa70ffa1eb0: f(5)> 4
3. 给程序打补丁
from gevent import monkey
import gevent
import random
import time

def coroutine_work(coroutine_name):
    for i in range(10):
        print(coroutine_name, i)
        time.sleep(random.random())

gevent.joinall([
        gevent.spawn(coroutine_work, "work1"),
        gevent.spawn(coroutine_work, "work2")
])

运行结果

work1 0
work1 1
work1 2
work1 3
work1 4
work1 5
work1 6
work1 7
work1 8
work1 9
work2 0
work2 1
work2 2
work2 3
work2 4
work2 5
work2 6
work2 7
work2 8
work2 9
from gevent import monkey
import gevent
import random
import time

# 有耗时操作时需要
monkey.patch_all()  # 将程序中用到的耗时操作的代码,换为gevent中自己实现的模块

def coroutine_work(coroutine_name):
    for i in range(10):
        print(coroutine_name, i)
        time.sleep(random.random())

gevent.joinall([
        gevent.spawn(coroutine_work, "work1"),
        gevent.spawn(coroutine_work, "work2")
])

运行结果

work1 0
work2 0
work1 1
work1 2
work1 3
work2 1
work1 4
work2 2
work1 5
work2 3
work1 6
work1 7
work1 8
work2 4
work2 5
work1 9
work2 6
work2 7
work2 8
work2 9
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

PCGuo999

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值