【Python】 进程与线程

一、进程简介

在计算机操作系统中,进程是资源分配和调度的基本单位。每个进程都有自己独立的内存空间、代码执行上下文以及系统资源,如文件描述符、网络连接等。进程之间相互隔离,一个进程的崩溃通常不会影响到其他进程。
在 Python 中,多进程编程允许我们充分利用多核处理器的优势,并行地执行多个任务,从而提高程序的执行效率和响应速度。例如,在处理大规模数据计算、网络爬虫的并发请求、图像处理等任务时,多进程可以显著缩短执行时间。

资源分配

进程能够让操作系统合理地分配计算机资源。这些资源包括 CPU 时间、内存空间、I/O 设备(如磁盘、打印机等)。例如,当多个程序同时运行时,操作系统通过进程管理,为每个进程分配一定的 CPU 时间片。就像一个有多个任务要完成的人,他会把自己的时间分成若干段,分别用于完成不同的任务。比如,在听音乐(一个进程)的同时浏览网页(另一个进程),操作系统会分配 CPU 时间让这两个进程交替执行。

独立运行环境

每个进程都有自己独立的地址空间,这使得不同的进程可以在同一台计算机上运行而不会相互干扰。例如,一个游戏进程和一个办公软件进程,它们的数据和代码是相互隔离的。游戏进程中的数据(如游戏角色的位置、等级等)不会被办公软件进程随意访问和修改,反之亦然。这就好比不同的家庭(进程)住在不同的房子(地址空间)里,每个家庭可以在自己的房子里自由活动,而不会轻易影响到其他家庭。

支持多任务处理

现代操作系统允许同时运行多个进程,这使得用户可以同时进行多种操作。例如,用户可以一边在后台下载文件(一个进程),一边在前台编辑文档(另一个进程)。操作系统通过进程调度来决定哪个进程可以在某个时刻使用 CPU 等资源,让计算机看起来像是在同时处理多个任务,提高了计算机系统的效率和用户体验。
进程在计算机中扮演的角色

任务执行主体

进程是实际执行计算机任务的主体。无论是简单的计算任务,如计算 1+1 等于多少,还是复杂的图形渲染任务(像在 3D 游戏中渲染精美的场景),都是通过进程来完成的。没有进程,程序代码只是存储在磁盘上的静态指令,无法发挥作用。

系统资源使用者

进程是计算机系统资源的主要使用者。它们从操作系统那里获取 CPU 时间、内存等资源,并且在运行过程中根据需要向操作系统请求更多的资源。例如,一个图像处理软件在打开大型图像文件时,可能会向操作系统请求更多的内存来存储图像数据,这个请求是通过该软件对应的进程来完成的。
系统状态的改变者
进程的运行会改变计算机系统的状态。例如,进程在运行过程中可能会修改文件系统中的文件内容,或者更新系统的一些配置信息。当一个文档编辑进程保存文档时,它会将修改后的文档内容写回到磁盘上的文件中,从而改变了文件系统的状态。同时,进程的运行状态(如运行、阻塞、就绪等)也会影响操作系统的调度策略,进而影响整个计算机系统的运行状态。

二、Multiprocessing 模块

multiprocessing 是 Python 内置的用于支持多进程编程的模块。它提供了丰富的功能和类,使得创建和管理多个进程变得相对容易。
该模块实现了类似于 threading 模块的 API,使得熟悉线程编程的开发者能够快速上手多进程编程。它提供了创建进程、进程间通信、进程同步等功能,方便我们在 Python 中进行高效的多进程开发。

Multiprocessing官方标准库
官方链接:Multiprocessing
相关参数,可查看官网,这里就不多说了

三、创建线程

使用 threading.Thread 类来创建线程。可以通过两种方式实现:一是定义一个函数,将其作为线程的执行体;二是创建一个继承自 threading.Thread 的子类,并重写 run 方法。

import threading

# 方式一:使用函数创建线程
def print_numbers():
    for i in range(10):
        print(i)

t1 = threading.Thread(target=print_numbers)

# 方式二:使用类创建线程
class PrintThread(threading.Thread):
    def run(self):
        for i in range(10):
            print(i)

t2 = PrintThread()

t1.start()
t2.start()

线程同步

在多线程编程环境中,当多个线程共同对特定数据进行修改操作时,往往会引发难以预期的结果。为了切实确保数据的准确性与完整性,对多个线程实施同步机制显得尤为关键。

