python入门系列十三(多线程)

1.引言

多线程即并发编程,目的是提升计算机处理应用的能力(提效)。python的并发编程稍显特别,因为GIL(全局解释器锁的存在)。GIL本质上是一个互斥锁,同一时间只能有一个线程执行python字节码。这样的设计简化了内存管理,当然也影响了并发的性能。

正因为这样,python还支持了另外一个特性:多进程。多进程不受GIL的影响。那么问题来了?在实际应用中,我们如何衡量选择多线程,还是多进程呢?毕竟都存在,存在即合理,自然有它的原由。

  • I/O密集型任务,线程在等待I/O时会释放锁,GIL影响小,适合多线程
  • CPU密集型任务,适合多进程

如何区分I/O密集型任务,和CPU密集型任务?

  • I/O密集型任务:文件读写任务,网络请求任务
  • CPU密集型任务:涉及大量计算的任务

2.多线程案例

2.1.线程入门

python模块threading,方便我们实现多线程编程模型。像这样

python 复制代码
import threading
import time

# 定义线程任务
def task(num):
    print(f"线程{num}启动")
    time.sleep(1)
    print(f"线程{num}完成")

# 循环创建线程任务,并启动执行
for i in range(3):
    t = threading.Thread(target=task, args=(i,))
    t.start()

2.2.线程属性

线程thread本身是对象,在实际应用中,我们需要关心相关的一些属性。

python 复制代码
import threading
import time

# 定义线程任务
def task(num):
    print(f"线程{num}启动")
    time.sleep(1)
    print(f"线程{num}完成")

# 循环创建线程任务,并启动执行
t = threading.Thread(target=task, args=(1,))
# 设置线程名称
t.name = "MyThread"
# 设置线程为守护线程
t.daemon = True
t.start()

# 输出线程相关属性
print(f"线程名称: {t.name}")
print(f"线程状态: {t.is_alive()}")
print(f"线程ID: {t.ident}")
print(f"线程守护状态: {t.daemon}")

2.3.线程方法

我们还需要关心的一些常用线程方法

2.3.1.线程生命周期控制

正确启动线程方式:

python 复制代码
t = threading.Thread(target=lambda: print("线程已启动"))
t.start()  # 正确启动方式
# t.run()   # 错误用法!

等待线程结束:

python 复制代码
import threading
import time

def long_task():
    time.sleep(2)
    print("任务完成")

t = threading.Thread(target=long_task)
t.daemon = True  # 设置为守护线程
t.start()
print("主线程继续执行")
t.join(timeout=1)  # 等待1秒
print("等待1秒后继续")
t.join()  # 继续等待直到结束

示例效果关键代码行,设置为守护线程,只有守护线程主线程不用等待它的执行完成。可以通过注释最后一行代码观察执行结果。

python 复制代码
t.join()

2.3.2.守护线程配置

守护线程,等于是后台执行线程,它的特点是主线程不用等待它的执行完成,即可结束。

python 复制代码
import threading
import time

def daemon_worker():
    while True:
        print("后台运行中...")
        time.sleep(1)

t = threading.Thread(target=daemon_worker)
t.daemon = True  # 必须设置在start之前
t.start()
time.sleep(3)
print("主程序退出,守护线程自动终止")

2.3.3.线程中断

线程中断是个很讲究的概念,和处理问题的方式。在并发编程中,我们不建议强制终止线程任务,那样会出现不受控的意外结果。但有时候业务上确实需要停止线程任务继续执行,该如何办?

答案是协商一致,和正在执行任务的线程商量:你可以停下来了吗?由线程自主决定,中断即是与线程协商的方法方式。

python 复制代码
import threading
import time

# 自定义线程类
class StoppableThread(threading.Thread):
    def __init__(self):
        super().__init__()
        self._stop_event = threading.Event()

    def stop(self):
        self._stop_event.set()

    def stopped(self):
        return self._stop_event.is_set()

    def run(self):
        while not self.stopped():
            print("线程运行中...")
            time.sleep(0.5)

# 创建并启动线程
t = StoppableThread()
t.start()

time.sleep(2)

# 发送线程中断信号
print("发送线程中断信号...")
t.stop()
# 等待线程结束
t.join()
print("线程已安全停止")

2.4.锁

并发编程中存在共享资源,多线程会争夺共享资源占用,需要有一种机制来保障资源的有序使用,不至于出现错乱现象。这种机制即是锁。

