浅析Python的进程、线程与协程(上)

简介: 进程进程是指在系统中正在运行的一个应用程序,是CPU的最小工作单元。进程有就绪、运行、阻塞、创建和退出五种状态。其中,运行中的三种状态:就绪、运行、阻塞。创建和退出是描述产生和释放的状态。

进程

进程是指在系统中正在运行的一个应用程序,是CPU的最小工作单元。

进程有就绪、运行、阻塞、创建和退出五种状态。其中,运行中的三种状态:就绪、运行、阻塞。创建和退出是描述产生和释放的状态。

网络异常,图片无法展示
|


进程的特点

  • 动态性:进程是程序的一次执行过程,动态产生,动态消亡。
  • 独立性:进程是一个能独立运行的基本单元。是系统分配资源与调度的基本单元。
  • 并发性:任何进程都可以与其他进程并发执行。
  • 结构性:进程由程序、数据和进程控制块三部分组成。

缺点

无法即时完成的任务带来大量的上下文切换代价与时间代价。

  • 进程的上下文:当一个进程在执行时,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容被称为该进程的上下文。
  • 上下文切换:当内核需要切换到另一个进程时,它需要保存当前进程的所有状态,即保存当前进程的上下文,以便在再次执行该进程时,能够得到切换时的状态并执行下去。


线程

线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。

一个进程可以有一个或多个线程,同一进程中的多个线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。

但同一进程中的多个线程有各自的调用栈和线程本地存储。

说明:

进程才是CPU的最小作业单元(最小资源管理单元),开更多的线程不会导致本进程得到更多CPU的青睐,而是提高了CPU在本进程使用时间段的利用率。但是,本进程使用时间段的各线程优先级是用户可以自己设置的。

进程与线程的区别

  1. 进程是CPU资源分配的基本单位,线程是独立运行和独立调度的基本单位(CPU上真正运行的是线程)。
  2. 进程拥有自己的资源空间,一个进程包含若干个线程,线程与CPU资源分配无关,多个线程共享同一进程内的资源。
  3. 线程的调度与切换比进程快很多。
  4. CPU密集型代码(各种循环处理、计算等等):使用多进程。IO密集型代码(文件处理、网络爬虫等):使用多线程。

关于IO密集型和CPU密集型,请参见:一分钟明白IO密集型与CPU密集型的区别

虽然线程大幅的提高了CPU的效率,且能够设置一定的优先级,但是线程的资源片分配还是由CPU来管理的。

那么能不能人为管理线程的资源分配(切换)呢?协程在语言层面实现了这一点。


协程

协程(Coroutine,又称微线程,纤程)是一种比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制。

如同一个进程可以有很多线程一样,一个线程可以有很多协程。

但是,协程不是被操作系统所管理的,没有改变CPU最小执行单元是线程,协程是完全由程序所控制的(用户态执行),不会产生上下文切换。

协程具有高并发、高扩展性、低成本的特点,一个CPU支持上万的协程都不是问题。所以,很适合用于高并发处理。通常,协程可以处理IO密集型程序的效率问题,但是处理CPU密集型不是它的长处,如要充分发挥CPU利用率可以结合多进程+协程。


优点

  1. 无需线程上下文切换的开销
  2. 无需原子操作锁定及同步的开销
  3. 方便切换控制流,简化编程模型

缺点

  1. 无法利用多核资源:协程的本质是个单线程,它不能同时在多个核用上。协程需要和进程配合才能运行在多CPU上。当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
  2. 进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序。


一些主流语言对协程的支持情况

  1. Python的yield/send,当协程执行到yield关键字时,会暂停在那一行,等到主线程调用send方法发送了数据,协程才会接到数据继续执行。
  2. Lua从5.0版本开始使用协程,通过扩展库coroutine来实现。
  3. Go语言对协程的实现非常强大而简洁,可以轻松创建成百上千个协程并发执行。
  4. Java语言并没有对协程的原生支持,但是某些开源框架模拟出了协程的功能,如Kilim


并发与并行

并发:在操作系统中,某一时间段,几个程序在同一个CPU上运行,但在任意一个时间点上,只有一个程序在该CPU上运行。

补充说明:

当有多个线程时,如果系统只有一个CPU,那么CPU不可能真正同时进行多个线程,CPU的运行时间会被划分成若干个时间段,每个时间段分配给各个线程去执行,一个时间段里某个线程运行时,其他线程处于挂起状态,这就是并发。

并发解决了程序排队等待的问题,如果一个程序发生阻塞,其他程序仍然可以正常执行。

并行:当操作系统有多个CPU时,一个CPU处理A线程,另一个CPU处理B线程,两个线程互相不抢占CPU资源,可以同时进行,这种方式成为并行。