通过 Thread 对象所提供的 Lock 和 Rlock 能够达成较为简易的线程同步效果。这两种对象均具备 acquire 方法与 release 方法。针对那些在同一时刻仅允许单个线程进行操作的数据而言,可以将针对该数据的操作代码放置于 acquire 方法与 release 方法之间。

多线程机制的显著优势在于能够让多个任务看似同时并行地运行起来。然而,当线程之间存在共享数据的需求时,数据不同步的棘手问题便可能随之产生。
设想存在这样一种情形:有一个列表,其内部所有元素初始值均为 0。其中一个名为 “set” 的线程负责从后向前将列表中的所有元素逐一修改为 1,而另一个名为 “print” 的线程则承担从前往后读取该列表并进行打印输出的任务。
如此一来,极有可能出现这样的状况:当线程 “set” 刚开始对列表元素进行修改操作时,线程 “print” 便前来读取并打印列表了,这样最终的输出结果就会呈现出列表前半部分为 0,后半部分为 1 的混乱局面,这正是典型的数据不同步问题所导致的不良后果。

为了有效规避此类情况的发生,锁的概念应运而生。锁存在两种基本状态,即锁定状态与未锁定状态。每当某个线程,例如 “set” 线程,意图访问共享数据时,其必须首先成功获取锁的锁定状态。倘若此时已经有其他线程,比如 “print” 线程,获取了该锁的锁定状态,那么线程 “set” 将会被强制暂停执行,也就是进入同步阻塞状态。直至线程 “print” 完成对共享数据的访问操作并释放锁之后,线程 “set” 才能够得以继续执行其原本的任务。

经过这样严谨的处理流程之后,当对列表进行打印输出操作时,其结果要么是全部输出 0,要么是全部输出 1,再也不会出现那种令人尴尬的一半 0 一半 1 的错误输出情况了。


import threading
import time

class myThread (threading.Thread):
    def __init__(self, threadID, name, delay):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name
        self.delay = delay
    def run(self):
        print ("开启线程: " + self.name)
        # 获取锁,用于线程同步
        threadLock.acquire()
        print_time(self.name, self.delay, 3)
        # 释放锁,开启下一个线程
        threadLock.release()

def print_time(threadName, delay, counter):
    while counter:
        time.sleep(delay)
        print ("%s: %s" % (threadName, time.ctime(time.time())))
        counter -= 1

threadLock = threading.Lock()
threads = []

# 创建新线程
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)

# 开启新线程
thread1.start()
thread2.start()

# 添加线程到线程列表
threads.append(thread1)
threads.append(thread2)

# 等待所有线程完成
for t in threads:
    t.join()
print ("退出主线程")

在这里插入图片描述

线程优先级队列( Queue)

Python 的 Queue 模块中提供了同步的、线程安全的队列类,包括FIFO(先入先出)队列Queue,LIFO(后入先出)队列LifoQueue,和优先级队列 PriorityQueue。

这些队列都实现了锁原语,能够在多线程中直接使用,可以使用队列来实现线程间的同步。

  • Queue 模块中的常用方法:
    • Queue.qsize() 返回队列的大小
    • Queue.empty() 如果队列为空,返回True,反之False
    • Queue.full() 如果队列满了,返回True,反之False
    • Queue.full 与 maxsize 大小对应
    • Queue.get([block[, timeout]])获取队列,timeout等待时间
    • Queue.get_nowait() 相当Queue.get(False)
    • Queue.put(item) 写入队列,timeout等待时间
    • Queue.put_nowait(item) 相当Queue.put(item, False)
    • Queue.task_done() 在完成一项工作之后,Queue.task_done()函数向任 务已经完成的队列发送一个信号
    • Queue.join() 实际上意味着等到队列为空,再执行别的操作
import queue
import threading
import time

exitFlag = 0

class myThread (threading.Thread):
    def __init__(self, threadID, name, q):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name
        self.q = q
    def run(self):
        print ("开启线程:" + self.name)
        process_data(self.name, self.q)
        print ("退出线程:" + self.name)

def process_data(threadName, q):
    while not exitFlag:
        queueLock.acquire()
        if not workQueue.empty():
            data = q.get()
            queueLock.release()
            print ("%s processing %s" % (threadName, data))
        else:
            queueLock.release()
        time.sleep(1)

