廖雪峰 进程、线程

2021/12/20 7:20:51

本文主要是介绍廖雪峰 进程、线程,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

目录
  • 进程和线程
    • 多进程
      • Pool
      • 子进程
      • 进程间的通信
    • 多线程
      • 多核CPU
    • ThreadLocal
    • 进程VS线程
      • 各自的特点
      • 线程切换
      • 计算密集型 VS. IO密集型
      • 异步IO
    • 分布式进程

进程和线程

  • 进程是操作系统分配资源的最小单元
  • 线程是操作系统调度的最小单元。
  • 一个应用程序至少包括1个进程,而1个进程包括1个或多个线程,线程的尺度更小。
  • 每个进程在执行过程中拥有独立的内存单元,而一个线程的多个线程在执行过程中共享内存。

对操作系统而言,一个任务就是一个进程,单核CPU 使用时间片轮转的方式,让各个任务交替进行

进程内部的“子任务”,就是线程

真正地同时执行多线程需要多核CPU

多任务的实现方式:

  • 多进程模式
  • 多线程模式
  • 多进程+多线程模式

总结:

  • 线程是最小的执行单元,而进程由至少一个线程组成。
  • 进程和线程的调度执行时候操作系统控制的

多进程

multiprocessing模块,就是跨平台版本的多进程模块。

multiprocessing模块提供了一个Process类来代表一个进程对象。

Pool

启动大量子进程,可以用进程池的方式批量创建子进程。

from multiprocessing import Pool

import os,time,random

def long_time_task(name):
    print("Run task %s (%s)..."%(name,os.gerpid()))
    start = time.time()
    time.sleep(random.random()*3)
    end = time.time()
    print("Task %s runs %0.2f seconds."%(name,(end-start)))

if __name__=='__main__':
    # 子进程调用getppid() 可以拿到父进程的ID
    print("Parent process %s"%os.getpid())
    # 同时设置4个进程
    p = Pool(4)
    for i in range(5):
        p.apply_async(long_time_task,args = (i,))
    print("等待所有subprocess执行完毕")
    p.close()
    # join()方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步
    p.join()
    print("All  subprocesses done")

子进程

subprocess模块用来启动子进程,然后控制其输入输出

进程间的通信

Queue、Pipes用来交换数据

在Unix/Linux下,可以使用fork()调用实现多进程。

要实现跨平台的多进程,可以使用multiprocessing模块。

进程间通信是通过Queue、Pipes等实现的。

多线程

Python的标准库提供了两个模块:_threadthreading,_thread是低级模块,threading是高级模块,对_thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块。

  1. 启动一个线程就是把一个函数传入并创建Thread实例,然后调用start()开始执行
  2. 由于任何进程默认就会启动一个线程,我们把该线程称为主线程,主线程又可以启动新的线程,Python的threading模块有个current_thread()函数,它永远返回当前线程的实例。主线程实例的名字叫MainThread,子线程的名字在创建时指定,我们用LoopThread命名子线程。名字仅仅在打印时用来显示,完全没有其他意义,如果不起名字Python就自动给线程命名为Thread-1,Thread-2……

Lock

多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。

解决:

如果我们要确保balance计算正确,就要给change_it()上一把锁,当某个线程开始执行change_it()时,我们说,该线程因为获得了锁,因此其他线程不能同时执行change_it(),只能等待,直到锁被释放后,获得该锁以后才能改。由于锁只有一个,无论多少线程,同一时刻最多只有一个线程持有该锁,所以,不会造成修改的冲突。创建一个锁就是通过threading.Lock()来实现

使用try...finally...保证锁被释放
锁的缺点:

  • 阻止了多线程并发执行
  • 可能会造成死锁

多核CPU

因为Python的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。

所以,python不能利用多线程实现多核任务,但是可以通过进程实现多核任务。多个python进程由各自独立的GIL锁,互补影响。

总结:

  • 多线程编程,模型复杂,容易发生冲突,必须用锁加以隔离,同时,又要小心死锁的发生
  • python解释器由于设计时有GIL全局锁,导致了多线程无法利用多核。

ThreadLocal

每个线程都有自己的局部变量,这样线程间传递参数的时候麻烦.然后就出现了ThreadLOcal

import threading

