Python学习 Day10 进程与线程(一)

一、基本概念

深刻理解几个概念:

1.1 什么是进程

  1. 进程(Process)是操作系统资源分配和调度的基本单位,是操作系统结构的基础。
  2. 每个进程有自己的内存空间和系统资源,执行过程中保持独立。
  3. 进程是程序的实体,代表一次程序执行过程,一个进程可以包含多个线程。

1.2 什么是线程

  1. 线程(Thread)是操作系统的最小单位,是CPU资源分配和调度的基本单位。
  2. 线程包含在进程当中,同一个进程中的线程共享进程拥有的全部资源,又各自有自己独立的资源。
  3. 线程是进程中的一个执行单元,负责执行进程中的代码和指令;进程中的不同线程独立的执行不同的任务或者任务的一部分。
  4. 可以说,进程的执行是通过进程内线程的执行而实现的。进程的执行就是进程内线程协同执行的结果。

1.3 区分程序和进程

  1. 进程是程序运行产生的。当程序被加载到内存中并开始执行时,它就在操作系统中创建了一个进程。未运行的程序只是一个代码的集合。如果程序没有运行,将不会在操作系统中产生进程。
  2. 进程是程序运行的一个实例,包含了程序运行所需的各种资源,如程序、数据、堆栈、进程器等等,以及操作系统为管理该进程所需的各种信息,如进程状态、优先级、内存指针等。

1.4 区分进程和线程

        线程与进程的一个主要区别在于它们共享的资源。

  1. 进程是系统资源分配的基本单位。系统资源不止包括CPU,还包含文件、内存等资源。所以,每个进程拥有独立的地址空间、代码、数据等资源。这使得不同的进程在内存中相互隔离,保证了它们的稳定性和安全性。
  2. 线程是CPU资源分配的基本单位。因为线程是进程中的实际运作单位,一条线程指的是进程中一个单一顺序的控制流。一个进程中可以并发多个线程,每条线程并行执行不同的任务,它们共享进程的内存空间、文件等资源。尽管线程共享进程资源,但它们仍然需要独立的CPU时间片来执行。CPU资源的分配是由操作系统来管理的,它负责将CPU时间分配给不同的线程,以实现并发执行的效果。操作系统通过调度算法来确定哪个线程应该在下一个时间片内获得CPU的执行权。因此,线程在共享进程资源的同时,还涉及到CPU资源的分配。这种共享与分配的结合使得多线程编程能够充分利用多核CPU的并行处理能力,提高程序的执行效率。

1.5 IO任务和CPU任务

        IO型任务和CPU型任务的主要区别体现在任务执行过程中的资源占用和瓶颈所在

IO任务:

  1. Input/Output-intensive tasks,主要依赖IO操作。在执行过程中,这类任务会进行大量的读取和写入操作,而CPU的计算需求较小。所以,IO操作的速度和效率通常是这类任务的瓶颈,而CPU的处理能力往往有闲置。
  2. 执行时间主要花在IO操作上,通常会导致进程或线程的堵塞。
  3. 优化策略常在于提高IO操作的效率和并发性,以减少等待时间和系统资源损耗。常见的优化策略有:使用异步IO、多线程/多进程、缓存数据、使用非阻塞IO等

CPU任务:

  1. CPU-intensive tasks,主要依赖于CPU的处理能力。这类任务任务通常涉及大量计算,逻辑判断,数据处理等,如科学计算,图像处理,加密解密等。这类任务中,CPU的处理能力是主要能力,而IO操作相对较少。
  2. 执行时间主要花在CPU计算上,会持续占用系统的CPU资源。优化策略主要集中在提高CPU计算的效率和并行性上,以减少计算时间和资源占用。常见的优化策略有:并行计算、优化算法和数据结构、采用硬件加速和分布式计算等。

1.6 一些其他概念

1.6.1 操作系统

操作系统:

  1. Operating System,操作系统是一种内置程序,用来协作计算机的各种硬件与用户进行交互。它是计算机最基本最重要的软件,负责管理和控制计算机硬件和软件,确保它们得到合理的分配和使用。
  2. 常见的操作系统有Windows,macOS,Linux

操作系统管理的主要资源:

1、CPU:计算机的主要部件,负责执行程序中的指令;操作系统通过进程管理和线程调度等机制,合理分配CPU时间片给不同的程序或任务,确保它们能够公平且有效地运行