并发与并行的区别

并发只是在宏观上给人感觉有多个程序在同时运行,但在实际的单CPU系统中,每一时刻只有一个程序在运行,微观上这些程序是分时交替执行。

在多CPU系统中,将这些并发执行的程序分配到不同的CPU上处理,每个CPU用来处理一个程序,这样多个程序便可以实现同时执行。


Python多线程

在Python多线程下,每个线程的执行方式如下:

  1. 获取GIL
  2. 执行代码直到sleep或者是python虚拟机将其挂起。
  3. 释放GIL

可见,某个线程想要执行,必须先拿到GIL,我们可以把GIL看作是“通行证”,并且在一个python进程中,GIL只有一个。拿不到通行证的线程,就不允许进入CPU执行。

因此,同一时刻,只有一个线程在运行,其它线程只能等待,即使是多核CPU,也没办法让多个线程「并行」地同时执行代码,只能是交替执行,因为多线程涉及到上线文切换、锁机制处理(获取锁,释放锁等),所以,在CPU密集型应用中多线程执行不快反慢。

补充说明:GIL

在 CPython 解释器(Python语言的主流解释器)中,有一把全局解释锁GIL(Global Interpreter Lock),某个线程须先拿到GIL才允许进入CPU执行。

什么时候 GIL 被释放呢?

当一个线程遇到 I/O 任务时,将释放GIL。

计算密集型(CPU-bound)线程执行 100 次解释器的计步(ticks)时(计步可粗略看作 Python 虚拟机的指令),也会释放 GIL。在Python3.x中,GIL不使用ticks计数,改为使用计时器(执行时间达到阈值后,当前线程释放GIL)。

特别提示:GIL只在CPython中才有,因为CPython调用的是c语言的原生线程,所以他不能直接操作cpu,只能利用GIL保证同一时间只能有一个线程拿到数据。而在PyPy和Jython中是没有GIL的。

示例代码如下:


1. 普通线程创建:

import threading
import time
def run(n):
    print(f"task {n} ")
    time.sleep(1)
    print(f"task {n} -> 2s ")
    time.sleep(1)
    print(f"task {n} -> 1s ")
    time.sleep(1)
    print(f"task {n} -> 0s ")
    time.sleep(1)
if __name__ == '__main__':
    t1 = threading.Thread(target=run, args=("t1",))
    t2 = threading.Thread(target=run, args=("t2",))
    t1.start()
    t2.start()
复制代码


运行结果:

task t1 task t2 
task t2 -> 2s 
task t1 -> 2s 
task t1 -> 1s 
task t2 -> 1s 
task t1 -> 0s 
task t2 -> 0s 
复制代码


2. 自定义线程创建:

import threading
import time
class MyThread(threading.Thread):
    def __init__(self, n):
        super(MyThread, self).__init__()  # 重构run函数必须要写
        self.n = n
    def run(self):
        print(f"task: {self.n}")
        time.sleep(1)
        print(f"task: {self.n} -> 2s")
        time.sleep(1)
        print(f"task: {self.n} -> 1s")
        time.sleep(1)
        print(f"task: {self.n} -> 0s")
        time.sleep(1)
if __name__ == "__main__":
    t1 = MyThread("t1")
    t2 = MyThread("t2")
    t1.start()
    t2.start()
复制代码


运行结果:

task: t1
task: t2
task: t1 -> 2s
task: t2 -> 2s
task: t2 -> 1s
task: t1 -> 1s
task: t2 -> 0s
task: t1 -> 0s
复制代码


Python多进程

Python要充分利用多核CPU,就用多进程。

原因是:每个进程有各自独立的GIL,互不干扰,这样就可以真正意义上的并行执行,所以在Python中,多进程的执行效率优于多线程(仅仅针对多核CPU而言)。

示例代码如下:

1. 直接调用

import time
import random
from multiprocessing import Process
def run(name):
    print('%s runing' % name)
    time.sleep(random.randrange(1, 5))
    print('%s running end' % name)
if __name__ == '__main__':
    p1 = Process(target=run, args=('anne',))  # 必须加,号
    p2 = Process(target=run, args=('alice',))
    p3 = Process(target=run, args=('biantai',))
    p4 = Process(target=run, args=('haha',))
    p1.start()
    p2.start()
    p3.start()
    p4.start()
    print('主线程')
复制代码


运行结果:

主线程
anne runing
alice runing
biantai runing
haha runing
biantai running end
haha running end
anne running end
alice running end
复制代码


2. 继承式调用

