Python - 多线程(thread和threading模块的使用)

关于线程、多线程、线程与进程的概念就不在赘述了。只记录一下python中多线程以及thread、threading模块的使用的使用。

1、线程和Python

在pyhon中使用线程,需要先了解一下python的全局解释器锁。

Python代码的执行是通过Python虚拟机(又名解释器主循环)进行控制的。在主循环中同时只能有一个控制线程在执行,就像单核CPU系统中的多进程一样。内存中可以有许多程序,但是在任意给定时刻只能有一个程序在运行。同理,在Python解释器中,尽管可以运行多个线程,但是在任意给定时刻只有一个线程会被解释器执行

对Python虚拟机的访问是有全局解释器锁(GIL)控制的。这个锁就是用来保证同时只能有一个线程运行。在多线程环境中,Python虚拟机将按照下面所属方式运行:1)、设置GIL;2)、切换一个线程去运行;3)、执行下面操作之一:指定数量的字节码指令\线程主动让出控制权(可调用time.sleep(0)来完成);4)、把线程设置回睡眠状态(切换出线程);5)、解锁GIL;6)、重复上述步骤。

所以由于GIL的限制,Python中的多线程更适合于I/O密集型应用(I/O释放了GIL,可以允许更多的并发),而不是计算密集型应用

2、thread模块

thread模块在Python3中重命名为_thread。thread模块除了派生线程外,还提供了基本的同步数据结构,称为锁对象(lock object,也叫原语锁、简单锁、互斥锁等)

2.1、thread模块的简单使用

# 使用thread模块
import _thread      #python3.0重命名为_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, ())
    _thread.start_new_thread(loop1, ())

    sleep(6)        # 如果没有此语句,则main函数直接执行结束,loop0和loop1线程也就直接结束了

    print('all DONE at: ', ctime())

if __name__=='__main__':
    main()


   在上述代码中,有一sleep(6)语句,该语句是用来是线程同步的, 如果没有此语句,则main函数继续执行下一条语句,直至执行结束,loop0和loop1线程也就直接结束了。但是,使用sleep()进行线程同步是不可靠的: 如果循环有独立且不同的执行时间怎么办?我们可能过早或过晚退出主线程,这就是引出锁的原因
    通过使用锁,我们可以在所有线程全部完成执行后立即退出。

2.2、thread模块使用锁

# 使用thread模块
import _thread      # python3.0重命名为_thread
from time import sleep, ctime

loops = [4, 2]
def loop(nloop, nsec, lock):
    print('start', nloop, ' at:', ctime())
    sleep(nsec)
    print('end', 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()

 在上述代码中使用了锁(lock)。代码并没有把sleep(4)和sleep(2)硬编码到不同的函数中,而是使用了唯一的loop()函数,通过传递不同的参数执行不同的sleep()。

程序执行时,先统一的创建锁对象,因为锁对象的创建与获取(上锁)是需要一定时间的,之所以不在上锁的时候启动线程,有两个原因:一是同步线程,就是想要让所有的线程能够同时启动;二是获取锁需要时间,如果线程执行的太快,有可能会出现获取锁之前线程就执行结束的情况。

最后一个for循环,则是在进行等待,等待所有的线程的锁都被释放之后才会继续执行。

3、threading模块

threading模块更高级,除了Thread类以外,该模块还包含了非常好用的同步机制。

守护线程:守护线程一般是一个等待客户端请求服务的服务器,如果没有客户端请求,守护线程就是空闲的,如果把一个线程设置为守护线程,就表示这个线程是不重要的,进程退出时不需要等待这个线程执行完成。

threading模块的Thread类是主要的执行对象。使用Thread类可以有多种方法创建线程,常用的有一下三种:

1)、创建Thread的实例,传给它一个函数;

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

3)、派生Thread的子类,并创建子类的实例;

其中1)和3)是最常用的。

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

# 使用 threading 模块
import threading
from time import sleep, ctime