2、主存储器:用于存储数据和程序的临时存储空间;操作系统通过存储管理功能,负责内存的分配、回收和保护,确保每个程序能安全的访问所需的内存空间。

3、外部设备:包括各种IO设备,如键盘、显示器、打印机等;操作系统通过设备管理功能,管理设备的分配、传输控制和独立性。

4、数据:程序处理的对象,如文件、数据库;操作系统通过文件管理的功能,提供数据的存储、检索和保护机制,确保数据的安全性和完整性。

5、程序:计算机任务的指令集合;操作系统通过进程管理和作业管理等功能,负责程序的加载、终止和执行,以及程序之间的通信和同步。

1.6.2 CPU和时间片

CPU:

  1. Central Processing Unit,即中央处理器,是计算机系统运算和控制核心,是信息处理、程序运行的最终执行单元。
  2. 它主要负责执行计算机指令和处理数据,包括控制器和运算器等多个部分。CPU的性能直接影响到计算机的整体性能,如:处理速度,计算时间等。

时间片:

  1. Timeslice,是系统管理和调度进程、线程的一种机制,它规定了进程、线程在CPU上运行的最大连续时间。
  2. 当进程、线程的时间片用完时,操作系统会暂停其执行,将其放入就绪队列的末尾,并调度下一个进程或线程执行。通过这种方式,操作系统能够公平地分配CPU时间,确保每个进程或线程都有机会执行,从而实现并发执行的效果
  3. 时间片的长度根据不同的操作系统进行调整。时间片较短可以提高系统响应性,使得进程、线程之间切换的更加频繁,但可能增加上下文切换开销。时间片较长可以减少上下文切换的开销,提高执行效率,但可能导致某些进程、线程要等待更长时间才能执行。

同一时间单核CPU只能运行一个进程或者一个线程,多进程和多线程实际是操作系统通过调度算法,分配CPU时间片给不同的程序或任务来实现的,这个称为时间片轮转(Time-slicing)调度策略。因为时间片切换较快,我们体感是多个任务在同一时间运行,即并发执行效果。

1.6.3 多核CPU和并行计算

多核CPU:

  1. 多核是指在一个CPU中集成多个计算引擎(或称为核心、内核)。每个核心都是一个独立的处理单元,能够执行指令并处理数据。
  2. 多核技术的出现旨在通过并行处理来提高计算机的性能和效率。通过将不同任务分配到多个核心上并行执行,可以显著提升计算机的计算速度,即每个核心可以单独运行一个线程或进程,从而实现物理上的并发执行。

并行计算:

  1. Parallel Computing,并行计算是指同时使用多种计算资源(如多个处理器核心、计算机集群或分布式网络)来执行一个或多个程序任务。其目标是将一个大型问题分解成多个较小、更容易管理的部分,然后将这些部分分配给不同的计算资源来并行处理。这样,整体任务的完成速度将显著提高,因为多个部分可以同时进行计算,而不是等待一个部分完成后才开始下一个部分。
  2. 并行计算可以大致分为两类:任务并行和数据并行。任务并行是将不同的任务分配给不同的处理器,而数据并行则是将数据集的不同部分分配给多个处理器进行处理。在分布式系统中,这些计算资源可能跨越多个物理节点,通过网络进行通信和协作。
  3. 多线程是并行计算的一种实现方式,如在共享内存并行计算中使用多线程技术,但多线程并不总是并行计算。多线程是否能够实现真正的并行计算取决于具体的硬件环境和操作系统的支持。在单核处理器上,多线程实际上是通过时间片轮转的方式交替执行的,而不是同时执行。这种情况下,多线程只能实现并发执行,而不是并行执行。只有在多核处理器或分布式系统中,多个线程才有可能真正并行执行。

