wing聊“Python进阶知识” - Python多线程那些事

python学习网 2017-09-18 14:32:01

wing聊“Python进阶知识” - 系列文章导航页

1、概述

本文会涉及多线程编程的方方面面知识,后面有新东西也会陆续更新进来

python对多线程的支持
先看一个概念:
GIL(Global Interpreter Lock,全局解释器锁),官方描述如下:
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)

由于GIL的存在,python其实无法利用多处理器的优势,任意时刻只会有一个线程运行在解释器中,也就是大计算量的程序在python中通过多线程处理其实不见得会变快。但是IO密集型程序可以很好地利用多线程,比如用python开发一个rest客户端程序,如果单线程实现,假如发送一个http请求服务器端需要耗费5s来处理,串行发送1000个就需要5000s左右;但是开1000个线程,就可以同时发送1000个请求,一起等待相应,基本10s之内就能完成这个过程。

Python中多线程相关的模块包括:thread,threading,Queue。

  • thread:多线程的底层支持模块,一般不建议使用【本文暂不涉及】
  • threading:对thread进行了封装,将一些线程的操作对象化
  • Queue:实现了多生产者(Producer)、多消费者(Consumer)的队列

2、开始使用多线程

2.1、入门例子

python的threading库可以实现在单独的线程中执行任意的python可调用对象,我们通过创建Thread类的实例,然后提供其需要被单独线程执行的可调用对象,就完成目的了,看一个入门例子:

# -*- coding: utf-8 -*-
import threading
from time import sleep


def show(name):
    sleep(1)
    print('Hello %s' % name)


if __name__ == '__main__':
    t1 = threading.Thread(target=show, args=('wing',))
    t2 = threading.Thread(target=show, args=('WING',))
    t1.start()
    t2.start()
    print('All done!')

输出结果如下

All done!
Hello wing
Hello WING

创建一个线程实例需要传递给它目标函数的引用,参数元组,然后调用start方法就会开始这个线程的运行。

通过Thread类可以有多种方法创建线程:

  • 用一个函数作为参数实例化一个Thread类,多线程执行这个函数(上面例子所示)
  • 用一个可调用类作为参数实例化一个Thread类,多线程执行这个“可调用类”(和第一个本质相同)
  • 从Thread类派生一个子类,重写run方法(比较常规的用法)

2.2、多线程中的join

观察上面输出会发现“All done!”居然在最开始输出了,我们的本意是线程执行完之后才输出“All done!”,咋办呢?看下面一段代码:

# -*- coding: utf-8 -*-
import threading
from time import sleep


def show(name):
    sleep(1)
    print('Hello %s' % name)


if __name__ == '__main__':
    t1 = threading.Thread(target=show, args=('wing',))
    t2 = threading.Thread(target=show, args=('WING',))
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print('All done!')

输出如下:

Hello WING
Hello wing
All done!

这里介绍一下这个join,文档中有如下一句话:
"Wait until the thread terminates"
也就是说要等待这个线程执行完毕才开始后续操作,这样就实现了等待子线程完成再进行其他操作的目的。
该函数定义是def join(self, timeout=None): ...
也就是说还可以设置timeout参数,避免子线程出问题一直不结束的情况下父线程无限等待的问题

2.3、用一个可调用类作为参数实例化一个Thread类

先看下面代码:

# -*- coding: utf-8 -*-
import threading
from time import sleep


def show(name):
    sleep(1)
    print('Hello %s' % name)


class MyThread(object):
    def __init__(self, func, args, name=""):
        self.func = func
        self.args = args
        self.name = name

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


if __name__ == '__main__':
    t1 = threading.Thread(target=MyThread(show, ('wing',)))
    t2 = threading.Thread(target=MyThread(show, ('Wing',)))
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print('All done!')

如上,其实这里很好理解,主要注意的一个知识点是类的“call()”方法,这里的target is a callable object, 是一个可调用对象,MyThread(show, ('wing',))实例化了一个类,得到的对象就是这样一个可调用对象,和函数名对应,真正调用的时候就是执行了__call__()方法,这里的__call__()只是简单地执行初始化时传递过来的函数,类来实现相比于函数要灵活很多。

2.4、从Thread类派生一个子类,重写run方法(推荐的方法)

先看下面代码:

# -*- coding: utf-8 -*-
import threading
from time import sleep


def show(name):
    sleep(1)
    print('Hello %s' % name)


class MyThread(threading.Thread):
    def __init__(self, func, args):
        super(MyThread, self).__init__()
        self.func = func
        self.args = args

    def run(self):
        self.func(self.args)


if __name__ == '__main__':
    t1 = MyThread(show, ('wing',))
    t2 = MyThread(show, ('Wing',))
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print('All done!')

输出结果如下:

Hello wing
Hello Wing
All done!

这里继承了threading模块的Thread类,重写了init和run方法,通过这种方式来实现多线程执行的效果。

3、多线程处理的返回值问题

上面的show函数只是简单的打印操作,但是如果需要多线程处理的函数如下:

def sum(args):
    res = 0
    for i in args:
        res += i
    return res

这个时候需要记录函数执行的结果,在上面的实现中并不能达到这样的效果,这个时候我们可以稍微修改一下MyThread类,如下:

# -*- coding: utf-8 -*-
import threading


def sum(args):
    res = 0
    for i in args:
        res += i
    return res


class MyThread(threading.Thread):
    def __init__(self, func, args):
        super(MyThread, self).__init__()
        self.func = func
        self.args = args
        self.result = None

    def run(self):
        self.result = self.func(self.args)

    def get_result(self):
        return self.result


if __name__ == '__main__':
    t1 = MyThread(sum, (1, 2))
    t2 = MyThread(sum, (11, 22))
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print('1 + 2 = ', t1.get_result())
    print('11 + 22 = ', t2.get_result())
    print('All done!')

输出结果如下:

1 + 2 =  3
11 + 22 =  33
All done!

4、python线程同步机制

先看下面示例

# -*- coding: utf-8 -*-
import threading
from time import sleep

count = 2


def show():
    global count
    if count > 0:
        # sleep(1)
        count -= 1


if __name__ == '__main__':
    threads = list()
    for i in range(3):
        threads.append(threading.Thread(target=show))
    for t in threads:
        t.start()
    for t in threads:
        t.join()
    print(count)

最后的结果是0,准确说多次执行发现结果是0,至于0是不是唯一结果,这里先不下结论,如果我们尝试把sleep(1)这一句注释放开,就会发现结果基本变成了-1
其实这里的sleep表示的只是对count操作之前的过程可能会耗时较长,这个时候count可能已经被改变了,而我们的本意是count大于0时才执行一次操作,本线程做这个处理的时候,不希望其他线程同时操作count的值。
再看下面一段代码:

# -*- coding: utf-8 -*-
import threading
from time import sleep

count = 2
lock = threading.Lock()


def show():
    global count
    # 获取锁对象(如果其它线程已经获得了该锁,则当前线程需等待其被释放)
    lock.acquire()
    # 加上try是为了保证数据处理过程就算发生异常,锁也要被释放
    try:
        if count > 0:
            sleep(1)
            count -= 1
    finally:
        lock.release()


if __name__ == '__main__':
    threads = list()
    for i in range(3):
        threads.append(threading.Thread(target=show))
    for t in threads:
        t.start()
    for t in threads:
        t.join()
    print(count)

如上,通过加锁实现了线程同步,这里的锁释放还可以用更优雅的方式实现,如下:

    with lock:
        if count > 0:
            count -= 1

---未完待续---

阅读(808) 评论(0)