本文首发于公众号:Hunter后端

原文链接:Python笔记二之多线程

这一篇笔记介绍一下在 Python 中使用多线程。

注意:以下的操作都是在 Python 3.8 版本中试验,不同版本可能有不同之处,需要注意。

本篇笔记目录如下:

  1. 概念
  2. 多线程的使用示例

    daemon

    run()
  3. 线程对象的属性和设置
  4. 线程模块相关函数
    1. threading.active_count()
    2. threading.current_thread()
    3. threading.enumerate()
  5. 线程的异常和函数结果获取
  6. 线程池
    1. result()
    2. done()
    3. exception()
    4. cancel()
    5. running()
  7. 如何探索出最佳的线程池线程数量

1、概念

关于进程与线程的概念,这里简单介绍下。

一个进程是一个独立的执行环境,包括代码、数据和系统资源等,每个进程都有自己的内存空间、文件描述符、环境变量等。

而线程存在于进程中,共享进程内的内存和资源。

至于多进程与多线程,多进程可以充分利用计算机的多核 CPU,适用于 CPU 密集型的任务,,比如进行大量计算操作

而多线程则适用于涉及到大量的 IO 操作的任务,比如网络请求,文件读写等,在 Python 中有一个 GIL 的概念,它的全称是 Global Interpreter Lock,为全局解释器锁。

GIL 的存在是为了使同一时刻只有一个线程在运行 Python 代码,保护解释器的内部数据避免收到并发访问的影响。

所以 Python 中的多线程操作实际上是在多个线程中进行切换,以此来实现想要的并发效果。

2、多线程的使用示例

前面介绍了 Python 中多线程的操作适用于 IO 密集型的任务,所以这里以访问某个接口为例介绍一下多线程的使用。

那个接口我们这里用 Flask 创建一个服务器,其内容如下:

# app/__init__.py

from flask import Flask
import time def create_app():
app = Flask(__name__) @app.route("/test/<int:delay>")
def test(delay):
time.sleep(delay)
return str(time.time()) return app

这个接口通过 delay 参数可以指定接口的休眠时间返回,比如 /test/4,那么接口响应时间大约会是 4 秒。

在 Python 中,用到多线程的模块是 threading 模块,以下是一个使用示例:

import threading
import time import requests def get_response(url):
response = requests.get(url)
print(response.content) def test_multi_threading():
url = "http://192.168.1.6:5000/test/2"
threads = [] for i in range(20):
threads.append(threading.Thread(target=get_response, args=(url,))) for t in threads:
t.start() for t in threads:
t.join() def test_single():
url = "http://192.168.1.6:5000/test/2" for i in range(5):
get_response(url) if __name__ == "__main__":
start_time = time.time()
test_multi_threading()
print("运行耗时:", time.time() - start_time) start_time = time.time()
test_single()
print("运行耗时:", time.time() - start_time)

在这里我们可以比对单个线程执行五次,需要的时间大约是 10 秒,而使用多线程的方式虽然调用了 20 次接口,但是耗时大约只有 2 秒,这就是多线程在 IO 密集型的情况下的好处。

接下来具体介绍下多线程的使用方法:

def test_multi_threading():
url = "http://192.168.1.6:5000/test/2"
threads = [] for i in range(20):
threads.append(threading.Thread(target=get_response, args=(url,))) for t in threads:
t.start() for t in threads:
t.join()

在这里,我们通过 threading.Thread() 的方式创建一个线程,然后通过 .start() 方法开始线程活动。

接着通过 join() 方法阻塞调用这个方法的线程,在这里也就是主线程,等待 t 线程完成后再执行主线程后面的操作。

如果我们尝试注释掉 t.join() 这两行,那么主线程就会不等待 t 线程直接往后面执行,造成我们后面在主函数里计算的时间不准确。

daemon

可以根据这个参数设置线程是否为守护线程,所有线程创建的时候默认都不是守护线程,如果需要设置线程为守护线程,需要额外做设置。

守护线程是一种特殊类型的线程,生命周期受到主线程的影响,也就是说当主线程结束时,守护线程会被强制终止,它不会阻止主线程的正常执行,主线程也不会像其他线程调用了 join() 一样被阻塞。

