python学习-多线程处理
本文介绍 Python 如何处理多线程,包括多任务概念、多线程基础、datetime 模块、多线程基础处理、线程同步、死锁和活锁、线程安全和数据共享、多线程性能优化等。
供自己以后查漏补缺,也欢迎同道朋友交流学习。
引言
之前介绍的主要都是单线程处理的程序,但在实际开发中,我们可能需要同时处理多个任务,这时候就需要用到多线程处理。
本篇主要介绍 Python 如何处理多线程,包括多任务
概念、多线程基
础、datetime
模块、多线程基础处理、线程同步
、死锁
和活锁
、线程安全
和数据共享
、多线程性能优化
等。
多任务概念
说多线程之前,我们先要了解下多任务
的概念,我们要学习的多进程
和多线程
本质就是要解决多任务的问题,也是希望在一个主线程里可以同时执行多个任务而开发的多线程。
多任务
是指操作系统能够同时执行多个任务
的能力。在现代计算机系统中,多任务可以通过多进程或多线程实现。操作系统
负责管理任务的调度,确保任务公平地共享 CPU 时间
。
- 抢占式多任务(
Preemptive multitasking
): 操作系统控制任务的执行,可以随时中断一个任务,转而执行另一个任务。 - 协同式多任务(
Cooperative multitasking
): 任务自己放弃控制权,允许其他任务运行。
多进程和多线程的区别
-
多进程:
- 每个进程拥有
独立的内存空间
。 - 进程间通信(
IPC
)较为复杂,如管道
、信号
、共享内存
等。 - 进程间切换
开销较大
,因为涉及到虚拟地址空间
的切换。
- 每个进程拥有
-
多线程:
- 同一进程内的线程
共享进程的内存空间
。 线程间通信简单
,因为它们可以直接访问共享数据
。- 线程切换
开销较小
,因为它们共享相同的地址空间
。
- 同一进程内的线程
多进程
在资源隔离
和稳定性
方面更优,但资源消耗
和调度开销
较大。
多线程
在资源利用
和上下文切换
方面更高效,但需要处理线程安全
问题。
多线程的应用场景
对于多线程的应用场景,主要有以下几个方面:
- I/O 密集型任务: 需要频繁进行
输入/输出
操作时,如文件读写、网络通信等。 - 计算密集型任务: 需要大量的
计算操作
时,同时执行多个独立计算任务的场景。 - 异步编程:通过线程实现
异步I/O
操作,提高程序的响应性。 - 用户界面:在
GUI程序
中,使用多线程可以避免界面在执行长时间任务
时无响应。
Python多线程基础
线程
是操作系统
能够进行运算调度
的最小单位
。它被包含在进程
之中,是进程中的实际运作单位。一条线程指的是进程中一个单一的顺序控制流
。
线程模块(threading)
Python
的 threading
模块提供了一个高级的
、基于线程的并发接口
。这个模块包括创建和管理线程所需的所有工具。
主要功能:
创建和启动线程
。线程同步
,包括锁(Locks
)、事件(Events
)、条件(Conditions
)和信号量(Semaphores
)。
基本组件:
- Thread:用于
创建线程
的类
。 - Lock:用于
线程同步
的锁
。 - RLock:可
重入锁
。 - Semaphore:
信号量
,用于控制资源访问。 - Event:用于线程间的
事件通知
。 - Condition:
条件变量
,用于线程间的条件同步。
datetime模块
在详细介绍线程的使用前,我们先学习下 datetime
模块,后面可以使用这个模块做时间记录、性能测试等。
datetime
模块提供了日期和时间的操作。它包括了日期(date
)、时间(time
)、日期时间(datetime
)以及时区处理(timezone
)等类。
主要类和功能:
- date:表示一个
日期
,包含年、月、日。 - time:表示一天中的时间,包含小时、分钟、秒和微秒。
- datetime:结合了日期和时间,包含
date
和time
的所有属性。 - timedelta:表示两个日期或时间之间的差异。
- timezone:用于时区的处理。
datetime的使用
import datetime
# 获取当前日期和时间
now = datetime.datetime.now()
print("当前日期和时间:", now)
# 创建日期
date = datetime.date(2024, 12, 5)
print("创建日期:", date)
# 创建时间
time = datetime.time(12, 30, 45)
print("创建时间:", time)
# 创建日期和时间
date_time = datetime.datetime(2024, 12, 5, 12, 30, 45)
print("特定日期时间:", date_time)
# 格式化日期和时间
formatted_date = now.strftime("%Y-%m-%d %H:%M:%S")
print("格式化日期和时间:", formatted_date)
# 将日期时间转换为时间戳
date_timestamp = datetime.datetime.timestamp(date_time)
print("将日期时间转换为时间戳:", date_timestamp)
# 计算日期时间差
start = datetime.datetime(2024, 12, 5, 12, 30, 45)
end = now
difference = end - start
print("时间差:", difference)
# 时间增量
one_day = datetime.timedelta(days=1)
tomorrow = now + one_day
print("明天的日期:", tomorrow)
# 时区处理
utc_time = datetime.datetime.now(datetime.timezone.utc)
print("UTC时间:", utc_time)
# 将 UTC 时间转换为本地时间
local_time = utc_time.astimezone()
print("本地时间:", local_time)
# 解析字符串为日期时间
date_string = "2024-12-05 12:30:45"
date_obj = datetime.datetime.strptime(date_string, "%Y-%m-%d %H:%M:%S")
print("从字符串解析的日期时间:", date_obj)
datetime的格式化字符表
指令 | 意义 | 示例(基于日期 2024-12-05 12:30:45) |
---|---|---|
%Y | 四位数的年份 | 2024 |
%y | 两位数的年份 | 24 |
%m | 两位数的月份(01至12) | 12 |
%d | 两位数的日(01至31) | 05 |
%H | 两位数的小时(00至23) | 12 |
%I | 两位数的小时(01至12),12小时制 | 12 |
%M | 两位数的分钟(00至59) | 30 |
%S | 两位数的秒(00至59) | 45 |
%f | 六位数的微秒(000000至999999) | 000000 |
%a | 星期的简写 | Wed |
%A | 星期的全称 | Wednesday |
%b | 月份的简写 | Dec |
%B | 月份的全称 | December |
%p | 上午或下午的标识符 | PM |
%z | UTC偏移量(如 +0200) | 无(取决于时区) |
%Z | 时区名称(如 PST) | 无(取决于时区) |
%j | 一年中的第几天(001至366) | 340 |
%U | 一年中的第几周(00至53),星期日作为一周的第一天 | 49 |
%W | 一年中的第几周(00至53),星期一作为一周的第一天 | 49 |
%c | 适合于本地的日期和时间表示 | Wed Dec 5 12:30:45 2024 |
%x | 适合于本地的日期表示 | 12/05/24 |
%X | 适合于本地的时间表示 | 12:30:45 |
%% | 百分号 | % |
这些格式化指令可以在 strftime()
和 strptime()
方法中使用,以控制日期和时间的格式化和解析。
多线程处理
线程的创建和启动
线程的启动是通过调用 start()
方法完成的。一旦线程启动,它将执行其 run()
方法中定义的代码。
import threading
def hello_thread(name):
print(f"Hello, {name}")
# 创建线程
thread_1 = threading.Thread(target=hello_thread, args=("NiuNai",))
# target 目标函数
# args 参数
# 启动线程
thread_1.start()
线程的名称
在 Python
中,可以使用 name
参数来为线程指定一个名称。
import threading
def hello_thread():
print(f"当前线程名称:{threading.current_thread().name}")
# 创建线程
thread_1 = threading.Thread(target=hello_thread)
# 默认是数字累计
thread_2 = threading.Thread(target=hello_thread)
# 使用name参数命名
thread_3 = threading.Thread(target=hello_thread, name="线程3")
thread_4 = threading.Thread(target=hello_thread)
# 直接使用name属性命名
thread_4.name = "线程4"
# 启动线程
thread_1.start()
thread_2.start()
thread_3.start()
thread_4.start()
# 输出
# 当前线程名称:Thread-1 (hello_thread)
# 当前线程名称:Thread-2 (hello_thread)
# 当前线程名称:线程3
# 当前线程名称:线程4
等待线程结束
在 Python
中,可以使用 join()
方法来等待线程结束。如果 join
里面不传参数,就会一直等待线程结束。如果传了具体的秒数,就会等待指定的秒数。
import time
import threading
def hello_thread():
thread_name = threading.current_thread().name
print(f"{thread_name} start")
time.sleep(5)
print(f"{thread_name} end")
# 创建线程
thread_1 = threading.Thread(target=hello_thread, name="线程1")
print('下面是一个线程执行:')
# 启动线程
thread_1.start()
# 等待线程结束
thread_1.join()
print('一个线程执行完成')
# 输出
# 下面是一个线程执行:
# 线程1 start
# 线程1 end
# 一个线程执行完成
线程同步
同步与异步的区别
同步(Synchronous):
- 在
同步
操作中,一个任务的完成必须等待
另一个任务先完成。调用者主动等待这个调用的结果。 - 同步执行通常意味着
阻塞
,即当前线程在任务完成之前会一直等待
。
异步(Asynchronous):
异步
操作允许任务并行执行
,调用者不需要等待
任务完成即可继续执行后续代码。- 异步执行通常意味着
非阻塞
,即当前线程在发起任务后可以去做其他事情,任务完成时会通过回调
、事件
或其他
机制通知调用者
。
线程锁(Locks)
线程锁
用于确保同一时间只有一个线程可以访问特定的资源。它通过 acquire()
方法获取锁
,通过 release()
方法释放锁
。
避免竞态条件和数据不一致性:
**竞态条件(Race Condition
)**发生在多个线程或进程并发访问共享数据
时,最终结果依赖于线程或进程的执行顺序。竞态条件可能导致数据不一致性、程序崩溃或其他不可预测的行为。
线程锁
通过确保同一时间只有一个线程
可以执行临界区代码
来避免
竞态条件。临界区
是指访问共享资源的代码段
。
import threading
# 创建一个锁对象
lock = threading.Lock()
# 共享的全局变量资源
total = 0
def add_total():
global total
# 获取锁
lock.acquire()
try:
total += 10
print("total = ", total)
finally:
# 释放锁
lock.release()
# 创建线程池
threads = []
for i in range(5):
thread = threading.Thread(target=add_total)
threads.append(thread)
thread.start()
# 等待所有线程完成
for thread in threads:
thread.join()
# 输出:
# total = 10
# total = 20
# total = 30
# total = 40
# total = 50
使用with语句自动管理锁:
threading.Lock
还支持上下文管理器协议,可以使用 with
语句自动
获取和释放锁,这可以使代码更简洁:
# ...其他代码
def add_total():
global total
with lock:
total += 10
print("total = ", total)
# ...其他代码
条件变量(Condition)
条件变量
用于线程间的协调
,允许一个线程等待某些条件成立
,而其他线程
在条件成立时通知
等待的线程。
使用 notify()
通知等待的线程,使用 wait()
等待条件成立。
import threading
import time
# 创建一个条件变量
cond = threading.Condition()
# 共享的全局变量资源
count = 0
def producer():
global count
print('生产者线程 start')
with cond:
count = 1 # 生产数据
time.sleep(3)
cond.notify() # 通知等待的线程
print('生产者线程 end')
def consumer():
global count
print('消费者线程 start')
with cond:
cond.wait() # 等待条件成立
# 处理数据
count = 2
print('消费者线程 end')
consumer_thread = threading.Thread(target=consumer)
producer_thread = threading.Thread(target=producer)
consumer_thread.start()
producer_thread.start()
consumer_thread.join()
producer_thread.join()
print('线程全部执行完成: ', count)
# 输出
# 消费者线程 start
# 生产者线程 start
# 生产者线程 end
# 消费者线程 end
# 线程全部执行完成: 2
信号量(Semaphore)
信号量
用于控制对一定数量
的资源的访问。它通过 acquire()
方法请求资源,通过 release()
方法释放资源。
import threading
import time
# 创建一个信号量
sem = threading.Semaphore(2)
def access_resource():
thread_name = threading.current_thread().name
print(f"{thread_name} start")
with sem:
# 访问资源
time.sleep(2)
print(f"{thread_name} end")
pass
threads = []
for i in range(5):
thread = threading.Thread(target=access_resource)
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
# 输出
# Thread-1 (access_resource) start
# Thread-2 (access_resource) start
# Thread-3 (access_resource) start
# Thread-4 (access_resource) start
# Thread-5 (access_resource) start
# 2s后...
# Thread-1 (access_resource) end
# Thread-2 (access_resource) end
# 2s后...
# Thread-3 (access_resource) end
# Thread-4 (access_resource) end
# 2s后...
# Thread-5 (access_resource) end
事件(Event)
事件
用于线程间的通信
,允许一个线程通知一个或多个线程某个事件已经发生。
import threading
import time
# 创建一个事件
event = threading.Event()
def wait_for_event():
print('等待事件被设置 start')
event.wait() # 等待事件被设置
# 事件已发生,继续执行
print('等待事件被设置 end')
def single_event():
# 某个条件满足后
print('设置事件 start')
time.sleep(3)
event.set() # 设置事件
print('设置事件 end')
wait_thread = threading.Thread(target=wait_for_event)
single_thread = threading.Thread(target=single_event)
wait_thread.start()
single_thread.start()
wait_thread.join()
single_thread.join()
# 输出:
# 等待事件被设置 start
# 设置事件 start
# 3s后...
# 设置事件 end
# 等待事件被设置 end
死锁和活锁
死锁
是指两个或多个线程相互等待对方释放资源,从而导致程序无法继续执行
的状态。死锁
产生的必要条件包括互斥使用
、占有且等待
、不可抢占
和循环
等待。
避免死锁
- 预防死锁:通过破坏产生死锁的条件来预防死锁的发生。例如,
资源一次性分配
、只要有一个资源得不到分配,也不给这个进程分配其他的资源、可剥夺资源
、资源有序分配法
等。 - 避免死锁:使用算法如银行家算法来动态避免死锁的发生。银行家算法通过预先分配资源前检查是否会发生死锁来避免死锁。
- 检测死锁并恢复:系统
定期检测死锁
,并尝试回滚其中一个事务,以解除死锁状态。
活锁及其解决方案
活锁
是指线程在执行过程中不断地改变
自己的状态
,但整体上没有进展,导致程序无法继续执行的状态。
活锁与死锁的区别在于,处于活锁的实体是在不断地改变状态,而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能
。
线程安全和数据共享
线程间的通信
线程间的通信
是指不同线程之间交换信息的过程。在多线程
程序中,线程间的通信可以通过以下几种方式实现:
- 共享内存:线程可以通过访问
共享变量
来通信。 - 消息传递:线程可以通过
队列
等数据结构来交换消息
。 - 同步原语:如锁(
Locks
)、事件(Events
)、条件变量(Conditions
)和信号量(Semaphores
)等,用于控制对共享资源的访问
和线程间的协调
。
线程的优先级和调度
- 线程优先级:在某些系统上,线程可以被赋予不同的
优先级
,操作系统的调度器
会根据这些优先级来决定线程的执行顺序。 - 线程调度:
线程调度
是指操作系统如何决定哪个线程应该被执行。调度策略
可以是抢占式
的,也可以是非抢占式
的。
使用线程安全的数据结构
- queue.Queue:提供了线程安全的
队列
实现,适用于生产者-消费者模型
。 - threading.local():提供了线程
局部数据
,每个线程都有自己独立的数据副本
。
import threading
import queue
# 创建一个线程安全的队列
q = queue.Queue()
def producer():
for i in range(5):
q.put(f"Item {i}")
print(f"Produced {i}")
def consumer():
while True:
item = q.get()
if item is None:
break
print(f"Consumed {item}")
q.task_done()
# 创建生产者和消费者线程
t1 = threading.Thread(target=producer)
t2 = threading.Thread(target=consumer)
t1.start()
t2.start()
# 发送结束信号
t1.join()
q.put(None)
t2.join()
# 输出:
# Produced 0
# Produced 1
# Produced 2
# Produced 3
# Produced 4
# Consumed Item 0
# Consumed Item 1
# Consumed Item 2
# Consumed Item 3
# Consumed Item 4
多线程性能优化
线程的开销
线程的开销主要体现在两个方面:空间开销
和时间开销
。
空间开销
包括线程内核对象
、线程环境块
、用户模式栈
和内核模式栈
所占用的内存
。
时间开销
则涉及到线程创建时的内存空间初始化
、DLLMain
方法调用等过程。
频繁地创建和销毁线程会带来较大的开销,因此线程池
被引入以提高资源使用效率
线程池(ThreadPoolExecutor)
线程池
是一组线程的集合,它们在程序启动时创建,并在整个程序生命周期内重复使用,从而避免
了线程创建和销毁的开销。
在 Python
中,可以通过 concurrent.futures
模块中的 ThreadPoolExecutor
类来管理线程池,并执行并发任务
。
线程池的核心参数
包括核心线程数
、最大线程数
、非核心线程
的空闲时间、任务队列等。
import concurrent.futures
import time
def task(count):
time.sleep(1)
return f"线程{count}任务完成"
# 创建线程池
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
# 使用 map 方法提交多个任务
futures = [executor.submit(task, i) for i in range(5)]
# as_completed 方法可以获取完成的任务结果
for future in concurrent.futures.as_completed(futures):
print(future.result())
异步编程(asyncio)
在 asyncio
中,使用 async
关键字定义一个协程,而使用 await
关键字来等待异步操作的完成。
import asyncio
async def task(count):
print(f"任务{count}执行 start")
await asyncio.sleep(1) # 异步等待1秒
print(f"任务{count}执行 end")
async def main():
# 并发执行多个协程
await asyncio.gather(task(1), task(2), task(3), task(4))
# 运行事件循环
asyncio.run(main())
# 输出
# 任务1执行 start
# 任务2执行 start
# 任务3执行 start
# 任务4执行 start
# 任务1执行 end
# 任务2执行 end
# 任务3执行 end
# 任务4执行 end
python学习专栏系列
- python学习-基础学习1
- python学习-基础学习2
- python学习-基础学习3
- python学习-面向对象编程1
- python学习-面向对象编程2
- python学习-文件读写
- python学习-程序异常处理
- python学习-正则
- python学习-处理word文档
- python学习-处理pdf文档
- python学习-处理excel文档
- python学习-处理csv文档
- python学习-使用matplotlib绘制图表
- python学习-处理JSON数据
- python学习-SQLite数据库
- python学习-多线程处理
- python学习-网络爬虫