一、多任务
在Python中,多任务处理指的是同时执行多个任务,以提高程序的效率和响应能力
多任务处理在需要处理大量I/O操作(如网络请求、文件读写等)或同时执行多个独立任务时特别有用
Python提供了几种实现多任务处理的方法,主要包括多线程、多进程和异步编程
二、多线程
2.1 进程和线程
进程:进程是操作系统进行资源分配和调度的基本单位。当我们打开一个程序时,操作系统会为该程序创建一个进程。这个进程会拥有独立的内存空间、文件句柄和其他系统资源。进程之间通过进程间通信(IPC)机制进行数据交换,以确保它们的独立性。
线程:线程是CPU调度的基本单位。每个进程在创建时都会默认拥有一个线程,这个线程被称为主线程。主线程负责执行进程的代码,并管理进程的生命周期。除了主线程外,进程还可以创建多个额外的线程来并发执行任务。
进程与线程的关系:
1、包含关系:进程包含线程,线程是进程的一部分。每个进程至少有一个线程(主线程),而线程则必须依附于某个进程才能存在和运行
2、资源共享:进程拥有独立的资源空间,而线程共享进程的资源。这使得线程之间的通信和数据交换更加高效,但也需要注意线程同步问题
3、并发执行:进程和线程都可以并发执行。操作系统通过调度线程来实现并发执行,而线程则依赖于进程的存在和运行环境
4、生命周期:进程的生命周期包括创建、运行、阻塞和终止等阶段。线程的生命周期则包括创建、就绪、运行、阻塞和终止等阶段。线程的生命周期受进程生命周期的影响,当进程终止时,其所有线程也会随之终止
2.2 多线程
在Python中,多线程指的是在单个进程中同时运行多个线程的能力。每个线程都是进程中的一个执行单元,它们共享进程的资源(如内存和文件句柄),但拥有自己独立的栈和程序计数器。多线程允许程序并发地执行多个任务,从而提高程序的效率和响应能力。
Thread类参数
1、target:这是一个必需的参数,它指定了要由线程运行的目标函数(也称为任务函数)。当线程启动时,它将调用这个函数
2、args:这是一个可选参数,它提供了一个元组,该元组包含了要传递给目标函数的参数。如果目标函数不需要参数,则可以省略此参数或将其设置为空元组( )
3、kwargs:这也是一个可选参数,它提供了一个字典,该字典包含了要传递给目标函数的关键字参数。如果目标函数不需要关键字参数,则可以省略此参数或将其设置为空字典{ }
eg:使用threading.Thread类来创建一个线程,并传递参数给目标函数
# 导入线程模块
import threading
# 导入time模块,用于模拟耗时操作
import time
def sing(name):
print(f"{name}在唱歌")
time.sleep(2) # 使程序暂停执行2秒,模拟唱歌的耗时
print("唱完歌了")
def dance(name2):
print(f"{name2}在跳舞")
time.sleep(2) # 使程序暂停执行2秒,模拟跳舞的耗时
print("跳完舞了")
# 主程序入口
if __name__ == "__main__":
# 1. 创建子线程
# 创建两个线程对象t1和t2,它们分别执行sing()和dance()函数
t1 = threading.Thread(target=sing,args=('junjun',)) # 已元组的形式传参,()后面记得用逗号隔开
t2 = threading.Thread(target=dance,args=('junjun',))
# 2. 开启子线程
# 通过调用start()方法,线程t1和t2开始执行它们的目标函数
t1.start()
t2.start()
# 3. 阻塞主线程,等待子线程结束
# join()方法会阻塞调用它的线程(在这里是主线程),直到被调用的线程(t1和t2)执行结束
# 这意味着主线程会等待t1和t2都执行完毕后,才会继续执行后面的代码
t1.join()
t2.join()
# 4. 获取线程名
print(t1.getName())
print(t2.getName())
# 5. 更改线程名
t1.setName("子线程一")
t2.setName("子线程二")
# 当t1和t2都执行完毕后,主线程继续执行,打印这条消息
print("完美谢幕,本次表演结束")
由于线程是并发执行的,因此输出的顺序可能会有所不同,但大致上应该是这样的(假设唱歌的线程先执行):
junjun在唱歌
junjun在跳舞
唱完歌了跳完舞了
Thread-1
Thread-2
完美谢幕,本次表演结束
2.3 多线程的三个特点
1、线程之间执行是无序的
2、线程之间共享资源
3、资源竞争
2.3.1 线程之间的执行是无序的
线程执行是根据cpu调度决定的,多线程程序的一个关键特性是线程之间的执行顺序是不确定的。这意味着,虽然您可以启动多个线程并按照某种顺序编写它们的代码,但操作系统调度这些线程的方式决定了它们实际上将如何交替执行。
因此,在不同的运行或相同的运行但不同的时间点,线程的执行顺序可能会有所不同。这种无序性使得多线程程序难以调试和预测其行为。
eg:使用threading
模块来演示线程之间的执行无序性
import threading
import time
# 定义线程要执行的函数
def thread_function(name, duration):
print(f"Starting {name}")
time.sleep(duration) # 模拟线程正在做一些工作
print(f"Ending {name}")
# 创建线程
thread1 = threading.Thread(target=thread_function, args=("Thread-1", 2))
thread2 = threading.Thread(target=thread_function, args=("Thread-2", 1))
# 启动线程
thread1.start()
thread2.start()
# 等待线程完成(虽然在这个例子中不是必需的,因为我们没有主线程需要等待它们的结果)
thread1.join()
thread2.join()
print("All threads have finished.")
在这个例子中,我们创建了两个线程thread1和thread2,它们分别执行thread_function函数,该函数接受一个线程名称和一个持续时间作为参数。time.sleep(duration)用于模拟线程正在执行一些耗时操作。
尽管我们按照thread1.start()和thread2.start()的顺序启动了线程,但thread2可能会因为持续时间较短而先于thread1结束
由于线程调度的不确定性,每次运行这个程序时,线程的输出顺序都可能不同
可能的输出之一(但不是唯一的):
Starting Thread-1
Starting Thread-2
Ending Thread-2
Ending Thread-1
All threads have finished.
但也有可能Thread-2先开始并结束,或者两个线程的输出几乎同时出现(尽管由于time.sleep的存在,这种情况不太可能完全同时发生)
2.3.2 线程之间共享资源
在Python中,线程之间可以共享全局变量、堆上的对象或通过参数传递的共享数据结构来实现资源共享。
由于Python的全局解释器锁(GIL)的存在,对于CPU密集型任务,多线程可能不会像在多处理器系统上那样提供显著的并行性提升。
然而,对于I/O密集型任务,多线程仍然可以显著提高性能,因为等待I/O操作完成的线程可以被挂起,让出CPU给其他线程使用。
eg:展示了两个线程如何共享一个全局计数器变量
import threading
# 共享的全局变量
shared_counter = 0
# 定义线程要执行的函数
def increment_counter(thread_name, iterations):
global shared_counter
for _ in range(iterations):
# 这里没有使用锁,因此存在竞态条件
shared_counter += 1
# 为了模拟一些工作,我们让线程休眠一小段时间
# 在实际应用中,这可能是等待I/O操作完成
time.sleep(0.001) # 非常短的时间,仅用于演示
print(f"{thread_name} incremented the counter {iterations} times.")
# 导入time模块(在上面的代码示例中遗漏了)
import time
# 创建线程
thread1 = threading.Thread(target=increment_counter, args=("Thread-1", 1000000))
thread2 = threading.Thread(target=increment_counter, args=("Thread-2", 1000000))
# 启动线程
thread1.start()
thread2.start()
# 等待线程完成
thread1.join()
thread2.join()
# 打印最终的计数器值
# 由于竞态条件,这个值可能不是预期的2000000
print(f"Final counter value: {shared_counter}")
注意:上面的代码示例存在竞态条件(race condition),因为两个线程在没有同步的情况下同时修改共享变量shared_counter。这意味着最终的计数器值可能不是预期的2,000,000,而是小于这个值,因为线程之间的操作可能会交错进行。
为了解决这个问题,我们可以使用threading.Lock来确保一次只有一个线程能够修改共享变量:
import threading
import time
# 共享的全局变量和锁
shared_counter = 0
counter_lock = threading.Lock()
# 定义线程要执行的函数
def increment_counter(thread_name, iterations):
global shared_counter, counter_lock
for _ in range(iterations):
# 在修改共享变量之前获取锁
with counter_lock:
shared_counter += 1
# 锁在with块结束时自动释放
time.sleep(0.001) # 模拟一些工作
print(f"{thread_name} incremented the counter {iterations} times.")
# 创建线程
thread1 = threading.Thread(target=increment_counter, args=("Thread-1", 1000000))
thread2 = threading.Thread(target=increment_counter, args=("Thread-2", 1000000))
# 启动线程
thread1.start()
thread2.start()
# 等待线程完成
thread1.join()
thread2.join()
# 打印最终的计数器值
# 现在这个值应该是预期的2000000
print(f"Final counter value: {shared_counter}")
在这个修改后的示例中,我们使用了threading.Lock来确保对共享变量shared_counter的访问是线程安全的。with counter_lock:语句块确保了在修改计数器时,一次只有一个线程能够执行该块内的代码
2.3.3 资源竞争
资源竞争发生在多个线程尝试同时访问或修改共享资源时。每个线程都试图执行其代码,这可能涉及读取、写入或更新共享变量。如果没有适当的同步机制(如锁、信号量或条件变量),则一个线程可能会读取到另一个线程尚未完成写入的数据,或者两个线程可能会同时写入导致数据损坏。这种情况称为数据竞争或竞态条件。
eg:使用两个线程来递增一个共享的全局计数器,由于缺少同步机制,这两个线程可能会同时访问和修改计数器,导致最终的结果不正确。
import threading
import time
# 共享的全局计数器
shared_counter = 0
# 定义线程要执行的函数
def increment_counter(thread_name, iterations):
global shared_counter
for _ in range(iterations):
# 这里没有使用锁,因此存在资源竞争
# 线程可能会同时读取和写入shared_counter,导致数据损坏
local_copy = shared_counter # 读取共享变量
local_copy += 1 # 在本地副本上进行修改
shared_counter = local_copy # 将修改后的值写回共享变量
# 为了模拟一些工作,我们让线程休眠一小段时间
# 在实际应用中,这可能是等待I/O操作完成
time.sleep(0.00001) # 非常短的时间,仅用于演示
print(f"{thread_name} incremented the counter {iterations} times.")
# 创建线程
thread1 = threading.Thread(target=increment_counter, args=("Thread-1", 100000))
thread2 = threading.Thread(target=increment_counter, args=("Thread-2", 100000))
# 启动线程
thread1.start()
thread2.start()
# 等待线程完成
thread1.join()
thread2.join()
# 打印最终的计数器值
# 由于资源竞争,这个值可能不是预期的200000
print(f"Final counter value: {shared_counter}")
在这个例子中,increment_counter函数试图通过以下步骤来递增计数器:
1、读取共享变量shared_counter到本地变量local_copy
2、在local_copy上进行加1操作
3、将修改后的local_copy值写回共享变量shared_counter
然而,由于两个线程可能会同时执行这些步骤,它们可能会读取到相同的初始值,然后都将其加1,并写回相同的值。这会导致计数器只递增了一次,而不是两次。这种情况在多线程环境中会频繁发生,导致最终的计数器值远小于预期的200,000。
为了解决这个问题,我们需要使用同步机制,如锁(threading.Lock),来确保一次只有一个线程能够访问和修改共享变量。以下是使用锁来避免资源竞争的修改后的示例:
import threading
import time
# 共享的全局计数器和锁
shared_counter = 0
counter_lock = threading.Lock()
# 定义线程要执行的函数
def increment_counter(thread_name, iterations):
global shared_counter, counter_lock
for _ in range(iterations):
# 在修改共享变量之前获取锁
with counter_lock:
# 锁保证了以下操作是原子的,即一次只有一个线程能够执行这些操作
shared_counter += 1
# 锁在with块结束时自动释放
# 为了模拟一些工作,我们让线程休眠一小段时间
time.sleep(0.00001) # 非常短的时间,仅用于演示
print(f"{thread_name} incremented the counter {iterations} times.")
# 创建线程
thread1 = threading.Thread(target=increment_counter, args=("Thread-1", 100000))
thread2 = threading.Thread(target=increment_counter, args=("Thread-2", 100000))
# 启动线程
thread1.start()
thread2.start()
# 等待线程完成
thread1.join()
thread2.join()
# 打印最终的计数器值
# 现在这个值应该是预期的200000
print(f"Final counter value: {shared_counter}")
今天的分享就到这里了,希望能够帮助到大家~