带你从入门到精通——Python(十四. 多任务编程)

建议先阅读我之前的博客,掌握一定的Python前置知识后再阅读本文,链接如下:

带你从入门到精通——Python(一. 基础知识)-CSDN博客

带你从入门到精通——Python(二. 判断语句和循环语句)-CSDN博客

带你从入门到精通——Python(三. 函数基础)-CSDN博客

带你从入门到精通——Python(四. 五大容器一)-CSDN博客

带你从入门到精通——Python(五. 五大容器二)-CSDN博客

带你从入门到精通——Python(六. 函数进阶)-CSDN博客

带你从入门到精通——Python(七. 文件操作)-CSDN博客

带你从入门到精通——Python(八. 异常、模块和包)-CSDN博客

带你从入门到精通——Python(九. 面向对象一)-CSDN博客

带你从入门到精通——Python(十. 面向对象二)-CSDN博客

带你从入门到精通——Python(十一. 闭包、装饰器和深浅拷贝)-CSDN博客

带你从入门到精通——Python(十二. 迭代器、生成器和正则表达式)-CSDN博客

带你从入门到精通——Python(十三. 网络编程)-CSDN博客

目录

十四. 多任务编程

14.1 多任务

14.2 多进程

14.2.1 进程

14.2.2 多进程的创建

14.2.3 多进程的注意事项

14.3 多线程

14.3.1 线程

14.3.2 多线程的创建

14.3.3 多线程的注意事项

14.3.4 线程同步

14.4 全局解释器锁

14.5 进程和线程的对比


十四. 多任务编程

14.1 多任务

        多任务是指计算机在同一时间内执行多个任务,多任务的实现能够充分利用CPU资源,提高程序的执行效率。

        多任务有以下两种表现形式:

        并发:在一段时间内,CPU交替执行任务,操作系统通过时间片轮转的方式,为每个任务分配一小段CPU时间片,在当前任务被分配的时间片用完后,操作系统会暂停当前任务,切换到下一个任务继续执行,并发本质上还是单任务,但由于CPU的执行和切换速度实在是太快了,表面上我们感觉就像各个任务都在同时执行一样。

        并行:在一段时间内,计算机真正的同时一起执行多个任务。

14.2 多进程

        多进程是多任务的一种实现方式。

14.2.1 进程

        进程(Process)是操作系统进行资源分配的最小单元,一个程序运行时都会默认创建一个进程,这个默认创建的进程被称为主进程,在程序运行后在主进程上创建的新进程被称为子进程,因此一个程序运行后至少有一个进程

        此外,在计算机中每个进程都有一个唯一的进程号,用于区分不同的进程,便于进行进程管理,在进程结束后其进程号也会被释放,因此进程号可以反复使用。

        在Python中当前进程的进程名可以使用multiprocessing.current_process().name获得;当前进程的进程号可以使用multiprocessing.current_process().pid或者os.getpid()获得;当前进程的父进程可以使用os.getppid()获得,当前进程的父进程指创建当前进程的进程

14.2.2 多进程的创建

        在Python中,子进程可以通过multiprocessing.Process(group=None,target=None, name=None,args=(),kwargs={})创建,其中各个参数的含义如下:

        参数group是为了与threading.Thread类的接口保持一致而保留的,目前在multiprocessing.Process中并未使用,因此通常传入None。

        参数target用于指定子进程要执行的可调用对象(通常是一个函数),当子进程启动后,会自动调用该对象。

        参数name用于指定子进程的名称,如果不指定,系统会为子进程自动分配一个默认名称。

        参数args以元组的形式向参数target指定的可调用对象传递位置参数,元组中的元素会按照顺序依次传递给可调用对象。

        参数kwargs以字典的形式向参数target指定的可调用对象传递关键字参数,字典的键是参数名,值是参数值。

        具体示例如下:

import multiprocessing
import os
import time
def eat(name):
    print(multiprocessing.current_process().name)  # p1
    print(os.getpid()) # 30808
    print(os.getppid()) # 27864,即主进程的进程号
    for i in range(1, 3):
        print(f'{name} is eating food for {i} minute!!')
        time.sleep(0.3) # 使程序暂停0.3秒

