目录

  • 十一、Python中线程
  • 11.1 线程的定义
  • 11.2 多线程
  • 11.3 线程池


十一、Python中线程

11.1 线程的定义

线程(Thread):一个进程还可以拥有多个并发的执行线索,简单的说就是拥有多个可以获得CPU调度的执行单元,这就是所谓的线程。由于线程在同一个进程下,它们可以共享相同的上下文,因此相对于进程而言,线程间的信息共享和通信更加容易。

11.2 多线程

在Python早期的版本中就引入了thread模块(现在名为_thread)来实现多线程编程,然而该模块过于底层,而且很多功能都没有提供,因此目前的多线程开发我们推荐使用threading模块,该模块对多线程编程提供了更好的面向对象的封装。

函数原型:threading.Thread(target=function, name=“Thread”, args=参数)

注意: 设定执行的参数,设定格式一定要Tuple(变量,)

进程Thread类的常用方法如下 -

方法

说明

run

子线程执行的目标方法

start

创建子线程的实例对象,并执行run方法

join

主线程阻塞等待子线程结束,可以设置截至时间 time

isAlive

判断子线程是否存在

getName

获取线程名

setName

设置线程名

setDaemon

子线程是否随主线程退出而终止

我们依旧通过下载文件的例子用多线程的方式来实现一遍。

from threading import Thread
from os import getpid
from time import time, sleep


def download_task(filename, downtime):
    """
    资源下载

    @param filename 资源名
    @param downtime 下载耗时
    """
    print('启动下载进程,进程号[%d].' % getpid())
    print('开始下载%s...' % filename)
    sleep(downtime)
    print('%s下载完成! 耗费了%d秒' % (filename, downtime))


def main():
    start = time()
    t1 = Thread(target=download_task, args=('Python从入门到精通.pdf', 5, ))
    t1.start()
    t2 = Thread(target=download_task, args=('Python的编程核心.pdf', 8, ))
    t2.start()
    t1.join()
    t2.join()
    end = time()
    print('总共耗费了%.2f秒.' % (end - start))


if __name__ == '__main__':
    main()

上面代码的执行结果 -

启动下载进程,进程号[3848].
开始下载Python从入门到精通.pdf...
启动下载进程,进程号[3848].
开始下载Python的编程核心.pdf...
Python从入门到精通.pdf下载完成! 耗费了5秒
Python的编程核心.pdf下载完成! 耗费了8秒
总共耗费了8.01秒.

**说明:**这里可以看到同一个进程,线程下载速度优先于进程。

前面我们学习了继承,这列我们通过自定义类来继承Thread类,然后再创建线程对象并启动线程。代码如下所示。

from threading import Thread
from time import time, sleep


class DownloadTask(Thread):

    def __init__(self, filename, downtime):
        super().__init__()
        self._filename = filename
        self._downtime = downtime

    def run(self):
        print('开始下载%s...' % self._filename)
        sleep(self._downtime)
        print('%s下载完成! 耗费了%d秒' % (self._filename, self._downtime))


def main():
    start = time()
    t1 = DownloadTask('Python从入门到精通.pdf', 5)
    t1.start()
    t2 = DownloadTask('Python的编程核心.pdf', 8)
    t2.start()
    t1.join()
    t2.join()
    end = time()
    print('总共耗费了%.2f秒.' % (end - start))


if __name__ == '__main__':
    main()

上面代码的执行结果 -

开始下载Python从入门到精通.pdf...
开始下载Python的编程核心.pdf...
Python从入门到精通.pdf下载完成! 耗费了5秒
Python的编程核心.pdf下载完成! 耗费了8秒
总共耗费了8.01秒.

因为线程之间可以共享进程的内存空间,因此要实现多个线程间的通信相对简单,最直接的办法就是设置一个全局变量,多个线程共享这个全局变量即可。但是当多个线程共享同一个变量的时候,很有可能产生不可控的结果从而导致程序失效甚至崩溃。举个例子50个线程分别向账户中转入1元钱,代码如下,代码中有坑哈,大家可以试试。

from time import sleep
from threading import Thread


class Account(object):
    """
    账户管理
    """

    def __init__(self):
        self._balance = 0

    def deposit(self, money):
        # 计算存款后的余额
        new_balance = self._balance + money
        # 模拟受理存款业务需要0.02秒的时间
        sleep(0.02)
        # 修改账户余额
        self._balance = new_balance

    @property
    def balance(self):
        return self._balance


class AddMoneyThread(Thread):
    """
    存钱的线程
    """

    def __init__(self, account, money):
        super().__init__()
        self._account = account
        self._money = money

    def run(self):
        self._account.deposit(self._money)


def main():
    account = Account()
    threads = []
    # 创建100个存款的线程向同一个账户中存钱
    for _ in range(50):
        t = AddMoneyThread(account, 1)
        threads.append(t)
        t.start()
    # 等所有存款的线程都执行完毕
    for t in threads:
        t.join()
    print('账户余额为: ¥%d元' % account.balance)