2.4.1.无锁案例

案例说明:

  • 1.共享资源变量num=100,我们开100个线程,每个线程将资源变量num 减 1
  • 2.预期通过100个线程执行完后,最后num=0.
  • 3.由于线程用资源时,没有加锁,可能会出现某几个线程拿到的资源一样
  • 4.比如当num=50时,有两个线程同时拿到num=50这个资源,就会出现100个线程都减一后,最后num不等于0,而是其它数值
python 复制代码
import threading
import time

#定义任务方法add_num
def add_num():
    global num
    temp = num
    # 模拟任务耗时
    time.sleep(0.001)
    num = temp-1
    print(f'线程:{threading.current_thread().name}执行.OK,拿到值为:{temp},减一后,num为:{num}')

# 设定一个共享变量,模拟共享资源,如后续的数据库
num = 100
# 线程组
thread_list = []
#循环开启100个线程执行任务
for i in range(100):
    t = threading.Thread(target=add_num)
    t.start()
    thread_list.append(t)

# 主线程等待所有线程都执行完
for i in thread_list:
    i.join()

#输出最终结果
print('Final num:',num)

2.4.2.加锁案例

python中threading模块提供了Lock接口,来看加锁以后的效果

python 复制代码
import threading
import time

#创建一把锁
r = threading.Lock()

#定义任务方法add_num
def add_num():
    global num
    # 加锁
    r.acquire()
    temp = num
    # 模拟任务耗时
    time.sleep(0.001)
    num = temp-1
    print(f'线程:{threading.current_thread().name}执行.OK,拿到值为:{temp},减一后,num为:{num}')
    # 释放锁
    r.release()

# 设定一个共享变量,模拟共享资源,如后续的数据库
num = 100
# 线程组
thread_list = []
#循环开启100个线程执行任务
for i in range(100):
    t = threading.Thread(target=add_num)
    t.start()
    thread_list.append(t)

# 主线程等待所有线程都执行完
for i in thread_list:
    i.join()

#输出最终结果
print('Final num:',num)

2.5.线程池

虽然python线程底层实现不是基于内核线程,而是基于用户态空间实现的轻量级线程,但毕竟频繁创建线程对象,释放线程对象,资源利用率不高。从资源复用的角度考虑,池化是一个不错的选择方案。

通过线程池改造上面的案例:

python 复制代码
import threading
import time
import concurrent.futures
# 创建一个线程池,最大线程数为5
executor = concurrent.futures.ThreadPoolExecutor(max_workers=5)

#创建一把锁
r = threading.Lock()

#定义任务方法add_num
def add_num():
    global num
    # 加锁
    r.acquire()
    temp = num
    # 模拟任务耗时
    time.sleep(0.001)
    num = temp-1
    print(f'线程:{threading.current_thread().name}执行.OK,拿到值为:{temp},减一后,num为:{num}')
    # 释放锁
    r.release()

# 设定一个共享变量,模拟共享资源,如后续的数据库
num = 100
#循环开启100个线程执行任务
for i in range(100):
    future = executor.submit(add_num)

#关闭线程池
executor.shutdown()

#输出最终结果
print('Final num:',num)
相关推荐
阿坡RPA6 小时前
手搓MCP客户端&服务端:从零到实战极速了解MCP是什么?
人工智能·aigc
用户27784491049936 小时前
借助DeepSeek智能生成测试用例:从提示词到Excel表格的全流程实践
人工智能·python
机器之心6 小时前
刚刚,DeepSeek公布推理时Scaling新论文,R2要来了?
人工智能
算AI8 小时前
人工智能+牙科:临床应用中的几个问题
人工智能·算法
JavaEdge在掘金8 小时前
ssl.SSLCertVerificationError报错解决方案
python
我不会编程5559 小时前
Python Cookbook-5.1 对字典排序
开发语言·数据结构·python
凯子坚持 c9 小时前
基于飞桨框架3.0本地DeepSeek-R1蒸馏版部署实战
人工智能·paddlepaddle
老歌老听老掉牙9 小时前
平面旋转与交线投影夹角计算
python·线性代数·平面·sympy
满怀10159 小时前
Python入门(7):模块
python
无名之逆9 小时前
Rust 开发提效神器:lombok-macros 宏库
服务器·开发语言·前端·数据库·后端·python·rust