loops = [4, 2]
def loop(nloop, nsec):
    print('start', nloop, ' at:', ctime())
    sleep(nsec)
    print('end', 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:                # wait for all threads to finish
        threads[i].join()           # 直至启动的线程结束前一直挂起,除非给出参数,否则会一直阻塞

    print('all DONE at: ', ctime())

if __name__=='__main__':
    main()

上面代码中,实例化了一个Thread的 的对象来创建线程,实例化Thread和滴啊用thread.start_new_tgread()的最大区别就是新线程不会立即开始执行。这是一个非常有用的同步功能,尤其是不希望线程立即开始执行时。

当所有线程分配完成之后,通过调用每个线程的start()方法开始让它们执行。相比于管理一组锁(分配、获取、释放、检查锁状态)而言,这里只需要为每个线程调用join()方法即可。join()方法将等待线程结束,或者在提供了超时时间的情况下,达到超时时间,然后退出。使用join()方法要比等待所释放的无限循环更加清晰(这也是这种锁又称为自旋锁的原因)。

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

# 使用 threading 模块
import threading
from time import sleep, ctime

loops = [4, 2]
class MyThread(threading.Thread):        # Thread派生类
    def __init__(self, func, args, name=''):
        threading.Thread.__init__(self)

        self.name = name
        self.func = func
        self.args = args

    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:                # wait for all threads to finish
        threads[i].join()           # 直至启动的线程结束前一直挂起,除非给出参数,否则会一直阻塞

    print('all DONE at: ', ctime())


if __name__=='__main__':
    main()

上述代码中,MyThread派生了threading.Thread类,并重写了run()函数。

3.3、without lock

当使用threading模块创建线程而没有使用锁时,可能会出现当多个线程访问 同一个变量时数据混乱的问题。如下代码:

from atexit import register
from random import randrange
from threading import Thread, current_thread
from time import sleep, ctime

class CleanOutputSet(set):
    def __str__(self):
        return ','.join(x for x in self)

loops = (randrange(2, 5) for x in range(randrange(3, 7)))   
remaining = CleanOutputSet()

def loop(nsec):
    myname = current_thread().name
    remaining.add(myname)

    print('[%s] Startted %s' % (ctime(), myname))

    sleep(nsec)

    remaining.remove(myname)

    print('[%s] Completed %s (%d secs)' % (ctime(), myname, nsec))

    print('   remaining:%s' % (remaining or 'NONE'))

def _main():
    for pause in loops:
        Thread(target=loop, args=(pause,)).start()

@register
def _atexit():
    print('all DONE at: ', ctime())

if __name__=='__main__':
    _main()

在上述代码中,

loops = (randrange(2, 5) for x in range(randrange(3, 7)))   语句的意思是: 生成3-6个(个数由randrange(3,7)控制)随机数,随机数的值为2-4(由randrange(2,5)控制);

_main():_main()函数是一个特殊的函数,只有这个模块从命令行直接运行时才会执行该函数(不能被其他模块导入);

_atexit(): 这个函数(装饰器的方式)会在python解释器中注册一个退出函数,也就是说,它会在脚本退出之前请求调用这个特殊函数;

CleanOutputSet:派生自set类,重写了__str__函数,可以将默认输出改变为将所有元素按照逗号分隔的字符串;

输出可能会混乱;多个线程同时访问一个变量(剩余线程名集合remaining),可能会出现以下输出结果:

3.4、with lock

引入线程锁:

from atexit import register
from random import randrange
from threading import Thread, Lock, current_thread
from time import sleep, ctime

class CleanOutputSet(set):
    def __str__(self):      # 重写了输出函数
        return ','.join(x for x in self)

lock = Lock()   # 创建锁
loops = (randrange(2, 5) for x in range(randrange(3, 7))) 
remaining = CleanOutputSet()

def loop(nsec):
    myname = current_thread().name

    lock.acquire()              # 获得锁
    remaining.add(myname)
    print('[%s] Startted %s' % (ctime(), myname))
    lock.release()              # 释放锁

    sleep(nsec)

    lock.acquire()              # 获得锁
    remaining.remove(myname)
    print('[%s] Completed %s (%d secs)' % (ctime(), myname, nsec))
    print('   remaining:%s' % (remaining or 'NONE'))
    lock.release()              # 释放锁

def _main():
    for pause in loops:
        Thread(target=loop, args=(pause,)).start()

@register
def _atexit():
    print('all DONE at: ', ctime())

if __name__=='__main__':
    _main()

引入锁之后当多个线程同时访问同一个变量时,也不会出现输出混乱的情况。

3.5、with lock context

使用lock对象时,还要手动的获取锁与释放锁,这是可以采用lock上下文的方式管理锁(Python2.5以上版本)。使用with语句,此时每一个对象的上下文管理器负责在进入该套件之前调用acquire()并在完成执行后调用realease()。

threading模块的对象Lock、RLock、Condition、Semaphore和BoundedSemaphore都包含上下文管理器,都可以使用with语句,使用with优化后代码如下所示:

def loop(nsec):
    myname = current_thread().name

    '''
    锁的申请和释放交给with上下文管理器
    '''
    with lock:
        remaining.add(myname)
        print('[%s] Startted %s' % (ctime(), myname))

    sleep(nsec)

    with lock:
        remaining.remove(myname)
        print('[%s] Completed %s (%d secs)' % (ctime(), myname, nsec))
        print('   remaining:%s' % (remaining or 'NONE'))

loop()函数更加简洁和美观了。

4、thread和threading模块

推荐使用threading模块。

一、threading模块更加先进,有更好的线程支持,而且thread模块中的一些属性会和threading模块有冲突;

二、thread模块(低级别)拥有的同步原语很少(实际上只有一个),而threading模块的同步原语则有多个;

三、thread模块对线程何时退出没有控制,当主线程结束时,所有线程也都强制结束,不会发出警告或者进行适当的清理,threading模块则能确保重要的子线程在进程退出前结束。(重要的:守护线程标记)。

四、threading模块支持守护进程,其工作方式为:守护线程一般是一个等待客户端请求服务的服务器,如果没有客户端请求,守护线程就是空闲的,如果把一个线程设置为守护线程,就表示这个线程是不重要的,进程退出时不需要等待这个线程执行完成

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值