python学习-多线程处理

163 阅读15分钟

python学习-多线程处理

本文介绍 Python 如何处理多线程,包括多任务概念、多线程基础、datetime 模块、多线程基础处理、线程同步、死锁和活锁、线程安全和数据共享、多线程性能优化等。

供自己以后查漏补缺,也欢迎同道朋友交流学习。

引言

之前介绍的主要都是单线程处理的程序,但在实际开发中,我们可能需要同时处理多个任务,这时候就需要用到多线程处理。

本篇主要介绍 Python 如何处理多线程,包括多任务概念、多线程基础、datetime 模块、多线程基础处理、线程同步死锁活锁线程安全数据共享、多线程性能优化等。

多任务概念

说多线程之前,我们先要了解下多任务的概念,我们要学习的多进程多线程本质就是要解决多任务的问题,也是希望在一个主线程里可以同时执行多个任务而开发的多线程。

多任务是指操作系统能够同时执行多个任务的能力。在现代计算机系统中,多任务可以通过多进程或多线程实现。操作系统负责管理任务的调度,确保任务公平地共享 CPU 时间

  • 抢占式多任务(Preemptive multitasking: 操作系统控制任务的执行,可以随时中断一个任务,转而执行另一个任务。
  • 协同式多任务(Cooperative multitasking: 任务自己放弃控制权,允许其他任务运行。

多进程和多线程的区别

  • 多进程

    • 每个进程拥有独立的内存空间
    • 进程间通信(IPC)较为复杂,如管道信号共享内存等。
    • 进程间切换开销较大,因为涉及到虚拟地址空间的切换。
  • 多线程

    • 同一进程内的线程共享进程的内存空间
    • 线程间通信简单,因为它们可以直接访问共享数据
    • 线程切换开销较小,因为它们共享相同的地址空间

多进程资源隔离稳定性方面更优,但资源消耗调度开销较大。

多线程资源利用上下文切换方面更高效,但需要处理线程安全问题。

多线程的应用场景

对于多线程的应用场景,主要有以下几个方面:

  • I/O 密集型任务: 需要频繁进行输入/输出操作时,如文件读写、网络通信等。
  • 计算密集型任务: 需要大量的计算操作时,同时执行多个独立计算任务的场景。
  • 异步编程:通过线程实现异步I/O操作,提高程序的响应性。
  • 用户界面:在GUI程序中,使用多线程可以避免界面在执行长时间任务时无响应。

Python多线程基础

线程操作系统能够进行运算调度最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一的顺序控制流

线程模块(threading)

Pythonthreading 模块提供了一个高级的基于线程的并发接口。这个模块包括创建和管理线程所需的所有工具。

主要功能

  • 创建和启动线程
  • 线程同步,包括锁(Locks)、事件(Events)、条件(Conditions)和信号量(Semaphores)。

基本组件

  • Thread:用于创建线程
  • Lock:用于线程同步
  • RLock:可重入锁
  • Semaphore信号量,用于控制资源访问。
  • Event:用于线程间的事件通知
  • Condition条件变量,用于线程间的条件同步。

datetime模块

在详细介绍线程的使用前,我们先学习下 datetime 模块,后面可以使用这个模块做时间记录、性能测试等。

datetime 模块提供了日期和时间的操作。它包括了日期(date)、时间(time)、日期时间(datetime)以及时区处理(timezone)等类。

主要类和功能

  • date:表示一个日期,包含年、月、日。
  • time:表示一天中的时间,包含小时、分钟、秒和微秒。
  • datetime:结合了日期和时间,包含 datetime 的所有属性。
  • 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
%zUTC偏移量(如 +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-study