Python核心编程3 多线程编程 简介、使用以及同步原语

在Python中,你可以启动一个线程,但却无法停止它。


进程特点:独立,每一个进程拥有自己的内存和数据栈等,所以只能采用进程间通信(IPC)的方式共共享信息。

线程特点:称为轻量级进程,一个进程可以拥有多个线程,它们共享相同的上下文,因此线程间的信息共享和通信更加容易。  在单核CPU中实现不了真正的并发,那么线程执行的时候会按照时分复用的方式完成伪并发。另一个需要注意的问题是,线程无法给予公平的执行时间。这是因为一些函数会在完成前保持阻塞状态,如果没有专门为多线程情况进行修改,会导致CPU的时间分配向这些贪婪的函数倾斜。

全局解释器锁:Python代码的执行是由Python虚拟机(又名解释器主循环)进行控制的。在Python的主循环中同时只能有一个控制线程在执行,就像单核CPU中的多进程一样。内存中可以有很多程序,但是在任意给定时刻只能有一个程序在运行。同理,尽管Python解释器中可以运行多个线程,但是在任意给定时刻只有一个线程会被解释器执行。因此,相比于Java等的多线程来说,Python的多线程会略显鸡肋。Python解释器由于设计时有GIL全局锁,导致了多线程无法利用多核。多线程的并发在Python中就是一个美丽的梦。详细信息:知乎-DarrenChan陈驰的回答 虽然Python的多线程无法实现多核利用,但是可以通过多进程来实现多核。

退出线程:当一个线程完成函数的执行时,它就会退出。另外还可以通过调用诸如thread.exit()之类的退出函数,或者sys.exit()之类的退出Python进程的标准方法,或者抛出SystemExit异常,来使线程退出。不过,不能直接“终止”一个线程。

相关模块:thread和threading。但是要尽量避免使用thread模块,而是使用更高级别的threading模块,它有更好的线程支持。thread模块只有一个同步原语,而threading有很多。且thread模块对于进程何时退出没有控制,当主线程结束时,其他所有线程也都强制结束,不会发出警告或进行适当的清理。而threading模块至少能保证重要的子线程在进程退出前结束。

守护线程:threading模块支持守护线程。守护线程一般是一个等待客户端请求服务的服务器。如果没有客户端请求,守护线程就是空闲的。如果把一个线程设置为守护线程,就表示这个线程是不重要的,进程退出时不需要等待这个线程执行完成。这种守护线程运行在一个无限循环里,在正常情况下不会退出。要将一个线程设置为守护线程,需要在启动线程之前执行如下赋值语句:thread.daemon = True。同样要检查线程的守护状态,只需要检查这个值即可。一个新的子线程会继承父线程的守护标记。整个Python程序(可以理解为主线程)将在所有非守护线程退出之后才退出。


threading模块:

threading.Thread类:是主要的执行对象。其重要的属性有name和daemon,后者作为一个标记线程是否守护进程的布尔标志。 Thread类重要的方法有:

  • __init__(group=None, tatget=None, name=None, args=(),kwargs ={}, verbose=None, daemon=None)。实例化一个线程对象,需要有一个可调用的target,以及其参数args或kkwargs。还可以传递name或group参数。daemon的值会设定Thread.daemon值。
  • start() : 开始执行该线程
  • run() : 定义线程功能的方法,一般会在子类中被应用开发者重写。
  • join(timeout = None) : 直至启动的线程终止之前一直挂起;除非给出了timeout(秒),否则会一直阻塞。
使用Thread类,有很多方法创建线程。选择一个最适合你的应用未来扩展的方法(推荐最后一种方案)。

  • 创建Thread的实例,传给它一个函数。
  • 创建Thread的实例,传给它一个可以调用的类的实例。
  • 派生Thread的子类,并创建子类的实例。
一般来说,当需要一个更加符合面向对象的接口时,会选择后者,否则会选择第一种方案。而第二种方案显得有些尴尬并且稍微难以阅读。

第一种方案编写很简单,只需要在创建Thread实例的时候 t = threading.Thread(target = func,args = ())。 其中func是我们希望线程执行的任务,args是该函数的参数,是一个元组变量。