def watch(name):
    print(multiprocessing.current_process().name) # p2
    print(multiprocessing.current_process().pid) # 24064
    print(os.getppid()) # 27864
    for i in range(1, 3):
        print(f'{name} is watching TV for {i} minute!!')
        time.sleep(0.3)

print(__name__)
if __name__ == '__main__':
    print(multiprocessing.current_process().name) # MainProcess
    print(os.getpid()) # 27864
    print(os.getppid()) # 19296
    p1 = multiprocessing.Process(target = eat, name = 'p1', args=('Bob',))
    p2 = multiprocessing.Process(target = watch, name = 'p2', kwargs = {'name':'John'})
    p1.start() # 启动进程p1执行任务
    p2.start() # 启动进程p2执行任务
'''
__main__
__mp_main__
Bob is eating food for 1 minute!!
__mp_main__
John is watching TV for 1 minute!!
Bob is eating food for 2 minute!!
John is watching TV for 2 minute!!
'''

        注意:由于子进程在创建时会重新导入父进程模块,因此在使用多进程实现多任务时,必须在父进程模块中添加以下判断:

if __name__ == '__main__':
    pass

如果不添加上述判断,子进程则会重新执行父进程模块中的全部代码,导致递归创建更多的子进程,导致出现无限循环,子进程在重新导入父进程模块模块时,该文件的__name__变量的值为'__mp_main__'

14.2.3 多进程的注意事项

        多进程的注意事项有以下三个:

        1. 进程之间执行是无序的,它是由操作系统调度决定的,操作系统调度哪个进程,哪个进程就先执行,没有被调度的进程不能执行,进程的调度通常是使用抢占式调度算法,即根据一定的规则中断当前正在执行的进程,转而去执行其他进程,而非抢占式调度算法则是需要进程执行完毕或是进程主动结束执行后才能执行其他进程。

        2. 进程之间不共享全局变量,多进程中的全局变量是指定义在以下判断之外的变量:

if __name__ == '__main__':
    pass

由于子进程会重新导入父进程模块时,会重新执行全局变量的赋值代码,相当于深拷贝了父进程中的全局变量,因此进程间的全局变量虽然变量名相同但不是同一个变量。

        3. 默认情况下,为了保证子进程能够正常的运行,父进程会等所有的子进程执行结束后再结束,使用以下两种方法可以在父进程结束时同时结束子进程,不让父进程继续等待子进程的执行:

        在父进程中为子进程设置守护进程:process_name.daemon = True

        在父进程中直接终止子进程:process_name.terminate()。

14.3 多线程

        多线程也是多任务的一种实现方式。

14.3.1 线程

        线程(Thread)依附于进程执行,是CPU调度和程序执行的最小单元,进程只负责接收操作系统分配的资源,而利用这些资源执行程序的是线程,因此在一个进程中会默认创建一个线程,这个默认创建的线程被称为主线程,在该进程中创建的新线程被称为子线程,因此每个进程至少都有一个线程线程自己不拥有系统资源,但同一个进程中的各个线程共享进程所拥有的全部系统资源

14.3.2 多线程的创建

        在Python中,子线程可以通过threading.Thread(group=None,target=None, name=None,args=(),kwargs={})创建,其中各个参数的含义如下:

        参数group是为了与multiprocessing.Process类的接口保持一致而保留的,目前在threading.Thread中并未使用,因此通常传入None。

        参数target用于指定子线程要执行的可调用对象(通常是一个函数),当子线程启动后,会自动调用该对象。

        参数name用于指定子线程的名称,如果不指定,系统会为子线程自动分配一个默认名称。

        参数args以元组的形式向参数target指定的可调用对象传递位置参数,元组中的元素会按照顺序依次传递给可调用对象。

        参数kwargs以字典的形式向参数target指定的可调用对象传递关键字参数,字典的键是参数名,值是参数值。

        具体示例如下:

import threading
import time
def eat(name):
    for i in range(1, 3):
        print(f'{name} is eating food for {i} minute!!')
        time.sleep(0.3) # 使程序暂停0.3秒

