详细解析Python中的线程与进程的区别

Posted pythonedu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了详细解析Python中的线程与进程的区别相关的知识,希望对你有一定的参考价值。

什么是进程/线程

众所周知,CPU是计算机的核心,它承担了所有的计算任务。而操作系统是计算机的管理者,是一个大管家,它负责任务的调度,资源的分配和管理,统领整个计算机硬件。应用程序是具有某种功能的程序,程序运行与操作系统之上。

进程

进程时一个具有一定功能的程序在一个数据集上的一次动态执行过程。进程由程序,数据集合和进程控制块三部分组成。程序用于描述进程要完成的功能,是控制进程执行的指令集;数据集合是程序在执行时需要的数据和工作区;程序控制块(PCB)包含程序的描述信息和控制信息,是进程存在的唯一标志。

线程

在很早的时候计算机并没有线程这个概念,但是随着时代的发展,只用进程来处理程序出现很多的不足。如当一个进程堵塞时,整个程序会停止在堵塞处,并且如果频繁的切换进程,会浪费系统资源。所以线程出现了。

线程是能拥有资源和独立运行的最小单位,也是程序执行的最小单位。一个进程可以拥有多个线程,而且属于同一个进程的多个线程间会共享该进行的资源。

进程与线程的区别

  1. 一个进程由一个或者多个线程组成,线程是一个进程中代码的不同执行路线。
  2. 切换进程需要的资源比切换线程的要多的多。
  3. 进程之间相互独立,而同一个进程下的线程共享程序的内存空间(如代码段,数据集,堆栈等)。某进程内的线程在其他进程不可见。换言之,线程共享同一片内存空间,而进程各有独立的内存空间。
    以下是作者在知乎上看到的关于进程与线程的讨论,其中一个我感觉很有道理,摘抄如下:
    作者:zhonyong
    链接:
首先来一句概括的总论:进程和线程都是一个时间段的描述,是CPU工作时间段的描述。下面细说背景:CPU+RAM+各种资源(比如显卡,光驱,键盘,GPS, 等等外设)构成我们的电脑,但是电脑的运行,实际就是CPU和相关寄存器以及RAM之间的事情。一个最最基础的事实:CPU太快,太快,太快了,寄存器仅仅能够追的上他的脚步,RAM和别的挂在各总线上的设备完全是望其项背。那当多个任务要执行的时候怎么办呢?轮流着来?或者谁优先级高谁来?不管怎么样的策略,一句话就是在CPU看来就是轮流着来。一个必须知道的事实:执行一段程序代码,实现一个功能的过程介绍 ,当得到CPU的时候,相关的资源必须也已经就位,就是显卡啊,GPS啊什么的必须就位,然后CPU开始执行。这里除了CPU以外所有的就构成了这个程序的执行环境,也就是我们所定义的程序上下文。当这个程序执行完了,或者分配给他的CPU执行时间用完了,那它就要被切换出去,等待下一次CPU的临幸。在被切换出去的最后一步工作就是保存程序上下文,因为这个是下次他被CPU临幸的运行环境,必须保存。串联起来的事实:前面讲过在CPU看来所有的任务都是一个一个的轮流执行的,具体的轮流方法就是:先加载程序A的上下文,然后开始执行A,保存程序A的上下文,调入下一个要执行的程序B的程序上下文,然后开始执行B,保存程序B的上下文。。。。

========= 重要的东西出现了========

进程和线程就是这样的背景出来的,两个名词不过是对应的CPU时间段的描述,名词就是这样的功能。进程就是包换上下文切换的程序执行时间总和 = CPU加载上下文+CPU执行+CPU保存上下文线程是什么呢?进程的颗粒度太大,每次都要有上下的调入,保存,调出。如果我们把进程比喻为一个运行在电脑上的软件,那么一个软件的执行不可能是一条逻辑执行的,必定有多个分支和多个程序段,就好比要实现程序A,实际分成 a,b,c等多个块组合而成。那么这里具体的执行就可能变成:程序A得到CPU =》CPU加载上下文,开始执行程序A的a小段,然后执行A的b小段,然后再执行A的c小段,最后CPU保存A的上下文。这里a,b,c的执行是共享了A的上下文,CPU在执行的时候没有进行上下文切换的。这里的a,b,c就是线程,也就是说线程是共享了进程的上下文环境,的更为细小的CPU时间段。到此全文结束,再一个总结:进程和线程都是一个时间段的描述,是CPU工作时间段的描述,不过是颗粒大小不同。

 

开进程需要时间