if __name__ == '__main__':
    main()

上面脚本的执行结果 -

账户余额为: ¥1元

运行上面的程序,结果是不是让人大跌眼镜,为什么会出现这种情况是因为我们没有对银行账户这个“临界资源”加以保护,多个线程同时向账户中存钱时,会一起执行到new_balance = self._balance + money这行代码,多个线程得到的账户余额都是初始状态下的0,所以都是0上面做了+1的操作,因此得到了错误的结果。
解决办法:加“锁”,通过“锁”来保护“临界资源”,只有获得“锁”的线程才能访问“临界资源”,而其他没有得到“锁”的线程只能被阻塞起来,直到获得“锁”的线程释放了“锁”,其他线程才有机会获得“锁”,进而访问被保护的“临界资源”。下面的代码演示了如何使用“锁”来保护对银行账户的操作,从而获得正确的结果。

from time import sleep
from threading import Thread, Lock


class Account(object):
    """
    账户管理
    """

    def __init__(self):
        self._balance = 0
        self._lock = Lock()

    def deposit(self, money):
        # 先获取锁才能执行后续的代码
        self._lock.acquire()
        try:
            new_balance = self._balance + money
            sleep(0.01)
            self._balance = new_balance
        finally:
            # 在finally中执行释放锁的操作保证正常异常锁都能释放
            self._lock.release()

    @property
    def balance(self):
        return self._balance


class AddMoneyThread(Thread):
    """
    存钱的线程
    """
    def __init__(self, account, money):
        super().__init__()
        self._account = account
        self._money = money

    def run(self):
        self._account.deposit(self._money)


def main():
    account = Account()
    threads = []
    for _ in range(50):
        t = AddMoneyThread(account, 1)
        threads.append(t)
        t.start()
    for t in threads:
        t.join()
    print('账户余额为: ¥%d元' % account.balance)


if __name__ == '__main__':
    main()

11.3 线程池

ThreadPoolExecutor线程池:可以指定线程的数量,供用户调用,便于管理线程池。主要用于执行的目标很多,而手动限制线程数量又太繁琐的情况

方法

说明

submit

函数格式:submit(fn, *args, **kwargs):将 fn 函数提交给线程池。*args 代表传给 fn 函数的参数,*kwargs 代表以关键字参数的形式为 fn 函数传入参数。

map

函数格式:map(func, *iterables, timeout=None, chunksize=1):该函数类似于全局函数 map(func, *iterables),只是该函数将会启动多个线程,以异步方式立即对 iterables 执行 map 处理。

shutdown

函数格式:shutdown(wait=True):关闭线程池。加了pool.shutdown(),则会等待所有线程都运行结束后,再运行后面语句。

其中submit 方法会返回一个 Future 对象,Future 类主要用于获取线程任务函数的返回值。

方法

说明

cancel

取消该 Future 代表的线程任务。如果该任务正在执行,不可取消,则该方法返回 False;否则,程序会取消该任务,并返回 True。

cancelled

返回 Future 代表的线程任务是否被成功取消。

running

如果该 Future 代表的线程任务正在执行、不可被取消,该方法返回 True。

done

如果该 Funture 代表的线程任务被成功取消或执行完成,则该方法返回 True。

result

获取该 Future 代表的线程任务最后返回的结果。如果 Future 代表的线程任务还未完成,该方法将会阻塞当前线程,其中 timeout 参数指定最多阻塞多少秒。

exception

获取该 Future 代表的线程任务所引发的异常。如果该任务成功完成,没有异常,则该方法返回 None。

add_done_callback

为该 Future 代表的线程任务注册一个“回调函数”,当该任务成功完成时,程序会自动触发该 fn 函数。

from concurrent.futures import ThreadPoolExecutor
from time import time, sleep


def download_task(filename, downtime):
    print ('开始下载%s...' % filename)
    sleep(downtime)
    return '%s下载完成! 耗费了%d秒' % (filename, downtime)


def get_result(future):
    print(future.result())


def main():
    start = time()
    with ThreadPoolExecutor(max_workers=2) as pool:
        future1 = pool.submit(download_task, *('Python从入门到精通.pdf', 5))
        future2 = pool.submit(download_task, *('Python的编程核心.pdf', 8))
        # 为future1添加线程完成的回调函数
        future1.add_done_callback(get_result)
        # 为future2添加线程完成的回调函数
        future2.add_done_callback(get_result)
    end = time()
    print('总共耗费了%.2f秒.' % (end - start))


if __name__ == '__main__':
    main()

上面代码执行结果 -

开始下载Python从入门到精通.pdf...
开始下载Python的编程核心.pdf...
Python从入门到精通.pdf下载完成! 耗费了5秒
Python的编程核心.pdf下载完成! 耗费了8秒
总共耗费了8.00秒.