第13章 进程和线程_0717ppt课件.pptx
第13章 进程和线程_0717 第13章 进程和线程 什么是进程 进程的创建方式 进程间通信 什么是线程 线程的基本操作 线程锁 线程同步 掌握了解掌握熟悉 学习目标掌握 创建进程的方式,进程间通信12掌握 线程的基本操作,线程锁熟悉 线程同步34了解 什么是进程,什么是线程 目录页01 什么是进程02 进程的创建方式03 进程间通信Queue04 什么是线程05 线程的基本操作06 线程锁 目录页07 线程同步08 实例1:生产者与消费者模式 过渡页01 什么是进程02 进程的创建方式03 进程间通信Queue04 什么是线程05 线程的基本操作06 线程锁 什么是进程程序是一个没有生命的实体,它包含许多由程序设计语言编写的、但未被执行的指令,这些指令经过编译和执行才能完成指定动作。for i in range(1,10):for j in range(1, i + 1):print(%d%d=%-2d %(j,i,i*j), end = )print() 什么是进程程序被执行后成为了一个活动的实体,这个实体就是进程。操作系统调度并执行程序,这个“执行中的程序”称为进程。进程是操作系统进行资源分配和调度的基本单位。 什么是进程在Windows操作系统下打开任务管理器,单击任务管理器窗口中的“进程”选项卡查看计算机中所有的进程。 什么是进程每个进程都在内存中占据一定空间,进程占据的内存空间一般由控制块、程序段和数据段三个部分组成。控制块控制块系统为管理进程专门设置的数据结构,常驻于内存中,用于记录进程的外部特征与进程的运动变化过程。程序段程序段用于存放程序执行代码的一块内存区域。数据段数据段存储变量和进程执行期间产生中间或最终数据的一块内存区域。 什么是进程随着外界条件的变化,进程的状态会发生变化。在五态模型中,进程有新建态、就绪态、运行态、阻塞态和终止态这五个状态。新建态新建态就绪态就绪态运行态运行态阻塞阻塞态态终止终止态态 什么是进程除了以上五种状态之外,进程还有一个挂起态。挂起态是一种主动行为,它是在计算机内存资源不足、处理器空闲、用户主动挂起、系统检查资源使用情况等条件下将进程暂时调离出内存形成的,在条件允许时可再次被调回内存。与挂起态相比,阻塞态是一种被动行为,它是在等待事件或者获取不到资源而引发的等待表现。 什么是进程下面通过一张图来描述进程状态间的转换关系。 什么是进程进程具有以下一些特点: 动态性动态性 并并发性发性 异步性异步性 独立性独立性 过渡页01 什么是进程02 进程的创建方式03 进程间通信Queue04 什么是线程05 线程的基本操作06 线程锁 通过fork()函数创建进程在Unix/Linux系统中,通过Python的os模块中封装的fork()函数可以轻松地创建一个进程。fork()以上函数执行后,操作系统会建立当前进程的副本以实现进程的创建,此时原有的进程被称为父进程父进程,复制的进程被称为子进程子进程。 通过fork()函数创建进程fork()函数的一次调用产生两个结果:若当前执行的进程是父进程,fork()函数返回子进程ID;若当前执行的进程是子进程,fork()函数返回0。如果fork()函数调用时出现错误,那么进程创建失败,将返回一个负值。 通过fork()函数创建进程示例:import osimport timevalue = os.fork()if value = 0: print(-子进程-) time.sleep(2)else: print(-父进程-) time.sleep(2)-父进程父进程-子进程子进程- 通过fork()函数创建进程子进程和父进程执行的顺序是不确定的,会受到时间片、调度优先级或其它因素的影响。 通过fork()函数创建进程若程序中顺序调用两次fork()函数,那么第一次调用fork()后系统中存在的两个进程都会调用第二个fork()函数创建新进程。“父进程父进程1 1”和和“子进程子进程1 1”再次复再次复制出两个子进程,制出两个子进程,“父进程父进程1 1”成为成为“子进程子进程2 2”的父进程,的父进程,“子进程子进程1 1”成为成为“子进程子进程3 3”的父进程,变成的父进程,变成“父进程父进程2 2”。 通过fork()函数创建进程示例:print(-第一次fork()调用-)value = os.fork()if value = 0: print(-进程1-)else: print(-进程2-)print(-第二次fork()调用-)value = os.fork()if value = 0: print(-进程3-)else: print(-进程4-)-第一次第一次fork()调用调用-进程进程2-进程进程1-第二次第二次fork()调用调用-进程进程4-进程进程4-进程进程3-进程进程3- 多学一招:获取当前进程的ID进程ID是进程的唯一标识。os模块提供了getpid() 和getppid()函数来分别获取当前进程ID和当前进程的父进程ID。process = os.fork()if process = 0: print(我是子进程%d,父进程是%d %(os.getpid(), os.getppid() 我是子进我是子进程程2498,父,父进程是进程是2497 通过Process类创建进程通过Process类的构造方法Process()可以创建一个代表子进程的Process对象,该方法的声明如下:Process(group=None, target=None, name=None, args=(), kwargs=, *, daemon=None) group - 必须为None,为以后扩展功能保留的参数。target - 表示子进程的功能函数,用于为子进程分派任务。 name - 表示当前进程的名称。若没有指定,则默认为Process-N,N为从1开始递增的整数。 通过Process类创建进程class MyProcess(Process): def _init_(self, interval): Process._init_(self) self.interval = interval def run(self): time_start = time.time() time.sleep(self.interval) time_stop = time.time() print(子进程%s执行结束,耗时%0.2f秒 % (os.getpid(), time_stop - time_start)自定义一个继承自Process类的子类,调用子类的构造方法亦可创建子进程。my_process = MyProcess(5) 通过Process类创建进程if _name_ = _main_: my_process = MyProcess(5) my_process.start()进程在创建完之后,需要通过start()方法启动。 通过Process类创建进程Windows系统中使用multiprocessing模块时,必须采用“if _name_ =_main_”的方式运行程序。 通过Pool类批量创建进程多进程模块multiprocessing中提供了Pool(进程池)类,通过Pool类的构造方法Pool()可以批量创建子进程。Pool(processes=None, initializer=None, initargs=(), maxtasksperchild=None, context=None) processes - 表示进程的数量。若processes参数设为None,则会使用os.cpu_count()返回的结果。 通过Pool类批量创建进程示例:pool = Pool(processes=5)进程池的内部维护了一个进程序列。当使用进程池中的进程执行任务时,如果没有达到进程池中的进程数量的最大值,那么会创建一个新的进程来执行任务;如果进程池中没有可供使用的进程,那么程序会等待,直到进程池中有可用的进程为止。 通过Pool类批量创建进程Pool类中提供了一些操作进程池的方法,关于这些方法的说明如下表所示。 通过Pool类批量创建进程apply_async(self, func, args=(), kwds=, callback=None,error_callback=None)通过Pool类的apply_async()方法可以采用非阻塞的方式给进程池中的进程添加任务。 func - 表示函数名称。args和kwds - 表示函数func接收的参数。 callback - 表示程序执行成功后调用的函数。 通过Pool类批量创建进程def work(num): print(进程%s:执行任务%d% (os.getpid(), num) time.sleep(2)if _name_ = _main_: pool = Pool(3) for i in range(9): pool.apply_async(work, (i,) time.sleep(3) print(主进程执行结束)示例:进程进程6956: 执行任务执行任务0进程进程6776: 执行任务执行任务1进程进程5076: 执行任务执行任务2.进程进程5076: 执行任务执行任务5主进程执行结束主进程执行结束主进程在主进程在三个子进程三个子进程69566956、67766776和和50765076执执行行完完6 6个任务个任务后后直接直接退出退出,剩余的剩余的3 3个任务未执行个任务未执行。 通过Pool类批量创建进程若希望主进程能等待所有的子进程执行完之后结束,需要通过join()方法将主进程切换成阻塞状态。pool.close() # 关闭进程池pool.join() # 阻塞主进程 通过Pool类批量创建进程通过Pool类的apply () 方法可以采用阻塞的方式给进程池中的进程添加任务。apply(self, func, args=(), kwds=) 通过Pool类批量创建进程def work(num): print(进程%s: 执行任务%d% (os.getpid(), num) time.sleep(2)if _name_ = _main_: pool = Pool(3) for i in range(9): pool.apply(work, (i,) time.sleep(3) print(主进程执行结束)进程进程5928: 执行任务执行任务0进程进程6408: 执行任务执行任务1进程进程5840: 执行任务执行任务2.进程进程5840: 执行任务执行任务8主进程执行结束主进程执行结束主进程在主进程在子进程全子进程全部执部执行行完任务完任务后才后才退退出。出。示例: 过渡页01 什么是进程02 进程的创建方式03 进程间通信Queue04 什么是线程05 线程的基本操作06 线程锁 进程间通信Queue每个进程中所拥有的数据(包括全局变量)都是独有的,无法与其它进程共享。进程进程A A进程进程B B数据数据数据数据 进程间通信Queuemultiprocessing模块中提供了Queue类,使用该类的构造方法Queue()可以创建能管理共享资源的队列。Queue(self, maxsize=-1)以上方法中,maxsize参数表示队列中数据的最大长度,若该参数小于0或不设置,说明队列没有长度限制,可以存储任意个数据 。 进程间通信Queue队列的作用类似于数据中转站,可以供多个进程向其内部写入或读取数据。QueueQueue类中提供了类中提供了put()put()和和get()get()这两个方法分别向队这两个方法分别向队列中写入数据和从列中写入数据和从队列队列中读中读取并删除数据取并删除数据。 进程间通信Queueput()方法的声明如下:put()方法put(item, block=True, timeout=None) item - 表示向队列中写入的数据。 block - 表示是否阻塞队列。 timeout - 表示超时时长,默认为None。 进程间通信Queue当调用put()方法向队列中写入数据时,若将block参数设为True、timeout参数设为正值,则队列在装满数据后会先阻塞timeout指定的时长,并在超时后抛出Queue.Full异常;若将block参数设为False,则队列在装满数据后会立即抛出Queue.Full异常。 进程间通信Queueget()方法的声明如下:get()方法get(block=True, timeout=None) block - 表示是否阻塞队列。 timeout - 表示超时时长,默认为None。 进程间通信Queue当调用get()方法从空队列中读取数据时,若将block参数设为True且timeout参数设为正值,则队列在等待timeout指定的时长后再抛出Queue.Empty异常;若将block参数设为False,则会队列会立即抛出Queue.Empty异常。 过渡页01 什么是进程02 进程的创建方式03 进程间通信Queue04 什么是线程05 线程的基本操作06 线程锁 什么是线程思考:什么是线程? 什么是线程线程是系统进行运算调度的最小单位,也被称为轻量级进程,它包含在进程之中,是进程的实际运作单位。进程中可以包含多个线程,每个线程是进程中单一顺序的控制流,可以并行执行不同的任务。 什么是线程线程一般可分为以下几种类型:主线程主线程程序启动时,操作系统在创建进程,的同时会立即运行一个线程,该线程通常被称为主线程。子线程子线程程序中创建的其它线程。守守护线程护线程守护线程是在后台为其它线程提供服务的线程,它独立于程序,不会因程序的终止而结束。前台前台线程线程相对于守护线程的其它线程称为前台线程。 什么是线程线程与进程相似,也具有五个状态,分别是新建态、就绪态、运行态、阻塞态和消亡态。 什么是线程线程因某些条件发生时会由运行态转换为阻塞态,这些条件可能为以下任意一种: 线程主动调用sleep()函数进入休眠状态; 线程试图获取同步锁,但是该锁正被其它线程持有; 线程等待一些I/O操作完成; 线程等待某个条件触发。 过渡页01 什么是进程02 进程的创建方式03 进程间通信Queue04 什么是线程05 线程的基本操作06 线程锁 线程的创建和启动模块threading中定义了Thread类,该类专门用于管理线程。可以直接通过Thread类的构造方法Thread()创建线程,该方法的声明如下:Thread(group=None, target=None, name=None, args=(), kwargs=, *, daemon=None) 线程的创建和启动使用Thread()方法创建的线程默认是前台线程。前台线程的特点是主线程会等待其执行结束后终止程序。 线程的创建和启动创建线程的示例如下:import threadingfrom threading import Threadimport timedef task(): time.sleep(3) print(子线程运行,名称为:%s% threading.currentThread().name)thread_one = Thread(target=task) 线程的创建和启动使用构造方法Thread()创建线程时,还可以将daemon参数设为True,创建一个后台线程。thread_two = Thread(target=task, daemon=True) 线程的创建和启动还可以自定义一个继承自Thread的子类,在该子类中重写run()方法,再利用子类的构造方法创建线程。class MyThread(Thread): def run(self): time.sleep(3) message = self.name + 运行 print(message)thread_three = MyThread() 线程的创建和启动线程创建完之后,需要调用start()方法进行启动,以真正地将线程转换为就绪状态,等待操作系统地调度。thread_three.start() 线程的创建和启动threading模块中提供了current_thread()函数,使用该函数可获取当前操作的线程对象。主线程对象的名字叫Main Thread,子线程对象的名字在创建时可以指定,若没有指定名字,将命名为Thread-1、Thread-2 线程的阻塞为了避免线程处于无休止的阻塞态,可以调用join()方法指定等待的时长。join(timeout=None)timeout参数表示以秒为单位的超时时长。若timeout参数设为None,则线程会一直处于阻塞态,直至消亡。 过渡页01 什么是进程02 进程的创建方式03 进程间通信Queue04 什么是线程05 线程的基本操作06 线程锁 互斥锁假设售票厅有100张火车票,它同时开启两个窗口(视为线程)卖票,每出售一张火车票显示当前的剩余票数。由由于两个窗口共同修于两个窗口共同修改同一份车票资源,改同一份车票资源,容易导致票数混容易导致票数混乱乱。 互斥锁为了解决这类问题,Python中引入了互斥锁和可重入锁,保证任一时刻只能有一个线程访问共享的数据。互斥锁互斥锁可重入锁可重入锁 互斥锁互斥锁是最简单的加锁技术,它只有两种状态:锁定(locked)和非锁定(unlocked)。当某个线程需要更改共享数据时,它会先对共享数据上锁,将当前的资源转换为“锁定”状态,其它线程无法对被锁定的共享数据进行修改;当线程执行结束后,它会解锁共享数据,将资源转换为“非锁定”状态,以便其它线程可以对资源上锁后进行修改。 互斥锁在售卖车票的示例中加入互斥锁,示意图如下。每个窗口在修改剩余票数前都每个窗口在修改剩余票数前都会上锁,确保同一时刻只能自会上锁,确保同一时刻只能自己访问剩余票数,一旦修改完己访问剩余票数,一旦修改完票数之后,就对剩余票数进行票数之后,就对剩余票数进行解锁。解锁。 互斥锁threading模块中提供了一个Lock类,通过Lock类的构造方法可以创建一个互斥锁。mutex_lock = threading.Lock() 互斥锁Lock类中定义了acquire()和release()两个方法,分别用于锁定和释放共享数据。acquire()方法可以设置锁定共享数据的时长,其声明如下:acquire(blocking=True,timeout=-1)blocking参数代表是否阻塞当前线程,若设为True(默认),则会阻塞当前线程至资源处于非锁定状态;若设为False,则不会阻塞当前线程。 互斥锁处于锁定状态的互斥锁调用acquire()方法会再次对资源上锁,处于非锁定状态的互斥锁调用release()方法会抛出RuntimeError异常。 死锁思考:什么是死锁? 死锁死锁是指两个或两个以上的线程在执行过程中,由于各自持有一部分共有资源或者彼此通信而造成的一种阻塞的现象。若没有外力作用,线程们将无法继续执行,一直处于阻塞状态。 死锁在使用Lock对象给资源加锁时,若操作不当很容易造成死锁。常见的不当行为主要包括:(1)上锁与解锁的次数不匹配。(2)两个线程各自持有一部分共享资源。 死锁示例:上锁与解锁次数不匹配def do_work(): mutex_lock.acquire() mutex_lock.acquire() mutex_lock.release()if _name_ = _main_: mutex_lock = Lock() thread = Thread(target=do_work) thread.start()程序执行后始终无法程序执行后始终无法结束,只能主动停止结束,只能主动停止运行。运行。 死锁示例:两个线程互相使用对方的互斥锁class ThreadOne(Thread): def run(self): if lock_a.acquire(): . if lock_b.acquire(): . lock_b.release() lock_a.release()class ThreadTwo(Thread): def run(self): if lock_b.acquire(): . if lock_a.acquire(): . lock_a.release() lock_b.release() 死锁若产生像第二种死锁的情况,可以设置锁定的时长,即调用acquire()方法时为timeout参数传入值。lock_b.acquire(timeout=2) 可重入锁RLock类代表可重入锁,它允许同一线程多次锁定和多次释放。通过RLock类的构造方法可以创建一个可重入锁对象。r_lock = RLock() 可重入锁RLock类中包含以下三个重要的属性: _block,表示内部的互斥锁。 _owner,表示可重入锁的持有者的线程ID。 _count,表示计数器,用于记录锁被持有的次数。针对RLock对象的持有线程(属主线程),每上锁一次计数器就+1,每解锁一次就-1。若计数器为0,则释放内部的锁,这时其他线程可以获取内部的互斥锁,继而获取RLock对象。 可重入锁可重入锁的实现原理是通过为每个内部锁关联计数器和属主线程。当计数器为0时,内部锁处于非锁定状态,可以被其它线程持有;当线程持有一个处于非锁定状态的锁时,它将被记录为锁的持有线程,计数器置为1。 过渡页07 线程同步08 实例1:生产者与消费者模式 通过Condition类实现线程同步线程按预定的次序执行称为线程的同步。例如,由线程1执行完任务1,之后由线程2执行完任务2,最后由线程3执行完任务3。 通过Condition类实现线程同步Condition类代表条件变量,它允许线程在触发某些事件或达到特定条件后才开始执行。通过Condition类提供的构造方法可以创建一个实例Condition(lock = None )以上构造方法中只有一个lock参数,该参数用于接收一个Lock对象或RLock对象。若没有为lock参数传入值,Condition对象会自动生成一个RLock对象。 通过Condition类实现线程同步Condition类中提供了与锁相关的acquire()和release()方法,这两个方法与Lock类中的用法一致,该类还提供了以下一些常用的方法。 通过Queue类实现线程同步Queue类表示一个FIFO(先进先出)队列,用于多个线程之间的信息传递。创建队列的方式比较简单,可以直接通过如下构造方法实现:Queue(maxsize=0)以上方法中只有一个maxsize参数,该参数指定了队列的长度,默认为0,表示队列的长度没有任何限制。 通过Queue类实现线程同步Queue类中提供了一些操作队列的常见方法,这些方法的功能说明如右表所示。 过渡页07 线程同步08 实例1:生产者与消费者模式 实例1:生产者与消费者模式生产者与消费者模式通过一个固定大小的缓冲区解决了代表“生产者”和 “消费者”的两个线程在实际运行时发生的强耦合的问题。由于生产者的生产能力与消费者的消费能力互不匹配,导致双方必须互相阻塞等待处理。 实例1:生产者与消费者模式在生产者与消费者模式中,生产者与消费者彼此之间通过缓冲区进行通讯,示意过程如下图所示。 实例1:生产者与消费者模式假设现在有一群生产者(Producer)和一群消费者(Consumer)通过一个市场来交互产品。生产者的“策略”是若市场上剩余的产品少于1000个则生产100个产品放到市场上;消费者的“策略”是若市场上剩余产品的数量多于100个则消费3个产品。 实例1:生产者与消费者模式本实例要求编写代码,模拟以上描述的生产者与消费者模式的场景。 本章主要介绍了两种多任务编程的方式:进程和线程,首先介绍的是关于进程的知识,包括什么是进程、进程的创建方式、进程间的通信,然后介绍的是关于线程的知识,包括什么是线程、线程的基本操作、线程中的锁和线程的同步,最后开发了生产者与消费者模式的实例。通过对本章的学习,希望读者能掌握进程和线程的使用,合理地运用到现实开发中。本章小结