Python进程、线程、协程

一、进程、线程、协程–概念

0、进程与线程的历史

我们都知道计算机是由硬件和软件组成的。硬件中的CPU是计算机的核心,它承担计算机的所有任务。
操作系统是运行在硬件之上的软件,是计算机的管理者,它负责资源的管理和分配、任务的调度。
程序是运行在系统上的具有某种功能的软件,比如说浏览器,音乐播放器等。 每次执行程序的时候,都会完成一定的功能,比如说浏览器帮我们打开网页,为了保证其独立性,就需要一个专门的管理和控制执行程序的数据结构——进程控制块。
进程就是一个程序在一个数据集上的一次动态执行过程。 进程一般由程序、数据集、进程控制块三部分组成。我们编写的程序用来描述进程要完成哪些功能以及如何完成;数据集则是程序在执行过程中所需要使用的资源;进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。

在早期的操作系统里,计算机只有一个核心,进程执行程序的最小单位,任务调度采用时间片轮转的抢占式方式进行进程调度。每个进程都有各自的一块独立的内存,保证进程彼此间的内存地址空间的隔离。 随着计算机技术的发展,进程出现了很多弊端,一是进程的创建、撤销和切换的开销比较大,二是由于对称多处理机(对称多处理机(SymmetricalMulti-Processing)又叫SMP,是指在一个计算机上汇集了一组处理器(多CPU),各CPU之间共享内存子系统以及总线结构)的出现,可以满足多个运行单位,而多进程并行开销过大。 这个时候就引入了线程的概念。 线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、程序计数器、寄存器集合 和堆栈共同组成。线程的引入减小了程序并发执行时的开销,提高了操作系统的并发性能。 线程没有自己的系统资源,只拥有在运行时必不可少的资源。但线程可以与同属与同一进程的其他线程共享进程所拥有的其他资源。

1、进程(Process)

进程是资源分配的基本单位,通俗理解:一个正在运行的程序就是一个进程。例如:正在运行的qq , 微信等 他们都是一个进程。

  • 是操作系统进行资源分配和调度运行的基本单位
  • 每个进程都是独立的,有自己独立的地址空间、内存、数据栈
  • 进程之间可以相互通信

2、线程(Thread)

线程是进程的实体,是CPU调度和分配的最小单元,线程必须依托进程存活,线程无法给予公平执行时间,会被其他线程抢占。

线程是程序执行的最小单位 , 实际上进程只负责分配资源 , 而利用这些资源执行程序的是线程 , 也就说进程是线程的容器 , 一个进程中最少有一个线程来负责执行程序 。同时线程自己不拥有系统资源,只需要一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源 。这就像通过一个QQ软件(一个进程)打开两个窗口(两个线程)跟两个人聊天一样 , 实现多任务的同时也节省了资源。

多线程是在一个进程内运行,共享进程的内存空间,通讯效率较高、开销较小。但缺点是:因为python底层的GIL锁(global interpeter lock),python的底层的同步锁太多导致多线程被同步很多次,搞成并发效果不佳。

  • 同一进程所产生的线程共享同一内存空间,共享进程所拥有的全部资源
  • 当进程退出时该进程所产生的线程都会被强制退出并清除

3、协程(Coroutine)

比线程更小的执行单元,也被成为微线程。单个线程上执行多个任务,等待、切换执行任务,线程内切换没有切换开销,执行效率高。

线程是在程序层面操作的,可以控制遇到阻塞的时候候程序该干什么,是用户态执行的轻量级编程模型,由单一线程内部发出控制信号进行调度,而非受到操作系统管理。
协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。

4、线程和进程的区别

  1. 进程是资源分配的最小单位,线程是程序执行的最小单位。
  2. 进程之间的内存空间、数据是独立的,同一个进程中的线程共享同一内存空间和数据。