# 创建全局的ThreadLocal对象
local_school = threading.local()
def process_student():
    # 获取当前线程关联的student:
    std = local_school.student
    print('Hello, %s (in %s)' % (std, threading.current_thread().name))

def process_thread(name):
    # 绑定ThreadLocal的student:
    local_school.student = name
    process_student()
    
t1 = threading.Thread(target= process_thread, args=('Alice',), name='Thread-A')
t2 = threading.Thread(target= process_thread, args=('Bob',), name='Thread-B')
t1.start()
t2.start()
t1.join()
t2.join()

ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等。

小结:

  • 一个ThreadLocal变量虽然是全局变量,但每个线程都只能读写自己线程的独立副本,互不干扰。ThreadLocal解决了参数在一个线程中各个函数之间互相传递的问题。

进程VS线程

各自的特点

  1. 多进程模式最大的优点就是稳定性高,因为一个子进程崩溃了,不会影响主进程和其他子进程。(当然主进程挂了所有进程就全挂了,但是Master进程只负责分配任务,挂掉的概率低)著名的Apache最早就是采用多进程模式。
  2. 多进程模式的缺点是创建进程的代价大,在Unix/Linux系统下,用fork调用还行,在Windows下创建进程开销巨大。另外,操作系统能同时运行的进程数也是有限的,在内存和CPU的限制下,如果有几千个进程同时运行,操作系统连调度都会成问题
  3. 多线程模式通常比多进程快一点,但是也快不到哪去,而且,多线程模式致命的缺点就是任何一个线程挂掉都可能直接造成整个进程崩溃,因为所有线程共享进程的内存。在Windows上,如果一个线程执行的代码出了问题,你经常可以看到这样的提示:“该程序执行了非法操作,即将关闭”,其实往往是某个线程出了问题,但是操作系统会强制结束整个进程。
  4. 在Windows下,多线程的效率比多进程要高,所以微软的IIS服务器默认采用多线程模式。由于多线程存在稳定性的问题,IIS的稳定性就不如Apache。

线程切换

操作系统在切换进程或者线程时也是一样的,它需要先保存当前执行的现场环境(CPU寄存器状态、内存页等),然后,把新任务的执行环境准备好(恢复上次的寄存器状态,切换内存页等),才能开始执行。

计算密集型 VS. IO密集型

计算密集型任务同时进行的数量应当等于CPU的核心数。

计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写。

第二种任务的类型是IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。

IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少,因此,用运行速度极快的C语言替换用Python这样运行速度极低的脚本语言,完全无法提升运行效率。对于IO密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差。

异步IO

如果充分利用操作系统提供的异步IO支持,就可以用单进程单线程模型来执行多任务,这种全新的模型称为事件驱动模型

对应到Python语言,单线程的异步编程模型称为协程,有了协程的支持,就可以基于事件驱动编写高效的多任务程序。我们会在后面讨论如何编写协程。

子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。

所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。

子程序调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。

协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。

def A():
    print('1')
    print('2')
    print('3')

def B():
    print('x')
    print('y')
    print('z')
    
协程执行的结果:
1
2
x
y
3
z

最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。

第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

因为协程是一个线程执行,那怎么利用多核CPU呢?最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。

分布式进程

在线程和进程中,优先选择Process,因为Process更稳定。

from multiprocessing import Queue

import queue 

# 两者是不同的

# Queue.Queue是进程内非阻塞队列。

# multiprocess.Queue是跨进程通信队列。

# 多进程前者是各自私有,后者是各子进程共有

Python的multiprocessing模块不但支持多进程,其中managers子模块还支持把多进程分布到多台机器上。一个服务进程可以作为调度者,将任务分布到其他多个进程中,依靠网络通信。由于managers模块封装很好,不必了解网络通信的细节,就可以很容易地编写分布式多进程程序。

举个例子:如果我们已经有一个通过Queue通信的多进程程序在同一台机器上运行,现在,由于处理任务的进程任务繁重,希望把发送任务的进程和处理任务的进程分布到两台机器上。怎么用分布式进程实现?

### 略 ###
# 服务进程,负责启动Queue,把Queue注册到网络上,然后往Queue里面写任务

import random,time,queue
from multiprocessing.managers import BaseManager

# 发送任务的队列

task_queue = queue.Queue()

# 接收结果的队列



这篇关于廖雪峰 进程、线程的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程