def watch(name):
    for i in range(1, 3):
        print(f'{name} is watching TV for {i} minute!!')
        time.sleep(0.3)

if __name__ == '__main__':
    t1 = threading.Thread(target = eat, name = 't1', args=('Bob',))
    t2 = threading.Thread(target = watch, name = 't2', kwargs = {'name':'John'})
    t1.start() # 启动线程t1执行任务
    t2.start() # 启动线程t2执行任务
'''
Bob is eating food for 1 minute!!
John is watching TV for 1 minute!!
John is watching TV for 2 minute!!
Bob is eating food for 2 minute!!
'''

        注意:使用多线程实现多任务时可以不添加如下的判断:

if __name__ == '__main__':
    pass

14.3.3 多线程的注意事项

        多线程的注意事项有以下三个:

        1. 线程之间执行是无序的,它是由CPU调度决定的,CPU调度哪个线程,哪个线程就执行,没有被调度的线程是不能执行的,线程的调度通常也是使用抢占式调度算法。

        2. 线程之间共享全局变量,线程中的全局变量是指一个进程中被共享的变量。

        3. 默认情况下,为了保证子线程能够正常的运行,主线程会等所有的子线程执行结束后再结束,使用以下方法可以在主线程结束时同时结束子线程,不让主线程继续等待子线程的执行:

        在进程中为子线程设置守护进程:thread_name.daemon = True

14.3.4 线程同步

        由于线程间共享全局变量,可能会导致多个线程同时访问和修改一个全局变量的情况,导致全局变量出现数据不一致的错误,线程同步可以解决该问题,线程同步是指在多线程中,通过特定的机制和方法,对多个线程的执行顺序以及对全局变量的访问进行协调和控制,以确保程序的正确性和数据的一致性,使用互斥锁即可实现线程同步。

        互斥锁能够对全局变量进行锁定,保证同一时刻只能有一个线程去操作全局变量,进而实现线程同步,具体的过程是:多个线程一起去争抢互斥锁,抢到互斥锁的线程先执行,没有抢到互斥锁的线程进行等待,当互斥锁被释放后,各个线程再去争抢互斥锁,示例如下:

import threading

def add1():
    mutex.acquire()  # 使用acquire()方法获取互斥锁
    global tot
    for _ in range(1000000):
        tot += 1
    print(tot) # 1000000
    mutex.release()  # 使用release()方法释放互斥锁

def add2():
    mutex.acquire() # 使用acquire()方法获取互斥锁
    global tot
    for _ in range(1000000):
        tot += 1
    print(tot) # 2000000
    mutex.release() # 使用release()方法释放互斥锁

if __name__ == '__main__':
    tot = 0
    mutex = threading.Lock() # 创建互斥锁
    t1 = threading.Thread(target=add1)
    t2 = threading.Thread(target=add2)
    t1.start()
    t2.start()

        在使用互斥锁时需要注意死锁问题,如果未在合适的地方注意释放互斥锁,线程就一直等待互斥锁的释放,进而出现死锁问题,涉及死锁的线程会陷入无限等待状态,无法继续执行后续任务。

14.4 全局解释器锁

        在 Python 里,存在全局解释器锁(Global Interpreter Lock,GIL),它是一个互斥锁保证了同一时刻只有一个线程可以执行Python代码。

        由于GIL的存在,单进程多线程无法充分利用多核CPU的优势,只能轮流使用CPU的一个核心,而且如果某个线程出现异常且没有被正确捕获,可能会导致整个进程崩溃,影响其他线程的正常运行,稳定性较差。

        而在多进程中,每个进程都有自己独立的Python解释器和GIL,因此多进程可以充分利用多核 CPU的优势,并行执行多个任务,并且即使某个进程由于出现异常而崩溃,也不会影响其他进程的正常运行,稳定性更高。

14.5 进程和线程的对比

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

        2. 进程之间不共享全局变量,而线程之间共享全局变量。

        3. 创建进程的资源开销大,而创建线程的资源开销小。

        4. 在Python中,多进程比单进程多线程的稳定性要强。

        5. 在Python中,多进程能够充分利用多核CPU的优势,而单进程多线程只能轮流使用CPU的一个核心。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值