守护线程通常用于执行一些辅助性任务,比如日志记录、定时任务等,示例如下,我们开启了一个守护线程用于定时 print() 某些信息:

def print_info():
while True:
print("daemon threading, curr_time:", time.time())
time.sleep(1) def test_daemon_threading():
base_url = "http://192.168.1.6:5000/test/" t1 = threading.Thread(target=get_response, args=(base_url + str(6),))
t2 = threading.Thread(target=get_response, args=(base_url + str(2),)) daemon_t = threading.Thread(target=print_info, args=(), daemon=True) t1.start()
t2.start()
daemon_t.start() t1.join()
t2.join()

这样,守护线程 daemon_t 就会在后台一直循环打印信息,直到主线程结束,守护线程也会被强制终止。

run()

run()start() 方法都和线程的执行有关。

start() 用于启动线程,线程变量调用 start() 后,比如前面的 t.start(),会立即开始执行线程,且线程的执行与主线程并行进行。

run() 定义的是线程内的执行逻辑,是线程的入口点,表示的是线程活动的方法,线程开启后就会调用 run() 方法,执行线程的任务。

在执行 start() 方法后,线程会自动调用 run() 方法,以此来执行线程内需要调用的函数,我们可以通过重写 run() 方法来实现我们想要的定制化功能,比如在后面我们就是通过重写 run() 方法来实现线程的异常信息以及函数的结果返回的,

3、线程对象的属性和设置

线程本身有一些属性可以用于设置和获取,我们先创建一条线程:

t1 = threading.Thread(target=get_response, args=(base_url + str(6),))

查看线程名称

线程名称只是用于标记线程的,并无实际意义,根据用户设置而定,比如前面创建了线程,默认名为 Thread-1,我们可以通过下面的两个操作获取,两个操作是等效的:

t1.name
t1.getName()

设置线程名称

设置线程名称的方法如下:

t1.setName("test_thread")

判断线程是否存活

在未进行 start() 操作前,不是存活状态:

t1.is_alive()
# False

判断线程是否是守护线程

t1.daemon
t1.isDaemon()
# False

设置线程为守护线程

将线程设置为守护线程:

t1.setDaemon(True)

True 为是,False 为否

4、线程模块相关函数

对于 threading 模块,有一些函数可以用于进行相关操作,比如当前存活的线程对象,异常处理等。

接下来先介绍这些函数及其功能,之后会用一个示例应用上这些函数

1. threading.active_count()

返回当前存活的 Thread 对象的数量

2. threading.current_thread()

返回当前对应调用者的线程

3. threading.enumerate()

列表形式返回当前所有存活的 Thread 对象

接下来我们修改 print_info() 函数,运用我们刚刚介绍的这几种函数:

def print_info():
while True:
active_count = threading.active_count()
print("当前存活的线程数量为:", active_count)
for thread in threading.enumerate():
print("存活的线程分别是:", thread.getName())
print("当前所处的的线程名称为:", threading.current_thread().getName())
print("\n")
time.sleep(1)

还是执行 test_daemon_threading() 就可以看到对应的输出信息。

5、线程的异常和函数结果获取

Python 中使用 threading 模块创建的线程中的默认异常以及函数执行结果是不会被主线程捕获的,因为线程是独立运行的,我们可以通过定义全局的变量,比如 dict 或者队列来获取对应的信息。

这里介绍一下通过改写 run() 方法来实现我们的功能。

