进程可以包含多个并行运行的线程;
通常,操作系统创建和管理线程比进程更省CPU资源;
线程用于一些小任务,进程用于繁重的任务;
同一个进程下的线程共享地址空间和其他资源,进程之间相互独立;
I/O 密集型
任务,如网络请求、文件操作等,因为线程之间的切换开销较小。threading
模块实现。CPU 密集型
任务,如数值计算、图像处理等,因为多进程能够充分利用多核处理器的能力。multiprocessing
模块实现。内存和资源开销:多线程共享同一进程的内存空间,因此创建线程的开销较小;而多进程每个进程都有独立的内存空间,创建进程的开销较大。
数据共享:多线程可以轻松共享数据,因为它们共享相同的内存空间;而多进程需要通过 IPC 来传递数据,因为它们的内存空间是独立的。
CPU 利用率:多线程适合于 I/O 密集型任务,因为线程切换开销较小;多进程适合于 CPU 密集型任务,因为可以充分利用多核处理器的能力。
总的来说,多线程适合于需要频繁进行 I/O 操作的任务,而多进程适合于需要大量 CPU 计算的任务。
在计算机编程中,任务通常可以根据它们是如何使用计算机资源的来分类。主要有两种类型:I/O 密集型任务和 CPU 密集型任务。
I/O 是 Input/Output(输入/输出)的缩写。当说到 I/O 密集型任务时,我们通常是指那些需要大量数据输入和输出的任务。这些任务往往涉及到磁盘、网络或其他类型的数据传输。
特点:
CPU 密集型任务是指那些需要大量计算的任务,它们会充分利用 CPU 的计算能力。
特点:
想象一下,你在厨房里准备晚餐。I/O 密集型任务就像是你在等待水烧开或者烤箱预热。在这期间,你基本上无事可做,只能等待。CPU 密集型任务则像你在切菜、拌沙拉或进行烹饪,你需要不断地工作,直到完成。
在编程中,我们选择多线程、多进程或其他并发模型,取决于任务的类型。如果任务主要是 I/O 密集型的,那么多线程可能会有所帮助,因为线程可以在等待 I/O 操作完成时被操作系统挂起,让其他线程运行。而如果任务是 CPU 密集型的,那么多进程可能更合适,因为每个进程可以在 CPU 上真正地并行运行,从而实现更快的处理速度。
在多线程编程中,主线程(Main Thread) 和子线程(Child Thread) 是用来描述线程之间关系的术语。
join()
方法)子线程完成,以确保在继续执行之前子线程的任务已经结束。假设你有一个 Python 程序,它使用 threading
模块创建了多个线程:
import threading
import time
def child_thread_task(name):
print(f"Child thread {name} is running.")
time.sleep(1)
print(f"Child thread {name} has finished.")
def main_thread_task():
print("Main thread is setting up child threads.")
threads = []
for i in range(3):
t = threading.Thread(target=child_thread_task, args=(i,))
threads.append(t)
t.start()
# 主线程等待所有子线程完成
for t in threads:
t.join()
print("All child threads have completed. Main thread is finishing.")
if __name__ == "__main__":
main_thread_task() # 主线程的任务
[Run]
Main thread is setting up child threads.
Child thread 0 is running.
Child thread 1 is running.
Child thread 2 is running.
Child thread 2 has finished.Child thread 1 has finished.
Child thread 0 has finished.
All child threads have completed. Main thread is finishing.
在这个示例中:
main_thread_task()
函数中的代码运行在主线程上,它负责创建和启动子线程。child_thread_task()
函数中的代码运行在子线程上,每个子线程执行一个独立的任务。join()
方法等待所有子线程完成后才结束。在计算机编程中,锁是一种用来控制对共享资源(如内存、文件等)访问的机制。当多个线程(程序执行的独立流)需要同时运行时,锁可以帮助避免它们在不适当的时刻相互干扰。
想象一下,如果有多个小孩同时在一个图书馆里,他们都想写同一本书。如果没有规则,他们可能会相互覆盖对方的内容,导致书变得乱七八糟。锁就像是图书馆的规则,它确保一次只有一个人能够写这本书。
在计算机中,当多个线程需要修改同一个变量时,如果不使用锁,就可能出现类似的情况,这被称为 “竞态条件” 。竞态条件会导致程序的行为变得不可预测,因为最终结果取决于线程执行的顺序,而这个顺序是无法控制的。
锁的工作原理很简单:
获取锁: 当一个线程想要修改一个共享资源时,它首先需要“获取”(或“锁定”)一个锁。这通常通过调用 acquire()
方法来实现。
执行操作: 一旦线程获取了锁,它就可以安全地执行对共享资源的修改,因为其他线程必须等待锁被释放。
释放锁: 修改完成后,线程会“释放”(或“解锁”)锁,这通常通过调用 release()
方法来实现。这样,其他等待锁的线程就可以继续执行。
互斥锁(Mutex Lock): 最常见的锁类型,一次只允许一个线程获取锁。
递归锁(Recursive Lock): 允许同一个线程多次获取同一个锁,避免了递归函数中的死锁问题。
读写锁(RWLock): 允许多个读取操作同时进行,但写操作是排他的。
信号量(Semaphore): 类似于锁,但可以设定一个计数值,允许多个线程同时访问资源。
条件锁(Condition): 允许线程在某个条件成立之前挂起,直到其他线程发出信号。
假设我们有一个变量 counter
,我们希望两个线程能够交替地对其进行加一操作。
import threading
counter = 0
lock = threading.Lock()
def increment():
global counter
for _ in range(100000):
lock.acquire() # 获取锁
counter += 1
lock.release() # 释放锁
thread1 = threading.Thread(target=increment)
thread2 = threading.Thread(target=increment)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(counter) # 应该打印 200000
在这个例子中,lock
确保了 counter
的增加操作是安全的,即使两个线程在同时运行。如果没有 lock
,两个线程可能会同时读取和修改 counter
,导致最终结果不正确。
死锁: 如果不正确地使用锁,可能会导致死锁,即两个或多个线程相互等待对方释放锁,但没有一个线程愿意放弃,导致程序停止响应。
性能: 过度使用锁可能会降低程序的性能,因为线程需要等待获取锁。
避免锁: 在可能的情况下,尽量避免使用共享资源,或者使用线程局部数据,这样可以减少对锁的需求。
正确地使用锁对于避免竞态条件和确保程序的正确性至关重要。
深入探讨Python中的并发编程,特别关注多线程和多进程的应用。我们将先从基本概念开始,然后通过详细举例探讨每一种机制,最后分享一些实战经验以及一种优雅的编程技巧。