threadList = ["Thread-1", "Thread-2", "Thread-3"]
nameList = ["One", "Two", "Three", "Four", "Five"]
queueLock = threading.Lock()
workQueue = queue.Queue(10)
threads = []
threadID = 1

# 创建新线程
for tName in threadList:
    thread = myThread(threadID, tName, workQueue)
    thread.start()
    threads.append(thread)
    threadID += 1

# 填充队列
queueLock.acquire()
for word in nameList:
    workQueue.put(word)
queueLock.release()

# 等待队列清空
while not workQueue.empty():
    pass

# 通知线程是时候退出
exitFlag = 1

# 等待所有线程完成
for t in threads:
    t.join()
print ("退出主线程")

在这里插入图片描述

进程池

进程池的概念
进程池是一种用于管理多个进程的机制,它预先创建好一定数量的进程,将这些进程放在一个 “池子” 里,当有任务需要处理时,可以从进程池中获取一个空闲的进程来执行任务,任务执行完毕后,该进程并不会销毁,而是回到进程池中等待下一次被分配任务,这样可以避免频繁地创建和销毁进程带来的开销,提高系统的整体效率,尤其适用于需要处理大量短期任务的场景。

import multiprocessing

def task(num):
    """定义一个简单任务,这里只是打印传入的数字"""
    print(f"处理任务 {num}")

if __name{
    # 创建一个包含 4 个进程的进程池
    pool = multiprocessing.Pool(processes=4)
    tasks = list(range(10))
    # 使用进程池并行处理任务
    pool.map(task, tasks)
    # 关闭进程池,不再接受新任务
    pool.close()
    # 等待进程池中所有进程完成任务
    pool.join()
}
  • 首先通过 multiprocessing.Pool(processes=4) 创建了一个有 4 个进程的进程池。
  • 然后定义了一个简单的任务函数 task,这里只是简单地打印传入的参数。
  • 接着准备了一个任务列表 tasks,里面包含了从 0 到 9 共 10 个任务。
  • 使用 pool.map 方法将任务分配给进程池中的进程去执行,它会自动将任务分配到各个空闲进程上。
  • 之后调用 pool.close() 表示不再接受新的任务进入进程池,最后通过 pool.join() 等待进程池中所有进程都完成它们正在执行的任务。

进程间通信(IPC,Inter - Process Communication)

  • 定义
    进程间通信是指在不同进程之间进行数据交换或信号传递的机制。由于每个进程都有自己独立的地址空间,在操作系统的隔离保护下,进程之间不能直接访问对方的数据,因此需要特殊的通信方式来实现信息共享和协调工作。
  • 常见的进程间通信方式
    • 管道(Pipe)
    • 概念:管道是一种半双工的通信方式,数据只能单向流动。它就像一个连接两个进程的管道,一个进程向管道写入数据,另一个进程从管道读取数据。管道分为无名管道和有名管道。
    • 无名管道(Unix Pipe):
    • 通常用于具有亲缘关系(如父子进程)之间的通信。它在创建后会返回两个文件描述符,一个用于读,一个用于写。
      例如,在 Linux 系统下,父进程可以创建一个管道,然后通过fork函数创建子进程。父子进程可以通过管道来传递数据。
      代码示例(Python 中模拟无名管道通信):
import os
import sys

r, w = os.pipe()
pid = os.fork()
if pid:
    # 父进程
    os.close(w)  # 父进程关闭写端
    r = os.fdopen(r)
    print("父进程读取:", r.read())
    r.close()
else:
    # 子进程
    os.close(r)  # 子进程关闭读端
    w = os.fdopen(w, 'w')
    w.write("来自子进程的数据")
    w.close()
  • 消息队列(Message Queue)

  • 概念:消息队列是一个由消息的链表,存放在内核中并由消息队列标识符标识。进程可以向消息队列发送消息,也可以从消息队列中接收消息。消息队列克服了管道通信无格式、缓冲区大小受限等缺点。

    • 工作原理:
    • 发送消息时,进程将消息添加到消息队列的末尾;接收消息时,进程从消息队列的头部获取消息。消息队列中的消息通常具有一定的格式,如消息类型和消息内容。
    • 不同的操作系统对消息队列的实现有所不同,但基本原理相似。例如,在 Linux 系统中,使用msgget函数来创建或获取一个消息队列的标识符,msgsnd函数用于发送消息,msgrcv函数用于接收消息。
  • 共享内存(Shared Memory)

  • 概念:共享内存是最快的进程间通信方式,它允许多个进程访问同一块内存区域。就好像在不同的房子(进程)之间开辟了一个公共的储物间(共享内存区域),这些房子里的人(进程)都可以往这个储物间里放东西或者拿东西

    • 工作原理:
    • 操作系统为共享内存区域分配物理内存,并将这块内存映射到多个进程的虚拟地址空间中。这样,多个进程就可以通过读写这块共享内存来交换数据。不过,由于多个进程可以同时访问共享内存,需要使用同步机制(如信号量等)来避免数据冲突。
