2023-3-8 多进程与多线程复习

本文介绍了进程和线程的概念,强调了它们在资源管理和CPU执行上的区别。并行处理与并发处理的区别在于是否真正同时执行。Python中的GIL限制了多线程的并行执行,适合IO密集型任务。为解决GIL问题,文章提到了使用多进程和线程同步方法,如锁和Queue,以实现并发执行和资源共享。
摘要由CSDN通过智能技术生成

一、进程与线程

  1. 进程:进程是一个程序在数据集(程序在执行过程中所需要使用的资源)上的一次动态执行过程。其中进程控制块也是进程的一部分,系统通过进程控制块来控制和管理该进程。注意进程之间不共享资源。

  1. 线程:线程的出现是降低为了进程之间切换的消耗,把以前需要切换进程才能实现的功能,囊括在一个进程内,旨在提高系统的并发性,并突破一个进程只能干一样事的缺陷,使到进程内并发成为可能,从这里可以看出一个进程可以包括多个线程,这些线程共享进程的资源。

补充:(1)线程是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID, 程序计数器,寄存器集合和堆栈共同组成。

       线程没有自己的系统资源,系统资源是按照进程划分的,这里有个误区对于什么是系统资源,注意系统资源不是CPU资源,系统资源是用来跟踪程序执行的而不是用来执行程序的,例如在WINDOWS中,系统资源使用资源堆内存块组成,用于跟踪应用程序的执行状态像光标、窗口状态等相关信息。

          (2)进程是系统进行资源分配和调度的基本单位,是操作系统结构的基础,因此一个程序其本质实体是进程,而这个进程的运行则要分为一个至多个线程,而且至少一个线程(因为要运行),因此说进程是系统分配资源的最小单位,而线程则是CPU执行的基本单位。

