Python核心编程笔记————多线程编程(一)

简介

. 多线程编程的目的是提高整个任务的性能。其对具有如下特点的编程任务是非常理想的:
  需要多个并发活动;
  每个活动的处理顺序是不确定的(随机的)。
  使用多线程来规划合适的编程任务可以降低程序的复杂性,使其更加清晰、高效、简介。

线程和进程

进程

. 进程是一个执行中的程序,每个进程都拥有自己的地址空间、内存、数据栈以及其他用于跟踪执行的辅助数据。操作系统管理其上的所有进程,为这些进程分配时间。进程也可以通过“派生”(fork或spawn)新的进程来执行其他任务。新的进程也就拥有了自己的内存和数据栈等,进程之间的通信只能使用**进程间通信(IPC)**的方式共享信息。

线程

. 线程与进程类似,不过它们是在同一个进程下执行的,并共享相同的上下文(ps:上下文到底是啥意思啊?我自己的理解是线程所属的进程当时的状态,比如进程拥有某个资源,那此刻大家都有使用权,进程把这个资源还了,那此刻大家就都没有使用权)。
  线程包括开始、执行顺序和结束三部分。它有一个指令指针,用于记录当前的上下文。当其他线程运行时,它可以被抢占(中断)和临时挂起(睡眠)——这种做法叫做让步
  一个进程中的各线程与主线程共享同一片数据空间,因此相比较于独立的进程而言,线程间的通信更加容易。线程一般是以并发的方式执行的,由于这种并发和数据共享机制,使得多任务的协作成为可能。其实在单核CPU系统中不可能做到真正的并发,所以线程的执行实际上是这样规划的:每个线程运行一小会儿,然后让步给其他的线程并排队等候。在整个进程的执行过程中,每个线程执行自己特定的任务,在必要时与其他线程进行通信。
  当然,这种共享不是没有风险的。如果多个线程访问同一片数据,由于数据的访问顺序不一样,可能导致结果不一致。这种情况通常称为竞态条件。幸运的是,大多数线程库都有一些同步原语,以允许线程管理器控制执行和访问。
  另一个需要注意的问题是,线程无法给予公平的执行时间。这是因为一些函数会在完成前保持阻塞状态,如果没有专门为多线程情况进行修改,会导致 CPU 的时间分配向这些贪婪的函数倾斜。

线程和Python

全局解释器锁

. Python 代码的执行是由 Python 虚拟机进行控制的。Python 在设计时是这样考虑的,在主循环中同时只能有一个控制线程在执行,就像单核 CPU 系统中的多进程一样。内存中可以有许多程序,但是在任意给定时刻只能有一个程序在运行。同理,尽管 Python 解释器中可以运行多个线程,但是在任意给定时刻只有一个线程会被解释器执行。
  对 Python 虚拟机的访问是由全局解释器锁(GIL)控制的。这个锁保证了同时只有一个线程在运行。在多线程的环境中,Python虚拟机执行方式如下所示:
  1.设置GIL;
  2.切换到一个线程去运行;
  3.执行下面的操作之一:
    a.指定数量的字节码指令;
    b.线程主动让出控制权(可以调用 time.sleep(0)来完成);
  4.把线程设置回睡眠状态(切换出线程);
  5.解锁 GIL;
  6.重复上述步骤。
  对于Python代码来说,任意面向I/O的Python例程(调用了内置的操作系统 C 代码的那种),GIL会在I/O调用前被释放,以允许其他线程在 I/O 执行的时候运行。因此I/O 密集型的 Python 程序要比计算密集型的代码能够更好地利用多线程环境。

退出线程

. 当一个线程完成函数的执行时,就会退出,也可以通过调用thread.exit()之类的退出函数,或者sys.exit()之类的退出Python进程的标准方法,亦或是抛出SystemExit异常来使线程退出。

在Python中使用线程

. Python支持多线程编程,但是程序是否正常运行还是要取决于运行的操作系统,另外,编写代码时还需要知道所使用的解释器是否支持线程,可以尝试从交互式解释器中导入thread模块,没有报错说明可用,如果报错,可能需要重新编译Python解释器才能够使用线程(ps:自带的IDLE居然报错了)。一般可以在调用configure 脚本的时候使用–with-thread 选项。查阅你所使用的发行版本的 README 文件,来获取如何在你的系统中编译线程支持的 Python 的指定指令。

Python的threading模块

. Python 提供了多个模块来支持多线程编程,包括 thread、threading 和 Queue 模块等。程序可以使用thread和threading模块来创建和管理线程。thread模块提供了基本的线程和锁定支持;而threading模块提供更高级别、功能更全面的线程管理。使用Queue模块,可以创建一个队列数据结构,用于在多线程之间进行共享。
  值得注意的是书中给了一个建议:尽量避免使用thread模块(在Python中被重命名为_thread),因为threading模块中有更先进、更好的线程支持,并且thread模块中的一些属性和threading模块中的会有冲突;另一个原因是thread模块拥有的同步原语(事件)很少,而threading中有很多;还有一个原因是thread对进程何时退出没有控制。当主线程结束时,所有其他线程也都强制结束,不会发出警告或是做适当的处理,至少threading模块能确保重要的子线程在进程退出前结束