import mmap
import os
import sys

# 创建共享内存对象
size = 1024
shared_memory = mmap.mmap(-1, size, tagname="my_shared_memory")
pid = os.fork()
if pid:
    # 父进程
    # 向共享内存写入数据
    shared_memory.write(b"这是父进程写入的数据")
    shared_memory.seek(0)
    print("父进程读取:", shared_memory.read())
    shared_memory.close()
else:
    # 子进程
    shared_memory.seek(0)
    print("子进程读取:", shared_memory.read())
    # 向共享内存写入数据
    shared_memory.write(b"这是子进程写入的数据")
    shared_memory.close()
  • 信号量(Semaphore)
  • 概念:信号量是一个计数器,主要用于实现进程间的互斥与同步。它可以控制多个进程对共享资源的访问,确保在同一时刻只有一定数量的进程能够访问特定资源。
    • 工作原理:
    • 信号量有两种操作:P操作(也叫wait操作)和V操作(也叫signal操作)。P操作会将信号量的值减 1,如果信号量的值小于 0,则进程会被阻塞;V操作会将信号量的值加 1,如果信号量的值小于等于 0,则唤醒一个被阻塞的进程。
    • 例如,假设有一个共享资源(如打印机),可以使用信号量来控制多个进程对打印机的访问。信号量初始值为 1,表示打印机空闲。当一个进程要使用打印机时,它会执行P操作,信号量减 1 变为 0,表示打印机被占用。当进程使用完打印机后,执行V操作,信号量加 1 变为 1,表示打印机又空闲了。
import threading

# 创建信号量对象,初始值为3
semaphore = threading.Semaphore(3)

def worker():
    global semaphore
    # 获取信号量
    semaphore.acquire()
    print("线程 {} 获得资源".format(threading.current_thread().name))
    # 模拟工作过程
    import time
    time.sleep(2)
    print("线程 {} 释放资源".format(threading.current_thread().name))
    # 释放信号量
    semaphore.release()

threads = []
for i in range(5):
    t = threading.Thread(name="线程{}".format(i), target=worker)
    threads.append(t)
    t.start()

四、多线程

多线程概述

多线程是一种在单个进程内实现并发执行多个任务的编程技术。在一个进程中可以创建多个线程,这些线程共享进程的资源(如内存空间、文件描述符等),但又拥有各自独立的执行路径和局部变量等,可以并行或并发地执行不同的代码片段,从而提高程序的执行效率和响应能力。

多线程类似于同时执行多个不同程序,多线程运行有如下优点:

  • 使用线程可以把占据长时间的程序中的任务放到后台去处理。
  • 用户界面可以更加吸引人,比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度。
  • 程序的运行速度可能加快。
  • 在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,线程就比较有用了。在这种情况下我们可以释放一些珍贵的资源如内存占用等等。
  • 每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

每个线程都有他自己的一组CPU寄存器,称为线程的上下文,该上下文反映了线程上次运行该线程的CPU寄存器的状态。
指令指针和堆栈指针寄存器是线程上下文中两个最重要的寄存器,线程总是在进程得到上下文中运行的,这些地址都用于标志拥有线程的进程地址空间中的内存。

  • 线程可以被抢占(中断)。
  • 在其他线程正在运行时,线程可以暂时搁置(也称为睡眠) – 这就是线程的退让。

线程可以分为:

  • 内核线程:由操作系统内核创建和撤销。

  • 用户线程:不需要内核支持而在用户程序中实现的线程。

Python3 线程中常用的两个模块为:

  • _thread
  • threading(推荐使用)

thread 模块已被废弃。用户可以使用 threading 模块代替。所以,在 Python3 中不能再使用"thread" 模块。为了兼容性,Python3 将 thread 重命名为 “_thread”。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值