学习《python爬虫开发与项目实践》时,执行下面一段代码:

from multiprocessing import Process
import os

def run_process(name):
    print("Child process %s (%s) is running" % (name,os.getpid()))

if __name__ == "__main__":
    print("parant process %s " % os.getpid())
    for i in range(5):
        p = Process(target=run_process, args=(str(i),))
        print("process will start")
        p.start()
    p.join()
    print("process end")  

 

显示的结果是

parant process 6332 
process will start
process will start
process will start
process will start
process will start
Child process 2 (9896) is running
Child process 0 (11208) is running
Child process 3 (5464) is running
Child process 1 (10208) is running
Child process 4 (12596) is running
process end

 

可以看到,程序在执行完

print ("parant process %s " % os.getpid())

 

没有接着马上执行run_process(),而是先打印process will start,最后把子进程一起执行。这是因为子进程的创建是需要时间的,在这个空闲时间里父进程继续执行代码,而子进程在创建完成后显示。

Pool进程池

需要创建多个进程时,可以使用multiprocessing中的Pool类开进程池。Pool()默认开启数量等于当前cpu核心数的子进程(当然可以手动改变)

from multiprocessing import Pool

def hello(i):
    print("hello ,this is the %d process" % i)

def main():
    p = Pool()
    for i in range(1,5):
        p.apply_async(target=hell0,args=(i,))
    p.close    
    p.join

if __name__ == "__main__":
    main()

 

apply_async表示在开进程时不阻塞主进程,是异步IO的一种方式之一。targe参数传入要在子线程中执行的函数对象,args以元组的方式传入函数的参数。

join会等待线程池中的每一个线程执行完毕,在调用join之前必须要先调用close,close表示不能再向线程池中添加新的process了。

进程间的通信

每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程A把数据从用户空间拷到内核缓冲区,进程B再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信。假如创建了多个进程,那么进程间的通信是必不可少的。Python提供了多种进程通信的方式,其中以Queue和Pipe用得最多。下面分别介绍这两种模式。

Queue

Queue是一种多进程安全的队列。实现多进程间的通信有两种方法:
- get() 用于向队列中加入数据。有两个属性:blocked和timeout。blocked为true时(默认为True)且timeout为正值时,如果当队列已满会阻塞timeout时间,在这个时间内如果队列有空位会加入,如果超过时间仍然没有空位会抛出Queue.Full异常。
- put() 用于从队列中获取一个数据并将其从队列中删除。有两个属性:blocked和timeout。blocked为true(默认为True)且timeout为正值时,如果当前队列为空会阻塞timeout时间,在这个时间内如果队列有新数据会获取,如果超过时间仍然没有新数据会抛出Queue.Empty异常。

from multiprocessing import Process,Queue
import os

def put_data(q,nums):
    print(现在的进程编号为:%s,这是一个put进程 % os.getpid())
    for num in nums:
        q.put(num)
        print(%d已经放入队列中啦! % num)

def get_data(q):
    print(现在的进程编号为:%s,这是一个get进程 % os.getpid())
    while True:
        print(已经从队列中获取%s并从中删除 % q.get())

if __name__ == __main__:
    q = Queue()
    p1 = Process(target=put_data,args=(q,[1,2,3],))
    p2 = Process(target=put_data,args=(q,[4,5,6],))
    p3 = Process(target=get_data,args=(q,))
    p1.start()
    p2.start()
    p3.start()
    p1.join()
    p2.join()
    # p3是个死循环,需要手动结束这个进程
    p3.terminate()

 

我们来看一下运行结果:

现在的进程编号为:10336,这是一个put进程
1已经放入队列中啦!
2已经放入队列中啦!
3已经放入队列中啦!
现在的进程编号为:9116,这是一个get进程
已经从队列中获取1,并从中删除
已经从队列中获取2并从中删除
已经从队列中获取3并从中删除
现在的进程编号为:2732,这是一个put进程
4已经放入队列中啦!
5已经放入队列中啦!
已经从队列中获取4,并从中删除
6已经放入队列中啦!
已经从队列中获取5并从中删除
已经从队列中获取6并从中删除

 

Pipe

Pipe与Queue不同之处在于Pipe是用于两个进程之间的通信。就像进程位于一根水管的两端。让我们看看Pipe官方文档的描述:

Returns a pair (conn1, conn2) of Connection objects representing the ends of a pipe.

