第4篇 编程进阶
第18章 并发编程
人类有时可以同时处理两件事情,比如,一边吃饭一边看电视,吃饭和看电视这两件事情是并行处理的。而计算机处理问题的方式与人类不同,同一个CPU处理问题是线性的,一次只能处理一个命令。那么,计算机一边播放音乐一边处理文档是如何实现的呢?这就要依靠并发编程了。
使用并发编程可以充分利用计算机的时间碎片,为每个任务分配一小段时间,多个任务交替执行,由于计算机执行速度很快,只要分配的时间片足够短,多个任务就好像在同时进行一样。
18.1 多线程的相关概念
18.1.1 进程与线程
进程是计算机程序关于某数据集上的一次运行活动,是操作系统资源分配和调度的基本单位。
在操作系统中,每执行一个应用程序就会创建一个新的进程。每一个进程都有自己的地址空间、内存和数据,操作系统负责管理进程并为进程分配时间等资源。由于每个进程都包含独立的数据,所以不同进程之间不能直接共享数据,只能使用进程间通信。
一个进程中至少有一个线程来执行程序,如果一个进程想要同时处理多个任务,就需要多个线程并发执行,其中每个任务都对应一个线程。例如,在使用QQ时,可以同时开启多个会话。
进程与线程的关系如图18-1所示。
18.1.2 并行与并发
并行是指多个任务同时执行,在操作系统中是指多个任务无论是从宏观上还是微观上都是同时执行。
并发是指在一段时间内,多个任务从宏观上看起来是同时执行,但是实际从微观来看还是顺序执行。
使用计算机时,用户可以同时处理多个任务,如:一边播放音乐,一边打开浏览器浏览网页,还可以打开记事本记录今天要处理的事情,这些任务对应的就是进程,这些进程在用户看来是同时在处理,但是在计算机的微观层面上是否也是同时在处理呢?
现在的计算机普遍都是多核CPU,如果计算机在分配资源时,将这3个任务分配给3个CPU去执行,那么这3个进程的运行状态就是并行。如果这3个任务分配给了同一个CPU,CPU为每个进程分配一小段时间片,使得这3个进程可以交替执行,那这3个进程的运行状态就是并发,如图18-2所示。
并行和并发的概念也同样适用于线程,不过在Python中,同一个进程中的线程只会占用一个内核CPU,如果想在Python中实现并行,只能创建多个进程。本书在此之前的示例执行的都是单一任务,如果要同时执行多种任务怎么办呢?在Python中,有以下三种方法。
第一种方法,创建多个进程,每个进程执行一种任务。
第二种方法,创建一个进程,一个进程中创建多个线程,每个线程执行一个任务。
第三种方法,创建多个进程,每个进程中创建多个线程,进程和线程同时执行任务(这种方法过于复杂,实际开发中很少采用)。
执行多种任务的方法概括起来如图18-3所示。
创建进程需要消耗更多的资源,而且一般需要执行多任务时,各个任务之间可能有先后顺序,也可能需要进行数据传输和交换,而进程之间数据无法共享,只能使用进程间通信,因此一般我们选择使用多线程的方式来执行多任务。
使用多线程可以完成并发,实现宏观上的“多任务”模式,多个线程是属于同一个进程的,这里的“多任务”是指同一个进程内的多件任务。例如,运行QQ时,我们在会话窗口输入文字的同时也能接收到对方传来的消息,这就需要两个线程来分别完成“接收消息”和“输入文字”的任务。
编程宝典
使用多线程的优点
使用多线程有以下几个优点。
(1)使用多线程编程,可以将占据时间长的任务放到后台去处理,例如,使用word打印文件时,将打印程序放到后台去处理,不影响用户继续编辑文件。
(2)使用多线程编程,有时可以充分利用计算机的时间片段,提高程序的整体运行速度。例如,执行一些用户输入、网络收发数据等需要等待的任务时,将这些任务放到线程中去处理能提高程序的运行效率。
(3)使用多线程编程,可以提高用户的使用体验,增强程序的交互性。例如,在视频播放时,用户可以随时点击暂停按钮,停止播放。
18.1.3 线程的生命周期
一个线程有五种状态,分别为新建、就绪、运行、阻塞和死亡。
(1)新建状态:新创建的线程对象处于新建状态。
(2)就绪状态:当调用线程对象的start()方法后,线程处于就绪状态,等待调度。
(3)运行状态:处于就绪状态的线程获得CPU等资源后就可以转为运行状态,处于运行状态的线程可以执行本线程的代码。
(4)阻塞状态:阻塞状态根据产生阻塞的原因又可以分为三种:等待阻塞、同步阻塞和其他阻塞。
等待阻塞:一个A线程可以通过调用B线程的join()方法使A线程本身进入阻塞状态,直到B线程执行完毕后,A线程才从阻塞状态转变为就绪状态。
同步阻塞:同步阻塞是指两个线程同时想访问共享数据时,一旦其中一个线程,比如A线程获得访问权限,那么B线程就进入阻塞状态,直到A线程访问完毕后,B线程才从阻塞状态转变为就绪状态。
其他阻塞:在线程运行过程中,当调用sleep()函数或等待接收用户输入的数据时,线程进入阻塞状态,直到sleep()函数执行完毕或用户输入完毕,线程才从阻塞状态转变为就绪状态。
处于运行状态的线程遇到以上几种阻塞情况时,进入阻塞状态。
(5)死亡状态:当一个线程的程序执行完毕,或者发生错误或异常,线程就进入死亡状态。线程的五种状态之间的转换关系如图18-4所示。
18.1.4 线程安全
实际开发过程中,很多时候都需要用到多线程,多个线程同时执行时,可能会同时访问共享数据。例如,某网络商店在双十一期间搞秒杀活动,商品数量一共有n件,客户下单购买时,程序后台首先判断当前库存数量n是否大于0,如果大于0,客户就可以下单付款,相应地库存数量变为n-1。当有多个顾客同时购买时,就会有多个线程同时访问这段代码,如果A线程和B线程同时获得了当前的商品数量n,并且判断出n大于0,然后各自执行下单操作,最后可能会导致A和B都下单完毕时,库存数量本应变为n-2,却因为线程交替执行的原因导致库存数量变为n-1,导致数据不同步,产生逻辑错误,如图18-5所示。
所以,在进行多线程开发时,当多个线程都需要访问共享数据时需要着重考虑线程安全问题。
那么,线程安全的问题如何解决呢?
目前,解决线程访问共享数据的安全问题,通用做法是给共享数据上一道锁。共享数据就好像一个密室,一开始处于未锁定状态,当线程访问共享数据时会将密室锁定,其他线程就无法再访问,等该线程访问完毕,将锁打开,其他线程才能继续访问,如图18-6所示。
18.2 多线程开发
Python中提供了几个实现多线程开发的模块,例如:_thread和threading,等等。_thread和threading模块提供了创建和管理线程的方法,_thread是低级模块,threading模块提供了对_thread模块的封装,是高级模块。_thread模块中,当主线程结束时,子线程无论是否已完成都将被强制结束,而threading模块能确保重要子线程完成后进程才结束。所以,在实际开发时一般选用threading模块。
18.2.1 多线程
在Python中,创建多线程有以下两种方式:使用_thread.start_new_thread()函数创建线程;使用threading模块提供的Thread类创建线程。
1.使用_thread模块提供的start_new_thread()函数创建线程
_thread模块提供了start_new_thread()方法来创建线程。其语法格式如下所示。
_thread.start_new_thread(function,args[,kwargs])
参数说明:
Function:线程执行的函数。
Args:元组类型,表示传递给function的参数。
Kwargs:可选参数。
返回值:线程的标识。
_thread.start_new_thread()方法创建一个新线程并返回其标识。
【例】使用_thread模块的start_new_thread()方法创建一个新线程。具体代码如下所示。
import _thread
import time