5、线程和进程的优劣

  1. 线程执行开销小,但不利于资源的管理和保护;而进程正相反。
  2. 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信是以通信的方式进行。
  3. 多进程程序更为安全,多线程程序中有一个线程出现问题,整个进程也就死掉了,而多进程的程序保障了一个进程死掉的时候不会影响到另外的一个进程。
  4. 对主线程的修改可能会影响其他线程的行为,但是父进程的修改(除了删除以外)不会影响其他子进程。
  5. 同一个进程的线程之间可以直接通信,但是进程之间的交流需要借助中间代理来实现。
  6. 进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行

6、多进程、多线程、协程的使用场景

多进程:适合CPU密集运算,大部分时间花在计算的场景
多线程、多协程:适合IO密集型(网络IO、磁盘IO、数据库IO),大部分时间花在传输的场景

二、多任务、并行、并发、GIL锁

1、多任务

多任务是指在同一时间内执行多个任务。例如: 现在电脑安装的操作系统都是多任务操作系统。
在代码里,一个程序函数或方法就是一个任务,平时我们写的程序几乎都是单任务的,也就是说一个函数或者方法执行完成 , 另外一个函数或者方法才能执行。多任务就是多个程序函数或方法同时执行。
多任务分为以下两种:
计算密集型任务:这类任务主要占用cpu资源又叫cpu密集型任务。主要特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等。这种任务虽然也可以用多任务完成,但是当任务过多时(超过cpu的核数),花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。
IO密集型任务:涉及到网络、磁盘IO的任务都是IO密集型任务,比如网络传输、数据库等调用、爬虫等,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(存在阻塞),99%的时间花费在IO上(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务。

2、多任务的两种表现形式:并行和并发

并行处理是指计算机系统中能同时执行两个或多个任务的计算方法,并行处理可同时工作于同一程序的不同方面。对于多核cpu处理多任务,操作系统会给cpu的每个内核安排一个执行的任务,多个内核是真正的一起同时执行多个任务。

并发处理是同一时间段内有几个程序都在一个cpu中处于运行状态,但任一时刻只有一个程序在cpu上运行。原理是在一段时间内交替去执行多个任务。

并发的重点在于有处理多个任务的能力,不一定要同时,交替执行(伪并行);而并行的重点在于就是有同时处理多个任务的能力。

相对于所有语言来说的,Python的特殊之处在于Python有一把GIL锁,这把锁限制了同一时间内一个进程只能有一个线程能使用cpu。

3、GIL锁(未全)

全局解释器锁(global interpreter lock),某个线程想要执行,必须先拿到 GIL,我们可以把 GIL 看作是“通行证”,并且在一个 Python 进程中,GIL 只有一个。拿不到通行证的线程,就不允许进入 CPU 执行。Python 中,无论是单核还是多核,同时只能由一个线程在执行。其根源是 GIL 的存在。
每次释放 GIL锁,线程进行锁竞争、切换线程,会消耗资源。这就导致打印线程执行时长,会发现耗时更长的原因。

并且由于 GIL 锁存在,Python 里一个进程永远只能同时执行一个线程(拿到 GIL 的线程才能执行),这就是为什么在多核CPU上,Python 的多线程效率并不高的根本原因。

三、进程间的通信方式

  • 管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
  • 有名管道 (named pipe) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
  • 信号量( semophore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  • 消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  • 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
  • 共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
  • 套接字( socket ) : 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。

四、多进程

4.1 未使用多进程:

# hello.py
def func_a():
    print('任务A')

def func_b():
    print('任务B')

func_a()
func_b()

在这里插入图片描述

程序运行时会默认创建一个主进程。
一旦运行hello.py这个程序 , 按照代码的执行顺序 , func_a函数执行完毕后才能执行func_b函数 . 如果可以让func_a和func_b同时运行 , 显然执行hello.py这个程序的效率会大大提升 .

4.2 多进程完成多任务:

在这里插入图片描述

import multiprocessing  # 导入进程包
进程对象 = multiprocessing.Process(target=任务名, group, name, args, kwargs)  # 通过进程类创建进程对象 
进程对象.start()        # 启动进程执行任务

参数说明:

参数名说明
target执行的目标任务名,这里指的是函数名(方法名)
name进程名,通常不需要使用,默认为None。
group进程组,通常不需要使用,默认为None。
args指定传递给目标函数的参数元组。args=(1,2,‘anne’,)
kwargs指定传递给目标函数的关键字参数字典。kwargs={‘name’:‘anne’,‘age’:18}

4.3 多进程创建与启动

import multiprocessing
import time

def music():
    for i in range(3):
        print('听音乐...')
        time.sleep(0.2)

def coding():
    for i in range(3):
        print('敲代码...')
        time.sleep(0.2)

if __name__ == '__main__':
    music_process = multiprocessing.Process(target=music)
    coding_process = multiprocessing.Process(target=coding)

    music_process.start()
    coding_process.start()
# 运行结果:
听音乐...
敲代码...
听音乐...
敲代码...
听音乐...
敲代码...

代码通过multiprocessing.Process()方法创建了两个进程music_process和coding_process,
通过start()方法启动了这两个进程,它们会异步的同时执行。

4.4 多进程带参数

import multiprocessing
import time


def music(num, name):
    for i in range(num):
        print(name)
        print('听音乐...')
        time.sleep(0.2)


def coding(count):
    for i in range(count):
        print('敲代码...')
        time.sleep(0.2)


if __name__ == '__main__':
    music_process = multiprocessing.Process(target=music, args=(3, '多任务开始'))
    coding_process = multiprocessing.Process(target=coding, kwargs={'count': 3})

    music_process.start()
    coding_process.start()

加入join()方法是为了保证主进程在所有子进程执行完毕后再结束,否则主进程会在子进程还没执行完毕就结束了。在多进程编程中,子进程的执行是异步的,主进程和子进程是并发执行的,如果主进程没有等待子进程执行完毕就结束了,那么子进程可能会被强制终止,导致程序出现异常或者数据丢失等问题。

因此,在启动子进程后,应该调用join()方法等待子进程执行完毕后再结束主进程,以保证程序的正常运行。join()方法会阻塞主进程,直到所有的子进程都执行完毕后才会继续执行主进程。

4.5 获取进程编号

进程编号的作用:
当程序中进程的数量越来越多时 , 为了进行有效的进程管理,用进程编号来区分主进程和子进程还有不同的子进程。

两种进程编号

  • 获取当前进程编号:
    getpid()
  • 获取当前进程的父进程ppid = parent pid:
    getppid()
import os
def work():
    # 获取当前进程的编号
    print('work进程编号', os.getpid())
    # 获取父进程的编号
    print('work父进程的编号', os.getppid())

work()
import multiprocessing
import time
import os


def music(num):
    print('music>> %d' % os.getpid())         # music>> 12232
    print('music主进程>> %d' % os.getppid())  # music主进程>> 15080
    for i in range(num):
        print('听音乐...')
        time.sleep(0.2)


def coding(count):
    print('coding>> %d' % os.getpid())          # coding>> 1868
     print('coding主进程>> %d' % os.getppid())  # coding主进程>> 15080
    for i in range(count):
        print('敲代码...')
        time.sleep(0.2)


if __name__ == '__main__':
	print('主进程>> %d' % os.getpid())   # 主进程>> 15080
    music_process = multiprocessing.Process(target=music, args=(3, ))
    coding_process = multiprocessing.Process(target=coding, kwargs={'count': 3})

    music_process.start()
    coding_process.start()

4.6 主进程与子进程的结束顺序

在这里插入图片描述

import multiprocessing
import time

def work():
    for i in range(10):
        print('工作中...')
        time.sleep(0.2)

if __name__ == '__main__':
    work_process = multiprocessing.Process(target=work)  # 创建子进程
    work_process.start()   # 启动子进程
    
    time.sleep(1)          # 延迟1s
    print('主进程执行完毕')   
# 运行结果:
工作中...
工作中...
工作中...
工作中...
工作中...
主线程执行完毕
工作中...
工作中...
工作中...
工作中...
工作中...

子进程需要2s才能完成,而主进程1s后就完成后终止了。
为防止在主进程终止后,子进程仍在继续,有以下两种方法:

解决方案一:设置守护进程

import multiprocessing
import time


# 工作函数
def work():
    for i in range(10):
        print('工作中...')
        time.sleep(0.2)


if __name__ == '__main__':
    work_process = multiprocessing.Process(target=work)   # 创建子进程
    work_process.daemon = True  # 设置守护主进程,主进程退出后子进程直接销毁,不再执行子进程中的代码
    work_process.start()        # 启动子进程

    time.sleep(1)               # 延迟1s
    print('主进程执行完毕')
    
#运行结果:
工作中...
工作中...
工作中...
工作中...
工作中...
主进程执行完毕

将work_process的daemon属性设置为True,表示该子进程是守护进程,即当主进程退出时,该子进程也会随之退出,不再执行子进程中的代码。

解决方案二:销毁子进程

import multiprocessing
import time


# 工作函数
def work():
    for i in range(10):
        print('工作中...')
        time.sleep(0.2)


if __name__ == '__main__':
    work_process = multiprocessing.Process(target=work)    # 创建子进程
    work_process.start()       # 启动子进程

    time.sleep(1)              # 延迟1s
    work_process.terminate()   # 让子进程直接销毁,表示终止执行, 主进程退出之前,把所有的子进程直接销毁就可以了
    print('主进程执行完毕')

五、多线程

5.1 单线程执行

def func_a():
    print('任务A')

def func_b():
    print('任务B')

func_a()
func_b()

在这里插入图片描述

5.2 多线程执行

在这里插入图片描述

import threading
线程对象 = threading.Thread(target=任务名)   # 通过线程类创建线程对象
线程对象.start()   # 启动线程执行任务
import time
import threading

def music():
    for i in range(3):
        print('听音乐...')
        time.sleep(0.2)

def coding():
    for i in range(3):
        print('敲代码...')
        time.sleep(0.2)

if __name__ == '__main__':
    music_thread = threading.Thread(target=music)
    coding_thread = threading.Thread(target=coding)

    music_thread.start()
    coding_thread.start()

5.3 主线程与子线程的结束顺序

同多线程类似,有两种方式来守护:

解决方案一:设置守护进程

import time
import threading

def work():
    for i in range(10):
        print('work...')
        time.sleep(0.2)

if __name__ == '__main__':
    work_thread = threading.Thread(target=work, daemon=True)   # 创建子线程并设置守护主线程
    work_thread.start()

    time.sleep(1)
    print('主线程执行完毕')
import time
import threading

def work():
    for i in range(10):
        print('work...')
        time.sleep(0.2)

if __name__ == '__main__':
    work_thread = threading.Thread(target=work)
    work_thread.setDaemon(True)   # 设置守护主线程
    work_thread.start()

    time.sleep(1)
    print('主线程执行完毕')

5.4 线程间的执行顺序

for i in range(5):
    sub_thread = threading.Thread(target=task)
    sub_thread.start()

线程之间是如何执行的呢?按顺序执行?一起执行?还是其他的执行方式呢?
答:线程之间的执行是无序的

5.5 线程间共享全局变量

多个线程都是在同一个进程中 , 多个线程使用的资源都是同一个进程中的资源 ,因此多线程间是共享全局变量

import threading
import time

my_list = []

def write_data():
    for i in range(3):
        print('add:', i)
        my_list.append(i)
    print(my_list)

def read_data():
    print('read:', my_list)

if __name__ == '__main__':
    write_thread = threading.Thread(target=write_data)
    read_thread = threading.Thread(target=read_data)

    write_thread.start()
    time.sleep(1)
    read_thread.start()
#运行结果:

add:0
add:1
add:2
[0, 1, 2]
read:[0, 1, 2]

六、多协程

  • 11
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值