import time
import random
from multiprocessing import Process
class Run(Process):
    def __init__(self,name):
        super().__init__()
        self.name=name
    def run(self):
        print('%s runing' %self.name)
        time.sleep(random.randrange(1,5))
        print('%s runing end' %self.name)
if __name__ == '__main__':
    p1 = Run('anne')
    p2 = Run('alex')
    p3 = Run('ab')
    p4 = Run('hey')
    p1.start()  # start会自动调用run
    p2.start()
    p3.start()
    p4.start()
    print('主线程')
复制代码


运行结果:

主线程
anne runing
alex runing
ab runing
hey runing
anne runing end
alex runing end
hey runing end
ab runing end
复制代码


相关文章
|
10天前
|
并行计算 数据处理 调度
Python中的并发编程:探索多线程与多进程的奥秘####
本文深入探讨了Python中并发编程的两种主要方式——多线程与多进程,通过对比分析它们的工作原理、适用场景及性能差异,揭示了在不同应用需求下如何合理选择并发模型。文章首先简述了并发编程的基本概念,随后详细阐述了Python中多线程与多进程的实现机制,包括GIL(全局解释器锁)对多线程的影响以及多进程的独立内存空间特性。最后,通过实例演示了如何在Python项目中有效利用多线程和多进程提升程序性能。 ####
|
15天前
|
Linux 调度 C语言
深入理解操作系统:进程和线程的管理
【10月更文挑战第32天】本文旨在通过浅显易懂的语言和实际代码示例,带领读者探索操作系统中进程与线程的奥秘。我们将从基础知识出发,逐步深入到它们在操作系统中的实现和管理机制,最终通过实践加深对这一核心概念的理解。无论你是编程新手还是希望复习相关知识的资深开发者,这篇文章都将为你提供有价值的见解。
|
12天前
|
Java
java小知识—进程和线程
进程 进程是程序的一次执行过程,是系统运行的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程 线程,与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比
23 1
|
17天前
深入理解操作系统:进程与线程的管理
【10月更文挑战第30天】操作系统是计算机系统的核心,它负责管理计算机硬件资源,为应用程序提供基础服务。本文将深入探讨操作系统中进程和线程的概念、区别以及它们在资源管理中的作用。通过本文的学习,读者将能够更好地理解操作系统的工作原理,并掌握进程和线程的管理技巧。
34 2
|
18天前
|
调度 Python
深入浅出操作系统:进程与线程的奥秘
【10月更文挑战第28天】在数字世界的幕后,操作系统悄无声息地扮演着关键角色。本文将拨开迷雾,深入探讨操作系统中的两个基本概念——进程和线程。我们将通过生动的比喻和直观的解释,揭示它们之间的差异与联系,并展示如何在实际应用中灵活运用这些知识。准备好了吗?让我们开始这段揭秘之旅!
|
22天前
|
Java Unix 调度
python多线程!
本文介绍了线程的基本概念、多线程技术、线程的创建与管理、线程间的通信与同步机制,以及线程池和队列模块的使用。文章详细讲解了如何使用 `_thread` 和 `threading` 模块创建和管理线程,介绍了线程锁 `Lock` 的作用和使用方法,解决了多线程环境下的数据共享问题。此外,还介绍了 `Timer` 定时器和 `ThreadPoolExecutor` 线程池的使用,最后通过一个具体的案例展示了如何使用多线程爬取电影票房数据。文章还对比了进程和线程的优缺点,并讨论了计算密集型和IO密集型任务的适用场景。
44 4
|
22天前
|
调度 iOS开发 MacOS
python多进程一文够了!!!
本文介绍了高效编程中的多任务原理及其在Python中的实现。主要内容包括多任务的概念、单核和多核CPU的多任务实现、并发与并行的区别、多任务的实现方式(多进程、多线程、协程等)。详细讲解了进程的概念、使用方法、全局变量在多个子进程中的共享问题、启动大量子进程的方法、进程间通信(队列、字典、列表共享)、生产者消费者模型的实现,以及一个实际案例——抓取斗图网站的图片。通过这些内容,读者可以深入理解多任务编程的原理和实践技巧。
46 1
|
4天前
|
数据采集 Java Python
爬取小说资源的Python实践:从单线程到多线程的效率飞跃
本文介绍了一种使用Python从笔趣阁网站爬取小说内容的方法,并通过引入多线程技术大幅提高了下载效率。文章首先概述了环境准备,包括所需安装的库,然后详细描述了爬虫程序的设计与实现过程,包括发送HTTP请求、解析HTML文档、提取章节链接及多线程下载等步骤。最后,强调了性能优化的重要性,并提醒读者遵守相关法律法规。
28 0
下一篇
无影云桌面