Piep返回conn1和conn2代表水管的两端。Pipe还有一个参数duplex(adj. 二倍的,双重的 n. 双工;占两层楼的公寓套房),默认为True。当duplex为True时,开启双工模式,此时水管的两边都可以进行收发。当duplex为False,那么conn1只负责接受信息,conn2只负责发送信息。
conn通过send()和recv()来发送和接受信息。值得注意的是,如果管道中没有信息可接受,recv()会一直阻塞直到管道关闭(任意一端进程接结束则管道关闭)。

from multiprocessing import Process,Pipe
import os

def put_data(p,nums):
    print(现在的进程编号为:%s,这个一个send进程 % os.getpid())
    for num in nums:
        p.send(num)
        print(%s已经放入管道中啦! % num)

def get_data(p):
    print(现在的进程编号为:%s,这个一个recv进程 % os.getpid())
    while True:
        print(已经从管道中获取%s并从中删除 % p.recv())

if __name__ == __main__:
    p = Pipe(duplex=False)
    # 此时Pipe[1]即是Pipe返回的conn2
    p1 = Process(target=put_data,args=(p[1],[1,2,3],))
    # 此时Pipe[0]即是Pipe返回的conn1
    p3 = Process(target=get_data,args=(p[0],))
    p1.start()
    p3.start()
    p1.join()
    p3.terminate()

 

让我们看一下输出结果

现在的进程编号为:9868,这个一个recv进程
现在的进程编号为:9072,这个一个send进程
1已经放入管道中啦!
已经从管道中获取1,并从中删除
2已经放入管道中啦!
已经从管道中获取2并从中删除
3已经放入管道中啦!
已经从管道中获取3并从中删除

 

控制线程

我们是没有办法完全人为控制线程的,因为线程由系统控制。但是可以用一些方式来影响线程的调用,比如互斥锁,sleep(阻塞),死锁等。

线程的几种状态

 

新建-----就绪------------------运行-----死亡

等待(阻塞)

线程的生命周期由run方法决定,当run方法结束时线程死亡。可以通过继承Thread,重写run方法改变Thread的功能,最后还是通过start()方法开线程。

from threading import Thread

class MyThread(Thread):
    def run(self):
        print(i am sorry)

if __name__ == __main__:
    t = MyThread()
    t.start()

 

 

通过args参数以一个元组的方式给线程中的函数传参。

from threading import Thread

def sorry(name):
    print(i am sorry,name)

if __name__ == __main__:  
    t = Thread(target=sorry,args=(mike))
    t.start()

 

线程锁

多线程中任务中,可能会发生多个线程同时对一个公共资源(如全局变量)进行操作的情况,这是就会发生混乱。为了避免这种情况,需要引入线程锁的概念。只有一个线程能处于上锁状态,当一个线程上锁之后,如果有另外一个线程试图获得锁,该线程就会挂起直到拥有锁的线程将锁释放。这样就保证了同时只有一个线程对公共资源进行访问或修改。

from threading import Thread,Lock

num = 0
def puls():
    # 获得一个锁
    lock = Lock()
    global num
    # acquire()方法上锁
    lock.acquire()
    num += 1
    print(num)
    # release()方法解锁
    lock.release()

if __name__ == __main__:
    for i in range(5):
        t = Thread(target=plus)
        t.start()
    t.join()     

 

join()方法会阻塞主线程直到子线程全部结束(也就是同步)。

锁的用处:
1. 确保某段关键代码只能由一个线程从头到尾执行,保证了数据的唯一性。

锁的坏处:

1. 阻止了多线程并发执行,效率大大降低。
2. 由于存在多个锁,不同的线程持有不同的锁并试图获取对方的锁时,可能造成死锁。

守护线程

线程其实并没有主次的概念,我们一般说的‘主线程’实际上是main函数的线程,而所谓主线程结束子线程也会结束是因为在主线程结束时调用了系统的退出函数。而守护线程是指‘不重要线程’。主线程会等所有‘重要’线程结束后才结束。通常当客户端访问服务器时会为这次访问开启一个守护线程。将setDaemon属性设为True即可将该线程设为守护线程。

from threading import Thread

n = 100

def count(x,y):
    return n=x+y

if __name__ == __main__:

    t = Thread(target=count,args=(1,2))
    t.setDaemon = True
    # ...
 

python学习交流群:125240963

转载至:Python中的线程与进程























以上是关于详细解析Python中的线程与进程的区别的主要内容,如果未能解决你的问题,请参考以下文章

线程和服务的区别

多进程和多线程有啥区别?

进程线程服务和任务的区别以及多线程与超线程的概念

Python线程与进程的区别

python多进程和多线程的区别

python中多进程和多线程的区别