import threading
import traceback
import time
import request def get_response(url):
response = requests.get(url)
if url.endswith("2"):
1/0
return time.time() def print_info():
while True:
active_count = threading.active_count()
print("当前存活的线程数量为:", active_count)
for thread in threading.enumerate():
print("存活的线程分别是:", thread.getName())
print("当前所处的的线程名称为:", threading.current_thread().getName())
print("\n")
time.sleep(1) class MyThread(threading.Thread):
def __init__(self, func, *args, **kwargs):
super(MyThread, self).__init__()
self.func = func
self.args = args
self.kwargs = kwargs
self.result = None
self.is_error = None
self.trace_info = None def run(self):
try:
self.result = self.func(*self.args, **self.kwargs)
except Exception as e:
self.is_error = True
self.trace_info = traceback.format_exc() def get_result(self):
return self.result if self.is_error is not True else None def test_get_exception_and_result():
base_url = "http://192.168.1.6:5000/test/" t1 = MyThread(get_response, base_url + str(3))
t2 = MyThread(get_response, base_url + str(2)) daemon_t = MyThread(print_info)
daemon_t.setDaemon(True) t1.start()
t2.start()
daemon_t.start() t1.join()
t2.join() print(t1.get_result())
print(t2.is_error)
print(t2.trace_info) if __name__ == "__main__":
test_get_exception_and_result()

在这里,我们调用 get_response 函数时,通过判断 delay 的值,手动触发了报错,以及添加了一个 return 返回值,且通过 MyThread 这个重写的 threading.Thread 来进行操作,获取到线程执行是否有异常,以及异常信息,以及函数返回的结果。

6、锁

如果有时候多个线程需要访问同一个全局变量,可能会导致数据不一致的问题,我们使用线程里的锁来控制对相关资源的访问,以此来确保线程安全,下面是一个示例:

import threading

counter = 0
lock_counter = 0
lock = threading.Lock() def test_no_lock():
global counter
for i in range(1000000):
counter += 1
counter -= 1 def run_no_lock_thread():
t1 = threading.Thread(target=test_no_lock)
t2 = threading.Thread(target=test_no_lock) t1.start()
t2.start() t1.join()
t2.join() def test_lock():
global lock_counter
for i in range(1000000):
lock.acquire()
lock_counter += 1
lock_counter -= 1
lock.release() def run_lock_thread():
t1 = threading.Thread(target=test_lock)
t2 = threading.Thread(target=test_lock) t1.start()
t2.start() t1.join()
t2.join() if __name__ == "__main__":
print("before: ", counter)
run_no_lock_thread()
print("after: ", counter) print("before: ", lock_counter)
run_lock_thread()
print("after: ", lock_counter)

在上面的示例中,通过比对两个加锁和不加锁的情况下全局变量的值,可以发现,多执行几次的话,可以看法 counter 的值并不总是为 0 的,而 lock_counter 的值的结果一直是 0。

我们通过这种加锁的方式来保证 lock_counter 的值是安全的。

锁的引入我们使用的是:

lock = threading.Lock()

获取以及释放的方法是:

lock.acquire()

lock.release()

在这里对于 lock.acquire() 获取锁,有两个参数,blockingtimeout

blocking 表示是否阻塞,默认为 True,表示如果锁没有被释放,则会一直阻塞到锁被其他线程释放,为 False 的话,则表示不阻塞地获取锁,获取到返回为 True,没有获取到返回为 False

lock.acquire()
# 返回为 True,表示获取到锁 lock.acquire()
lock.acquire(blocking=True)
# 这两个操作都是阻塞获取锁,因为前一个操作已经获取到锁,所以这一步会被一直阻塞 is_lock = lock.acquire(blocking=False)
# 不阻塞的获取锁,如果拿到了锁并加锁,则返回为 True,否则返回为 False,表示没有拿到锁

还有一个参数为 timeout,表示 blockingTrue,也就是阻塞的时候,等待的秒数之后,超时没有拿到锁,返回为 False

release() 表示为锁的释放,没有返回值,当前面获取锁之后,可以通过 lock.release() 的方式释放锁。

locked() 返回为布尔型数据,判断是否获得了锁。

7、线程池

我们可以通过线程池的方式来自动管理我们的线程,用到的模块是 concurrent.futures.ThreadPoolExecutor

以下是一个使用示例:

from concurrent.futures import ThreadPoolExecutor
import concurrent.futures def get_response(url):
return True with ThreadPoolExecutor(max_workers=8) as executor:
future_list = [executor.submit(get_response, base_url) for _ in range(20)] for future in concurrent.futures.as_completed(future_list):
print(future.result()

在这里,首先实例化一个线程池,然后输入 max_workers 参数,表示线程池开启的最大的线程数。

之后通过 submit() 方法向线程池提交两个任务,并返回一个 Future 对象,我们可以通过这个 Future 对象获取线程函数执行的各种情况,比如线程函数的返回结果,线程异常情况等。

在这里有一个 concurrent.futures.as_completed() 输入的是一个 Future 列表,会按照 任务完成的顺序 逐个返回已经完成的 Future 对象,这个完成,可以是线程函数执行完成,也可以是出现异常的结果。

接下来介绍一下 Future 对象的几个方法,在此之前,我们设置一下用于试验的基本数据:

from concurrent.futures import ThreadPoolExecutor
import concurrent.futures
import requests
import time def get_response(url):
response = requests.get(url)
if url.endswith("2"):
1/0
return time.time() base_url = "http://192.168.1.6:5000/test/"
executor = ThreadPoolExecutor(max_workers=2) future_1 = executor.submit(get_response, base_url + "3")
future_2 = executor.submit(get_response, base_url + "2")

其中,future_1 线程是正常运行,future_2 在线程里执行报错了。

1. result()

用于获取线程执行的函数返回的结果,如果线程还未完成,那么调用这个方法会阻塞,直到返回结果。

而如果线程里函数执行异常了,调用 result() 方法会重新抛出异常,希望程序正常运行的话,可以加上一个 try-except 操作,或者先通过后面的 exception()方法进行判断。

我们调用 future_1.result() 可以正常返回,而 future_2.result() 会重新报异常。

2. done()

返回一个布尔值,表示线程是否已经完成:

future_1.done() # True
future_2.done() # True

线程执行发生异常也属于完成。

3. exception()

如果线程执行发生异常,可以用这个方法来获取异常对象,如果没有异常就会返回 None

future_2.exception()
# ZeroDivisionError('division by zero')

4. cancel()

尝试取消线程的执行,如果线程还没有开始执行,线程会被标记为取消状态,如果线程已经在执行中或者执行完毕,则不会被取消:

future.cancel()

判断一个线程是否已经被取消,使用方法 cancelled(),返回布尔型数据

5. running()

判断线程是否还在执行中,比如下面的操作:

future_3 = executor.submit(get_response, base_url + "65")
future_3.running() # True

8、如何探索出最佳的线程池线程数量

对于线程池中线程的数量需要指定多少个,是一个需要探索的问题。

比如需要判断我们的任务是否是 IO 密集型的,比如网络请求等,这种的话可以设置相对较高,但也并非无限高,因为等待的过程中,线程间的切换也是一部分开销。

在执行真正的任务前,我们可以通过一小部分任务来进行性能测试,逐步调整线程池的线程数量,然后观察服务器的内存啊,CPU 利用率啊,以及整个操作的消耗时间等,来综合判断出比较适合的线程数量作为最终的结果。

如果想获取更多后端相关文章,可扫码关注阅读:

Python笔记二之多线程的更多相关文章

  1. Python笔记(二)

    python笔记 函数式编程 函数 函数是Python内建支持一种封装(将大段代码拆成函数) 通过函数的调用,可以将复制的任务分解. 函数式编程(Functional Programming) 计算机 ...

  2. Java基础学习笔记二十一 多线程

    多线程介绍 学习多线程之前,我们先要了解几个关于多线程有关的概念.进程:进程指正在运行的程序.确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能. 线 ...

  3. python笔记二(数据类型和变量、编码方式、字符串的编码、字符串的格式化)

    一.数据类型 python可以直接处理的数据类型有:整数.浮点数.字符串.布尔值.空值. 整数 浮点数 字符串:双引号内嵌套单引号,可以输出 i'm ok. 也可以用\来实现,\n 换行 \t tab ...

  4. guxh的python笔记二:函数基础

    1,函数的参数 1.1,查看函数的参数类型 def run(a, *args, b, **kwargs): return a + b 可以通过如下方式查看参数类型: import inspect k ...

  5. python笔记二

    一 运算符 1算术运算+ - * /  % ** //其中%为取余,**为取幂如2**10=1024    9//4=2 需要注意的是python2.7中如9/2=4 需要正确表示,则在开头添加 fr ...

  6. python笔记9 : 多线程

    基础: 什么是进程(process)? 每一个程序的内存是独立的,例如:world不能访问QQ. 进程:QQ是以一个整体的形式暴露给操作系统管理,里面包含了各种资源的调用(内存管理.网络接口调用等). ...

  7. python笔记二(mysql数据库操作)

    python2.x使用MySQLdb python3.x使用pymysql代替MySQLdb 不过我使用的是python3.5,MySQLdb还是可以在python3.5使用的. #!/usr/bin ...

  8. Python笔记(二)查找重复元素

    一.查找数列重复元素---count() >>> list = [,,,,,,,,,,,] >>> set = set(list) >>> for ...

  9. python 笔记二

    17.进程线程 进程间通信方式:管道Pipe:队列Queue:共享内存Value.Array.Manager: 多进程同步:锁Lock.递归锁RLock.Condition(条件变量):事件event ...

  10. Python笔记(二十九)_模块

    模块 在Python中,一个.py文件就是一个模块 if __name__ == '__main__':所有模块都有一个 __name__ 属性,__name__ 的值取决于如何应用模块 run当前文 ...

随机推荐

  1. Python 潮流周刊第 20 期(摘要)

    你好,我是猫哥.本周刊分享优质的 Python.AI 及通用技术内容,大部分为英文.这里是标题摘要版,查看全文请至☞:https://pythoncat.top/posts/2023-09-16-we ...

  2. nodejs实现的一个简单粗暴的洗牌算法

    据说名字长别人不一定看得到 之前用python,自带shuffle用的还是超爽的: 去年6月份自己动手用nodejs写一个21点扑克游戏的后台时,就需要一个洗牌算法,于是简单粗暴的实现了一个. 贴出来 ...

  3. 「codeforces - 1674F」Madoka and Laziness

    link. 如果做过 codeforces - 1144G 那这题最多 *2200. 序列中的最大值必然为其中一个拐点,不妨设 \(a_p = a_\max\),先讨论另一个拐点 \(i\) 在 \( ...

  4. 文心一言 VS 讯飞星火 VS chatgpt (102)-- 算法导论9.3 8题

    八.用go语言,设 X[1..n]和 Y[1..n]为两个数组,每个都包含n个有序的元素.请设计一个 O(lgn)时间的算法来找出数组 X和Y中所有 2n 个元素的中位数. 文心一言: 要在 O(lg ...

  5. SharedPreferences-PreferenceUtils

    SharedPreferences   easy use import android.content.Context; import android.content.SharedPreference ...

  6. MySQL的index merge(索引合并)导致数据库死锁分析与解决方案

    背景 在DBS-集群列表-更多-连接查询-死锁中,看到9月22日有数据库死锁日志,后排查发现是因为mysql的优化-index merge(索引合并)导致数据库死锁. 定义 index merge(索 ...

  7. webwork学习

    学习了H5中的webworker 主机 > 程序 > 进程 > 线程 > 纤程 多进程(重) 多线程(轻) 开销 创建.销毁开销大 创建.销毁开销小 安全性 进程之间是隔离 线 ...

  8. POSIX 真的不适合对象存储吗?

    最近,留意到 MinIO 官方博客的一篇题为"在对象存储上实现 POSIX 访问接口是坏主意"的文章,作者以 S3FS-FUSE 为例分享了通过 POSIX 方式访问 MinIO ...

  9. kafka和zookeeper安装

    一.Kafka简介 Kafka 被称为下一代分布式消息系统,是非营利性组织ASF(Apache Software Foundation,简称为ASF)基金会中的一个开源项目,比如HTTP Server ...

  10. P4022 [CTSC2012]熟悉的文章 题解

    题目链接 简要题意 给定 \(m\) 个模板串和 \(n\) 个匹配串,如果一个字符串是一个模板串的子串且长度不小于 \(L\) 则称其为"熟悉的",对于每个匹配串,求一个最大的 ...