第三种方案则要派生一个子类,比如

class myThread(threading.Thread):
    def __init__(self,func,args,name=''):
        threading.Thread.__init__(self)
        self.func = func
        self.args = args
        self.name = name
    def run(self):
        self.func(*self.args)

首先要定义其构造函数__init__(),括号中是需要传入的参数。需要注意的是,在这个构造函数中,需要先把父类的构造函数threading.Thread.__init__(self)写在开头,再进行后续操作。     构造函数写完后,则通过重写run()方法的方式来指定我们希望这个线程完成的工作。重写完run()方法后,在后续的代码中得到myThread类的实例(比如t),再调用实例的t.start()方法,线程将会开始工作,执行run方法中的操作。

threading模块的其他函数

  • active_count():返回当前活动的Thread对象个数
  • current_thread():返回当前的Thread对象
  • enumerate():返回当前活动的Thread对象列表
  • settrace(func):为所有线程设置一个trace()函数
  • setprofile(func):为所有线程设置一个profile()函数
  • stack_size(size = 0):返回新创建线程的栈的大小;或为后续创建的线程设定栈的大小为size


由于Python虚拟机是单线程(GIL)的原因,只有线程在执行I/O密集型的应用时才能更好的发挥Python 的并发性。

计算密集型和I/O密集型任务

  • 计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写。
  • IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少,因此,用运行速度极快的C语言替换用Python这样运行速度极低的脚本语言,完全无法提升运行效率。对于IO密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差。

同步原语
一般在多线程代码中,总会有一些特定的函数或代码块不希望(或不应该)被多个线程同时执行,通常包括修改数据库、更新文件或其他会产生竞态条件的类似情况。当任意数量的线程可以访问临界区的代码,但是在给定的时刻只有一个线程可以通过时,就是使用同步的时候了。程序员选择适合的同步原语或线程控制机制来执行同步。两种基本的同步原语:锁/互斥 和 信号量。锁是最简单最低级的机制,信号量用于多线程竞争有限资源的情况。

锁: 锁有两种状态,锁定和未锁定。而且它也只支持两个函数,获得锁和释放锁。当多线程争夺锁时,允许第一个获得锁的线程进入临界区并执行代码。所有之后到达的线程将会被阻塞,直到第一个线程将锁释放。此时进行第二轮锁的争夺,胜出线程的选择是不确定的,而且还会根据Python 实现的不同而有所区别。锁的使用方法:
首先实例化一个锁对象: lock = Lock()    争夺锁的函数是 lock.acquire(),当没有争到锁的时候,线程会阻塞在这一步。当获得锁之后,进行自己想要操作的代码,然后要记得释放锁  lock.release()。      锁的使用比较简单,只需要考虑清楚什么时候要进入临界区,什么时候可以不在临界区操作。

信号量 :信号量是最古老的同步原语之一。它是一个计数器,当资源消耗时递减,当资源释放时递增。我们可以认为信号量代表它们的资源可用或不可用。消耗资源使计数器递减的操作习惯称为P(),释放资源称为V()。 Python中简化了所有的命名,使用和锁的函数一样的名字:acquire和release。信号量比锁更加灵活,因为可以有多个线程,每个线程拥有有限资源的一个实例。

queue模块、线程间通信: 
生产者、消费者模型:在这个场景下,商品或服务的生产者生产商品,然后将其放到类似队列的数据结构中。生产商品的时间是不确定的,同样消费者消费产品的时间也是不确定的。      我们使用queue模块来提供线程间通信的机制,从而让线程之间可以共享数据,具体而言,就是创建一个队列,让生产者线程在其中放入新的商品,而消费者线程消耗这些商品。 

以上模块没有Python3的实例代码,但是其思想很简单,用法只需要在Python3的api里查询对应的模块即可。
由于Python 的GIL 的限制,多线程更适合于I/O 密集型应用(I/O 释放了GIL,可以允许更多的并发),而不是计算密集型应用。对于后一种情况而言,为了实现更好的并行性,我们需要使用多进程,以便让CPU 的其他内核来执行。多进程的主要模块有:subprocess 、 multiprocessing、concurrent.futures 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值