1.6.4 并发执行和并行执行

  1. 并发执行(Concurrency)是指在同一时间段内同时执行多个独立的任务或子任务。这种执行方式是基于分时片段的,使得这些任务在感觉上像是同时执行的。这主要是为了提高系统的吞吐量和响应速度。在并发执行中,尽管从宏观上看多个任务似乎同时执行,但从微观角度看,实际上是CPU在多个线程之间快速地交替执行。这种交替执行往往在短时间内完成,给用户一种同时执行多个任务的错觉。

  2. 而并行执行(Parallelism)则是指多个任务或线程在同一时刻真正地同时执行。这通常发生在多核处理器或分布式系统中,其中每个核心或处理器都可以独立地执行一个任务。在并行执行中,线程间不会抢占CPU资源,因为每个线程都在其自己的处理器或核心上运行2。

  3. 总结来说,并发执行是宏观上同时执行多个任务,而微观上是CPU在多个线程之间快速交替执行;而并行执行则是多个任务或线程在同一时刻真正地同时执行,这通常发生在多核处理器或分布式系统中

  4. 我们今天主要学习并发执行

二、多进程和多线程

2.1 多进程

  1. Python中的多进程通过multiprocessing模块实现
  2. 不使用多进程,模拟下载两个文件:
import random
import time


def download(filename):
    t = random.randint(1, 5)
    print('准备下载%s,预计耗时%d秒。' % (filename, t))
    time.sleep(t)
    print('%s下载完成' % filename)


def main():
    start = time.time()
    download('卧虎藏龙')
    download('东邪西毒')
    end = time.time()
    print('文件下载总共花了%.2f秒' % (end-start))


if __name__ == '__main__':
    main()
"""
准备下载卧虎藏龙,预计耗时5秒。
卧虎藏龙下载完成
准备下载东邪西毒,预计耗时4秒。
东邪西毒下载完成
文件下载总共花了9.00秒
"""
  1. 使用multiprocessing的Process类创建两个进程,start()启动进程,join()等待进程执行结束
  2. 不知道为什么我的电脑会报错,所以此处没有结果,错误信息:OSError: [Errno 22] Invalid argument: 'C:\\Users\\li\\PycharmProjects\\pythonProject\\<input>'
import random
import time
from multiprocessing import Process
from os import getpid


def download(filename):
    print('启动下载进程,进程号:%d' % getpid())
    t = random.randint(1, 5)
    print('下载%s,耗时%d秒。' % (filename, t))
    time.sleep(t)
    print('%s下载完成' % filename)


def main():
    start = time.time()
    p1 = Process(target=download, args=('卧虎藏龙',))    # 创建一个Process对象
    p2 = Process(target=download, args=('东邪西毒',))
    p1.start()    # start()方法启动进程
    p2.start()
    p1.join()    # join()等待进程结束
    p2.join()
    end = time.time()
    print('文件下载总共花了%.2f秒' % (end-start))


if __name__ == '__main__':
    main()

2.2 多线程

  1. 多线程的创建方法和多进程类似,通常使用Threading模块的Thread类来创建进程
    import random
    import time
    from threading import Thread
    
    
    def download(filename):
        t = random.randint(1, 5)
        print('开始下载%s,预计耗时%d秒。' % (filename, t))
        time.sleep(t)
        print('%s下载完成' % filename)
    
    
    def main():
        start = time.time()
        p1 = Thread(target=download, args=('卧虎藏龙',))    # 创建一个Thread对象
        p2 = Thread(target=download, args=('东邪西毒',))
        p1.start()    # start()方法启动线程
        p2.start()
        p1.join()    # join()等待线程结束
        p2.join()
        end = time.time()
        print('文件下载总共花了%.2f秒' % (end-start))
    
    
    if __name__ == '__main__':
        main()
    """
    开始下载卧虎藏龙,预计耗时4秒。
    开始下载东邪西毒,预计耗时4秒。
    卧虎藏龙下载完成
    东邪西毒下载完成
    文件下载总共花了4.00秒
    """

    可以看到,我们模拟的多线程下载使文件下载时间大幅度缩短。

  2. 因为Thread是一个类,根据类的继承,我们可以通过继承Thread创建自定义的线程类,来实现多线程任务。

  3. 常见的Thread类方法:

  • start() 方法:启动线程,使其进入就绪状态。不能直接调用一个线程的run()方法,因为这样会在当前线程中执行它,使用start()方法调用则是在新的线程中执行run()方法的内容
  • run() 方法:线程的主体部分,包含了线程需要执行的任务代码。当使用start()启动线程时,run()方法中的代码会在新线程中执行。可以通过重写run()方法来定义线程的行为。
  • join() 方法:等待该线程终止。当前线程调用另一个线程的join()方法时,当前线程会阻塞(即暂停执行),直到被调用的线程执行完毕。在需要等待一个线程完成其任务再继续当前线程的执行时非常有用。
  • start(),run(),join()方法在进程和线程中都有类似的作用,但它们的操作对象和执行环境是不同的。进程是操作系统级别的并发执行单位,而线程是进程内部的执行流。
  • Process和Thread的继承类同样可以调用和重写父类的方法。