thread模块

. 除了派生线程外,thread模块还提供了基本的同步数据结构,成为锁对象(lock object,也叫原语锁、简单锁、互斥锁、互斥和二进制信号量),下表列出了一些常用的线程函数和锁对象的方法:

函数/方法 描述
thread模块的函数
start_new_thread(function,args,kwargs = None) 派生一个新线程,使用给定的args和可选的kwargs来执行function
allocate_lock() 分配LockType对象
exit() 给线程退出指令
LockType锁对象的方法
acquire(wait = None) 尝试获取锁对象
locked() 如果获取了锁对象则返回True,否则返回Flase
release() 释放锁

. 以下是使用thread模块的一个简单多线程示例:

import _thread
from time import sleep,ctime

def loop0():
    print('start loop0 at:',ctime())
    sleep(4)
    print('end loop0 at:',ctime())

def loop1():
    print('start loop1 at:', ctime())
    sleep(2)
    print('end loop1 at:', ctime())

def main():
    print('start at:',ctime())
    _thread.start_new_thread(loop0,())		#loop0即使没有参数,也要传递空元祖
    _thread.start_new_thread(loop1,())
    sleep(6)
    print('all done at:',ctime())

if __name__ == '__main__':
    main()

//运行结果如下:
start at: Thu Jan 24 11:50:55 2019
start loop0 at: Thu Jan 24 11:50:55 2019
start loop1 at: Thu Jan 24 11:50:55 2019
end loop1 at: Thu Jan 24 11:50:57 2019
end loop0 at: Thu Jan 24 11:50:59 2019
all done at: Thu Jan 24 11:51:01 2019

. 并发情况下两个函数执行总共花的时间只是4秒而不是6秒,主线程的sleep是为了防止两个线程在主线程退出的时候被强行终止,但是这种方式不适合用在代码中,因为无法确定主线程的时间是否比其他线程慢,这太不可靠了,因此的作用就显现出来了。下面是使用锁来完成的代码:

import _thread
from time import sleep,ctime

loops = [4,2]

def loop(nloop,nsec,lock):
    print('start loop',nloop,'at:',ctime())
    sleep(nsec)
    print('end loop', nloop, 'at:', ctime())
    lock.release()

def main():
    print('start at:',ctime())
    locks = []
    nloops = range(len(loops))
    for i in nloops:
        lock = _thread.allocate_lock()
        lock.acquire()
        locks.append(lock)
    for i in nloops:
        _thread.start_new_thread(loop,(i,loops[i],locks[i]))
    for i in nloops:
        while locks[i].locked():
            pass
    print('all done at:',ctime())

if __name__ == '__main__':
    main()

//执行结果如下
start at: Thu Jan 24 14:06:53 2019
start loop 1 at: Thu Jan 24 14:06:53 2019
start loop 0 at: Thu Jan 24 14:06:53 2019
end loop 1 at: Thu Jan 24 14:06:55 2019
end loop 0 at: Thu Jan 24 14:06:57 2019
all done at: Thu Jan 24 14:06:57 2019

. 在main函数中有一个检查每个锁对象状态的循环,一旦所有锁对象都释放,主线程就能尽快的执行后面的语句。

threading模块

. 现在介绍更高级别的threading模块,下表是模块中可用对象的列表:

对象 描述
Thread 表示一个执行线程的对象
Lock 锁原语对象(和thread中的一样)
RLock 可重入 锁对象,使单一线程可以(再次)获得已持有的锁(递归锁)
Condition 条件变量对象,使得一个线程等待另一个线程满足特定的条件,比如改变状态或某个数据值
Event 条件变量的通用版本,任意数量的线程等待某个事件的发生,在该事件发生后所有线程将被激活
Semaphore 为线程间共享的有限资源提供了一个“计数器”,如果没有可用资源时会被阻塞
BoundedSemaphore 与 Semaphore 相似,不过它不允许超过初始值
Timer 与 Thread 相似,不过它要在运行前等待一段时间
Barrier 创建一个“障碍”,必须达到指定数量的线程后才可以继续

. threading模块支持守护线程,这也是避免使用tread模块的原因,守护线程的工作方式是:其一般是一个等待客户端请求服务的服务器,如果没有客户端请求,守护线程就是空闲的。如果将一个线程设置为守护线程,就表示这个线程是不重要的,进程退出的时候不需要等待这个线程执行完成。
  要将一个线程设置为守护线程,需要在启动线程之前执行如下的赋值语句:thread.daemon = True。

Thread类

. Thread类是模块中主要的执行对象,下表给出了它的属性和方法:

属性 描述
Thread对象数据属性
name 线程名
ident 线程的标识符
daemon 表示这个线程是否是守护线程
Thread对象方法
_init_(group=None, tatget=None, name=None, args=(),kwargs ={}, verbose=None, daemon=None) 实例化一个线程对象,需要有一个可调用的 target,以及其参数 args或 kwargs。还可以传递 name 或 group 参数,不过后者还未实现。此外,verbose 标志也是可接受的。而 daemon 的值将会设定thread.daemon 属性/标志
start() 开始执行该线程
run() 定义线程功能的方法(通常在子类中被应用开发者重写)
join (timeout=None) 直至启动的线程终止之前一直挂起;除非给出了 timeout(秒),否则会一直阻塞
getName() 已弃用 返回线程名
setName (name) 已弃用 设定线程名
isAlivel /is_alive () 布尔标志,表示这个线程是否还存活
isDaemon() 如果是守护线程,则返回 True;否则,返回 False
setDaemon(daemonic) 已弃用 把线程的守护标志设定为布尔值 daemonic(必须在线程 start()之前调用)

. 使用Thread类创建线程的方式有很多,以下介绍三种较为相似的:

创建Thread实例,传给它一个函数

. 将Thread实例化,然后将函数及其参数传递进去,当线程开始执行时,函数也开始执行:

import threading
from time import sleep,ctime

loops = [4,2]

def loop(nloop,nsec):
    print('start loop',nloop,'at:',ctime())
    sleep(nsec)
    print('end loop', nloop, 'at:', ctime())

def main():
    print('start at:',ctime())
    threads = []
    nloops = range(len(loops))
    for i in nloops:
        t = threading.Thread(target=loop,args=(i,loops[i]))
        threads.append(t)

    for i in nloops:
        threads[i].start()
    for i in nloops:
       threads[i].join()
    print('all done at:',ctime())

if __name__ == '__main__':
    main()

. 和之前的版本不同的地方在于:线程并不是一被创建就开始执行,而是调用start()方法之后才会开始执行;另外一点就是相比管理一组锁,这里只使用了join方法,join方法将等待线程结束或是等待至超时时间。
  另一个使用join的重点是其实它根本不用被调用,一旦线程启动,它们就会一直执行,知道给定的函数完成后退出(关于这点,可以比较一下使用_thread模块的那个版本不用无限循环等待锁释放,和这个版本不写join部分的代码,执行结果的不同)。

创建 Thread 的实例,传给它一个可调用的类实例

. 不是传入函数而是传入一个可调用的类的实例,用于线程执行——这种方法更加接近面向对象的多线程编程。这种调用有更好的灵活性:

import threading
from time import sleep,ctime

loops = [4,2]

class ThreadFunc(object):

    def __init__(self,func,args,name=''):
        self.func = func
        self.args = args
        self.name = name

    def __call__(self):
        self.func(*self.args)

def loop(nloop,nsec):
    print('start loop',nloop,'at:',ctime())
    sleep(nsec)
    print('end loop', nloop, 'at:', ctime())

def main():
    print('start at:',ctime())
    threads = []
    nloops = range(len(loops))
    for i in nloops:
        t = threading.Thread(target=ThreadFunc(loop,(i,loops[i]),loop.__name__))
        threads.append(t)

    for i in nloops:
        threads[i].start()
    for i in nloops:
       threads[i].join()
    print('all done at:',ctime())

if __name__ == '__main__':
    main()

. 这次主要是添加了TreadFunc类,值得注意的是,这个类使得代码变得更加通用灵活,比如让这个类保存函数的参数、函数自身和函数名。构造函数__init__()用于设定上面这些值。
  当创建新的线程的时候,Thread类的代码将调用ThreadFunc对象,此时会调用__call__()这个特殊方法。

派生 Thread 的子类,并创建子类的实例

. 当创建线程时使用子类相对容易阅读,并且使我们在定制线程对象时拥有更多的灵活性,也能够简化线程创建的调用过程:

import threading
from time import sleep,ctime

loops = [4,2]

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)

def loop(nloop,nsec):
    print('start loop',nloop,'at:',ctime())
    sleep(nsec)
    print('end loop', nloop, 'at:', ctime())

def main():
    print('start at:',ctime())
    threads = []
    nloops = range(len(loops))
    for i in nloops:
        t = MyThread(loop,(i,loops[i]),loop.__name__)
        threads.append(t)

    for i in nloops:
        threads[i].start()
    for i in nloops:
       threads[i].join()
    print('all done at:',ctime())

if __name__ == '__main__':
    main()

. 与上个版本最主要的变化是:1.MyThread子类的构造函数必须先调用基类的构造函数;2.之前的特殊方法__call__()在这里必须为run(),这个方法是重写的,标准run()方法调用了传递给对象的构造函数的可调对象作为目标参数,如果有这样的参数的话,顺序和关键字参数分别从args和kargs取得。

Threading模块的其它函数

函数 描述
active_count() 当前活动的 Thread 对象个数
current_thread 返回当前的 Thread 对象
enumerate() 返回当前活动的 Thread 对象列表
settrace (func) 为所有线程设置一个 trace 函数
setprofile (func) 为所有线程设置一个 profile 函数
stack_size (size=0) 返回新创建线程的栈大小;或为后续创建的线程设定栈的大小为 size
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章