关系总结:

        (1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。

  (2)资源分配给进程,同一进程的所有线程共享该进程的所有资源。

  (3)CPU分给线程,即真正在CPU上运行的是线程。

二、并行与并发

  1. 并行处理是计算机系统中能同时执行两个或更多个处理任务的一种计算方法。并行处理可以同时工作于同一程序的不同方面,并行处理的目的主要是节省大型和复杂问题的解决时间

  1. 并发处理:是指一个时间段内有几个程序都处于已启动运行到运行完毕之间,而且这几个程序都处在一个CPU上运行,但值得注意的是,在某一时刻只运行一个程序。

补充:因此并行强调同时,而并发强调在一个时间段内都在运行,显然并行可以认为是并发的一部分,但注意由于python存在GIL的原因,在同一进程下不可能实现多线程并行

对线程,多进程的存在依靠于实际的物理基础,补充CPU的核心数与线程数概念,或许能够更好理解:

       简单地说,CPU的核心数是指物理上,也就是硬件上存在着几个核心。比如,双核就是包括2个相对独立的CPU核心单元组,四核就包含4个相对独立的CPU核心单元组, 各个处理器核心可以并行执行不同的进程。这种依靠多个CPU核心可以同时并行地运行程序。

      线程数是一种逻辑的概念,简单地说,就是模拟出的CPU核心数。比如,可以通过一个CPU核心数模拟出2线程的CPU,也就是说,这个单核心的CPU被模拟成了一个类似双核心CPU的功能。我们从任务管理器的性能标签页中看到的是两个CPU。(当然这种技术得益于intel的超线程技术,否则一个核心只能有一个线程。)目前主流CPU都是多核的。增加核心数目就是为了增加线程数,因为操作系统是通过线程来执行任务的。

      CPU的线程数概念仅仅只针对Intel的CPU才有用,因为它是通过Intel超线程技术来实现的,最早应用在Pentium4上。如果没有超线程技术,一个CPU核心对应一个线程。所以,对于AMD的CPU来说,只有核心数的概念,没有线程数的概念。CPU之所以要增加线程数,是源于多任务处理的需要。线程数越多,越有利于同时运行多个程序,因为线程数等同于在某个瞬间CPU能同时并行处理的任务数。

Python中的实现:

  三、示例:

1.使用线程的程序:

几个重要的而注意点:

(1)join() : 在子线程运行结束之前,这个子线程的父线程将一直被阻塞。

(2)setDeamon(): 将相应的子线程设为守护父类的守护线程(很形象),既如果父类线程结束,则不管该线程是否结束都要跟着父类线程一起结束;相反则父类线程要等待子线程结束,然后才能一起退出。

(3)GIL: 只用在CPYTHON的解释器中。python中的线程是操作系统的原生线程,为实现多线程机制,一个基本的要求就是需要实现不同线程对共享资源的访问互斥,因此python引入了GIL。GIL在一个线程拥有了解释器的访问权之后,其他的所有线程都必须等待它释放解释器的访问权即使这些线程的下一条指令并不会互相影响, 当然在python中GIL 是自动的,这节省了我们的加锁操作。 特点:GIL点:多处理器退化为单处理器点:避免大量的加锁解锁操作(GIL在解释器层面加了一把锁,后续开发的代码可以不用考虑加锁的问题)。

显然从上可知:python在执行一个进程的时候,不论你启动多少个线程,也不论你的CPU有多少个核心,python只会在同一时允许一个线程运行。所以python 是无法利用多核CPU实现真正的多线程的—(真正的并行处理)。

这样,python对于计算密集型的任务开多线程的效率甚至不如串行(没有大量切换),但是,对于IO密集型的任务效率还是有显著提升的。

示例程序:

# python
2.解决方案-使用进程的程序:

       为弥补单一进程中多线程竞争GIL问题,python中可以使用多进程处理代替多线程,这样每个进程都只设置一个主线程,他们有各自的GIL,就不会出现竞争问题: 用Process替代Thread ,multiprocessing库它完整地复制了一套threading库所提供的接口,方便迁移。

但是使用多进程也是有缺点的,原因在于多进程之间是不共享资源的,这也就意味着多个进程之间不能看到彼此的数据,这就使得多进程在处理一个共同目标时会变得复杂,只能通过一个Queue,进行put再get 或者使用sharemenory的方法。

示例程序:

3. 同步锁lock()

    锁通常用于实现对共享资源(例如程序的全局变量)同步访问。为每一个共享资源创建一个lock对象,当一个线程需要访问该资源的时候(如果其他线程已经获得了该锁,则当前线程需要等待其被释放,然后才能继续访问),当访问完资源后,要记得调用release方法释放锁。

import threading
lock = threading.Lock()  # 定义一个锁对象,来专门锁一个全局资源。
global a = 100

# 假设这里有个子线程在运行
lock.acquire()
#
#这里就是子线程共享全局变量a的处理,在其处理结束前(这个子线程释放锁),其他子线程要处理
#全局变量a只能在原地等待。
#
lock.release()

问题:为什么有了GIL,还需要线程同步?

(1)GIL 是一种解释器层面的粗粒度加锁,它保证了共享资源在一时刻只能被一个线程获取,仅此而已,他无法保证多线程操作结果的正确性。

(2)另一种就是细粒度加锁,也就是用户自己加锁,以保证多线程操作按我们想要的顺序进行,以保证计算结果的正确性。

注意:GIL限定在一个进程上,

你写一个py程序,运行起来本身就是一个进程,这个进程是有解释器来翻译的,所以GIL限定在当前进程;如果又创建了一个子进程,那么两个进程是完全独立的,这个字进程也是有python解释器来运行的,所以这个子进程上也是受GIL影响的

(此外还有死锁概念,自行了解)

4. event 对象

     线程的一个关键特性是每个线程都是独立运行且状态不可预测。如果程序中的其他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就会变得非常棘手。     为了解决这些问题,我们需要使用threading库中的Event对象。 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在初始情况下,Event对象中的信号标志被设置为假。如果有线程等待一个Event对象, 而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行。(显然,一个event对象可以被多个线程等待,一旦这个event对象的标志被设置为真,则这些所有等待它的线程都将被唤醒运行)

event.isSet():返回event的状态值;

event.wait():如果 event.isSet()==False将阻塞线程;

event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;

event.clear():恢复event的状态值为False。

       threading.Event的wait方法还接受一个超时参数,默认情况下如果事件一致没有发生,wait方法会一直阻塞下去,而加入这个超时参数之后,如果阻塞时间超过这个参数设定的值之后,wait方法会返回。

def worker(event):
    while not event.is_set():
        logging.debug('Waiting for redis ready...')
        event.wait(2)
    logging.debug('redis ready, and connect to redis server and do some work [%s]', time.ctime())
    time.sleep(1)
5. Queue

queue 用于多个线程之间交换信息。常用该方法如get()和put()方法。

'''

创建一个“队列”对象

import queue

q = queue.Queue(maxsize = 10)

Queue.Queue类即是一个队列的同步实现。队列长度可为无限或者有限。可通过Queue的构造函数的可选参数

maxsize来设定队列长度。如果maxsize小于1就表示队列长度无限。

将一个值放入队列中

q.put(10)

调用队列对象的put()方法在队尾插入一个项目。put()有两个参数,第一个item为必需的,为插入项目的值;

第二个block为可选参数,默认为1。如果队列当前为空且block为1,put()方法就使调用线程暂停,直到空出一个数据单元。如果block为0,

put方法将引发Full异常。

将一个值从队列中取出

q.get()

调用队列对象的get()方法从队头删除并返回一个项目。可选参数为block,默认为True。如果队列为空且

block为True,get()就使调用线程暂停,直至有项目可用。如果队列为空且block为False,队列将引发Empty异常。

''’

2.join()和 task_done()

'''

join() 阻塞进程,直到所有任务完成,需要配合另一个方法task_done。

def join(self):

with self.all_tasks_done:

while self.unfinished_tasks:

self.all_tasks_done.wait()

task_done() 表示某个任务完成。每一条get语句后需要一条task_done。

import queue

q = queue.Queue(5)

q.put(10)

q.put(20)

print(q.get())

q.task_done()

print(q.get())

q.task_done()

q.join()

print("ending!")

#queue里面有多少数据,就需要对应数量的task_done()

''

'''

此包中的常用方法(q = Queue.Queue()):

q.qsize() 返回队列的大小

q.empty() 如果队列为空,返回True,反之False

q.full() 如果队列满了,返回True,反之False

q.full 与 maxsize 大小对应

q.get([block[, timeout]]) 获取队列,timeout等待时间

q.get_nowait() 相当q.get(False)非阻塞

q.put(item) 写入队列,timeout等待时间

q.put_nowait(item) 相当q.put(item, False)

q.task_done() 在完成一项工作之后,q.task_done() 函数向任务已经完成的队列发送一个信号

q.join() 实际上意味着等到队列为空,再执行别的操作

'''

'''

Python Queue模块有三种队列及构造函数:

1、Python Queue模块的FIFO队列先进先出。 class queue.Queue(maxsize)

2、LIFO类似于堆,即先进后出。 class queue.LifoQueue(maxsize)

3、还有一种是优先级队列级别越低越先出来。 class queue.PriorityQueue(maxsize)

import queue
#先进后出
q=queue.LifoQueue()
q.put(34)
q.put(56)
q.put(12)
 
#优先级
q=queue.PriorityQueue()
q.put([5,100])
q.put([7,200])
q.put([3,"hello"])
q.put([4,{"name":"alex"}])
 
while 1:
  data=q.get()
  print(data)
6.多进程案例(以上都是线程)

multiprocessing包是Python中的多进程管理包。与threading.Thread类似,它可以利用multiprocessing.Process对象来创建一个进程。该进程可以运行在Python程序内部编写的函数。该Process对象与Thread对象的用法相同,也有start(), run(), join()的方法。此外multiprocessing包中也有Lock/Event/Semaphore/Condition类 (这些对象可以像多线程那样,通过参数传递给各个进程),用以同步进程,其用法与threading包中的同名类一致。所以,multiprocessing的很大一部份与threading使用同一套API,只不过换到了多进程的情境。

Process 类,:

 构造方法:

from  multiprocessing import Process
def cale(n):
    sum=0
    for i in range(n):
        sum+=i
    print(sum)
if __name__=='__main__':
    '''
    在 Windows 上,子进程会自动 import 启动它的这个文件,而在 import 的时候是会执行这些
    语句的。如果你这么写的话就会无限递归创建子进程报错。所以必须把创建子进程的部分用那个
     if 判断保护起来,import 的时候 __name__ 不是 __main__ ,就不会递归运行了。
    '''
    p=Process(target=cale,args=(1000,))
    p.start()

  Process( target, name , args/kwargs) 

    group: 线程组,目前还没有实现,库引用中提示必须是None;

    target: 要执行的方法;

    name: 进程名;

    args/kwargs: 要传入方法的参数。

  实例方法:

import multiprocessing
class MyProcess(multiprocessing.Process):
    def __init__(self,n):
        super().__init__()
        self.n=n
        self.sum=0
    def run(self):
        for i  in range(self.n):
            self.sum+=i
        print(self.sum)
if __name__=='__main__':
    p=MyProcess(1000)
    p.start()

is_alive():返回进程是否在运行。

    join([timeout]):阻塞当前上下文环境的进程程,直到调用此方法的进程终止或到达指定的timeout(可选参数)。

    start():进程准备就绪,等待CPU调度

    run():strat()调用run方法,如果实例进程时未制定传入target,这star执行t默认run()方法。

    terminate():不管任务是否完成,立即停止工作进程

  属性:

    daemon:和线程的setDeamon功能一样.(将进程P设置为守护进程,P.daemon=True)

    name:进程名字。

    pid:进程号。

6.1.进程间通信

  6.1.1 进程队列 queue

from multiprocessing import Process, Queue
'''
 Queue已经被封装了,在多进程并发时,用于解决多进程间的通信问题。
'''
import queue
def f(q,n):
    #q.put([123, 456, 'hello'])
    q.put(n*n+1)
    print("son process",id(q))
if __name__ == '__main__':
    q = Queue()  #try: q=queue.Queue()   使用queue模块会报错
    print("main process",id(q))
    for i in range(3):
        p = Process(target=f, args=(q,i))
        p.start()
    print(q.get())
    print(q.get())
    print(q.get())

 6.1.2 管道(pipe)

  The Pipe() function returns a pair of connection objects connected by a pipe which by default is duplex (two-way). For example:

from multiprocessing import Process, Pipe
def f(conn):
    conn.send([12, {"name": "yuan"}, 'hello'])
    response = conn.recv()
    print("response", response)
    conn.close()
if __name__ == '__main__':
    parent_conn, child_conn = Pipe()  #设置两个管道
    p = Process(target=f, args=(child_conn,))
    p.start()
    print(parent_conn.recv())  # prints "[42, None, 'hello']"
    parent_conn.send("你好!")
    p.join()

  Pipe()返回的两个连接对象代表管道的两端。 每个连接对象都有send()和recv()方法(等等)。 请注意,如果两个进程(或线程)尝试同时读取或写入管道的同一端,管道中的数据可能会损坏(线程安全,要用Rlock保护数据)。

7.进程池

  进程池内部维护一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池序列中没有可供使用的进进程,那么程序就会等待,直到进程池中有可用进程为止。(可以用来控制进程的并发数)

from multiprocessing import Pool
import time
def foo(args):
 time.sleep(1)
 print(args)
if __name__ == '__main__':
 p = Pool(5)
 for i in range(30):
     p.apply_async(func=foo, args= (i,))
 p.close()   # 等子进程执行完毕后关闭线程池
 # time.sleep(2)
 # p.terminate()  # 立刻关闭线程池
 p.join()

进程池内部维护一个进程序列,当使用时,去进程池中获取一个进程,如果进程池序列中没有可供使用的进程,那么程序就会等待,直到进程池中有可用进程为止。

  进程池中有以下几个主要方法:

apply:从进程池里取一个进程并执行

apply_async:apply的异步版本

terminate:立刻关闭线程池

join:主进程等待所有子进程执行完毕,必须在close或terminate之后

close:等待所有进程结束后,才关闭线程池

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值