import random
import time
from threading import Thread


# 创建Thread的继承类
class Download(Thread):

    def __init__(self, filename):
        super().__init__()
        self._filename = filename

    def run(self):
        t = random.randint(1, 5)
        print('开始下载%s,预计耗时%d秒。' % (self._filename, t))
        time.sleep(t)
        print('%s下载完成' % self._filename)


def main():
    start = time.time()
    p1 = Download('卧虎藏龙')    # 创建一个Download线程类对象
    p2 = Download('东邪西毒')
    p1.start()    # start()方法启动线程
    p2.start()
    p1.join()    # join()等待线程结束
    p2.join()
    end = time.time()
    print('文件下载总共花了%.2f秒' % (end-start))


if __name__ == '__main__':
    main()
"""
开始下载卧虎藏龙,预计耗时4秒。
开始下载东邪西毒,预计耗时3秒。
东邪西毒下载完成
卧虎藏龙下载完成
文件下载总共花了4.00秒
"""
"""
如果先后下载需要使用7秒,多线程实际使用4秒。可以看到,多线程大大缩减了下载的总时间。
"""

2.3 临界资源和锁 

  1. 当我们使用多线程完成任务时,多个线程可能会同时访问和修改同一个数据或者共享资源,导致数据不一致,产生不可预测的结果,这个可以被同时访问和修改的数据或资源被称为临界资源(Critical Resource)。
  2. 这时线程访问和修改的先后和是否会同时使用这个资源无法控制,将发生竞态条件(Race Condition)。竞态条件指:程序的执行结果依赖进程和线程的执行顺序或时机,进而可能导致程序崩溃,数据损坏,不可预测的后果。
  3. 为了保护临界资源,我们通常通过某种同步机制来保护临界资源,比如:锁(Locks)。锁可以保护临界资源在同一时刻只被一个进程或线程访问。
  4. 当一个线程或进程获得锁时,其他尝试访问该资源的线程或进程将被阻塞,直到锁被释放。
  5. 其中,线程想获得锁可以从threading模块导入Lock,进程想获得锁从multiprocessing模块导入Lock。获得锁的方法:Lock().acquire(),访问完临界资源后需要释放锁,释放锁的方法:Lock().release()。
  6. 下面是一个不使用锁访问临界资源self._balance的案例,self._balance之所以是临界资源,是因为我们使用多线程操纵100个账户同时向balance账户转账时,很多可能多个线程是同时转账的,那么访问到的self._balance可能不是其他账户已经转完的,而是同时访问到的。那访问的将都是self._balance=0,所以最终得到的加总值远小于100。
    from time import sleep, time
    from threading import Thread
    
    
    class Account:
    
        def __init__(self):
            self._balance = 0
    
        @property
        def balance(self):
            return self._balance
    
        def deposit(self, money):
            """存钱"""
            new_balance = self._balance + money
            # 银行系统操作存款需要0.1秒
            sleep(0.1)
            self._balance = new_balance
    
    
    class Transfer(Thread):
    
        def __init__(self, account, money):
            super().__init__()
            self._account = account # 转入的账户,是Account的实例,可以用self._account直接调用Account类的方法
            self._money = money # 转入的金额
    
        def run(self):
            self._account.deposit(self._money)
    
    
    def main():
        account = Account()
        threads = []
        start = time()
        for i in range(100):
            t = Transfer(account, 1)
            threads.append(t)
            t.start()
        for t in threads:
            t.join()
        end = time()
        print('银行账户余额:%d\n全部线程运行完毕,花费%.2fs' % (account.balance, (end - start)))
    
    
    if __name__ == '__main__':
        main()
    """
    银行账户余额:1
    全部线程运行完毕,花费0.14s
    """

可以看到,Lock保护临界变量后,数值是正确的。但多线程任务运行时间变长了,这是正常的。一方面,任何时刻只有一个进程能执行临界区的代码,其他进程会被阻塞,另一方面,锁获取和锁释放也会有一些时间开销。

  • 42
    点赞
  • 56
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值