要点:
- 进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。
参考文章:Python3多进程(mutiprocessing)
一 线程和进程的区别
主要介绍了线程和进程的区别及Python代码实例,本文给出了一个python的脚本让一个进程中运行两个线程,需要的朋友可以参考下,在程序猿的世界中,线程和进程是一个很重要的概念,很多人经常弄不清线程和进程到底是什么,有什么区别,本文试图来解释一下线程和进程。首先来看一下概念:
1.1 进程
一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进程。
英语:process,是计算机中已运行程序的实体。进程为曾经是分时系统的基本运作单位。在面向进程设计的系统(如早期的UNIX,Linux 2.4及更早的版本)中,进程是程序的基本执行实体;在面向线程设计的系统(如当代多数操作系统、Linux 2.6及更新的版本)中,进程本身不是基本运行单位,而是线程的容器。程序本身只是指令、数据及其组织形式的描述,进程才是程序(那些指令和数据)的真正运行实例。
1.2 线程
进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。
英语:thread,是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
是程序运行的实体,这句话的意思是,程序是存放在硬盘中的,当这个程序运行时,就会产生若干个进程。
与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
1.3 进程与线程的区别总结
线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元;而把传统的进程称为重型进程(Heavy—Weight Process),它相当于只有一个线程的任务。在引入了线程的操作系统中,通常一个进程都有若干个线程,至少包含一个线程。
根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。
资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的
影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行。
‘
二 Python 多线程
2.1 多线程
其他语言,CPU是多核时是支持多个线程同时执行。但在Python中,无论是单核还是多核,同时只能由一个线程在执行。其根源是GIL的存在。GIL的全称是Global Interpreter Lock(全局解释器锁),来源是Python设计之初的考虑,为了数据安全所做的决定。某个线程想要执行,必须先拿到GIL,我们可以把GIL看作是“通行证”,并且在一个Python进程中,GIL只有一个。拿不到通行证的线程,就不允许进入CPU执行。
并且由于GIL锁存在,Python里一个进程永远只能同时执行一个线程(拿到GIL的线程才能执行),这就是为什么在多核CPU上,Python 的多线程效率并不高的根本原因。
2.2 创建多线程
Python提供两个模块进行多线程的操作,分别是thread
和threading
,前者是比较低级的模块,用于更底层的操作,一般应用级别的开发不常用。
- 方法1:直接使用
threading.Thread()
import threading# 这个函数名可随便定义
def run(n):print("current task:", n)if __name__ == "__main__":t1 = threading.Thread(target=run, args=("thread 1",))t2 = threading.Thread(target=run, args=("thread 2",))t1.start()t2.start()
- 方法2:继承
threading.Thread
来自定义线程类,重写run
方法
import threadingclass MyThread(threading.Thread):def __init__(self, n):super(MyThread, self).__init__() # 重构run函数必须要写self.n = ndef run(self):print("current task:", n)if __name__ == "__main__":t1 = MyThread("thread 1")t2 = MyThread("thread 2")t1.start()t2.start()
2.3 线程合并
join
函数执行顺序是逐个执行每个线程,执行完毕后继续往下执行。主线程结束后,子线程还在运行,join
函数使得主线程等到子线程结束时才退出。
import threadingdef count(n):while n > 0:n -= 1if __name__ == "__main__":t1 = threading.Thread(target=count, args=("100000",))t2 = threading.Thread(target=count, args=("100000",))t1.start()t2.start()# 将 t1 和 t2 加入到主线程中t1.join()t2.join()
2.4 线程同步与互斥锁
线程之间数据共享的。当多个线程对某一个共享数据进行操作时,就需要考虑到线程安全问题。threading
模块中定义了Lock 类,提供了互斥锁的功能来保证多线程情况下数据的正确性。
用法的基本步骤:acquire()、 release()
#创建锁
mutex = threading.Lock()
#锁定
mutex.acquire([timeout])
#释放
mutex.release()
其中,锁定方法acquire可以有一个超时时间的可选参数timeout。如果设定了timeout,则在超时后通过返回值可以判断是否得到了锁,从而可以进行一些其他的处理。具体用法见示例代码:
import threading
import timenum = 0
mutex = threading.Lock()class MyThread(threading.Thread):def run(self):global num time.sleep(1)if mutex.acquire(1): num = num + 1msg = self.name + ': num value is ' + str(num)print(msg)mutex.release()if __name__ == '__main__':for i in range(5):t = MyThread()t.start()
2.5 定时器
如果需要规定函数在多少秒后执行某个操作,需要用到Timer
类。具体用法如下:
from threading import Timerdef show():print("Pyhton")# 指定一秒钟之后执行 show 函数
t = Timer(1, hello)
t.start()
三 Python 多进程
3.1 创建多进程
Python要进行多进程操作,需要用到 muiltprocessing
库,其中的Process
类跟threading
模块的Thread
类很相似。所以直接看代码熟悉多进程。
- 方法1:直接使用
Process
, 代码如下:
from multiprocessing import Process def show(name):print("Process name is " + name)if __name__ == "__main__": proc = Process(target=show, args=('subprocess',)) proc.start() proc.join()
- 方法2:继承
Process
来自定义进程类,重写run
方法, 代码如下:
from multiprocessing import Process
import timeclass MyProcess(Process):def __init__(self, name):super(MyProcess, self).__init__()self.name = namedef run(self):print('process name :' + str(self.name))time.sleep(1)if __name__ == '__main__':for i in range(3):p = MyProcess(i)p.start()for i in range(3):p.join()
3.2 多进程通信
进程之间不共享数据的。如果进程之间需要进行通信,则要用到 Queue 模块
或者 Pipe 模块
来实现。
- Queue
Queue是多进程安全的队列,可以实现多进程之间的数据传递。它主要有两个函数put
和get
。
put() 用以插入数据到队列中,put还有两个可选参数:blocked 和timeout。如果blocked为 True(默认值),并且timeout为正值,该方法会阻塞timeout指定的时间,直到该队列有剩余的空间。如果超时,会抛出 Queue.Full异常。如果blocked为False,但该Queue已满,会立即抛出Queue.Full异常。
get()可以从队列读取并且删除一个元素。同样get有两个可选参数:blocked和timeout。如果blocked为True(默认值),并且 timeout为正值,那么在等待时间内没有取到任何元素,会抛出Queue.Empty异常。如果blocked为False,有两种情况存在,如果Queue有一个值可用,则立即返回该值,否则,如果队列为空,则立即抛出Queue.Empty异常。
具体用法如下:
from multiprocessing import Process, Queuedef put(queue):queue.put('Queue 用法')if __name__ == '__main__':queue = Queue()pro = Process(target=put, args=(queue,))pro.start()print(queue.get()) pro.join()
- Pipe
Pipe的本质是进程之间的用管道数据传递,而不是数据共享,这和socket有点像。pipe() 返回两个连接对象分别表示管道的两端,每端都有send()和recv()函数。如果两个进程试图在同一时间的同一端进行读取和写入那么,这可能会损坏管道中的数据,具体用法如下:
from multiprocessing import Process, Pipedef show(conn):conn.send('Pipe 用法')conn.close()if __name__ == '__main__':parent_conn, child_conn = Pipe() pro = Process(target=show, args=(child_conn,))pro.start()print(parent_conn.recv()) pro.join()
3.3 进程池
创建多个进程,我们不用傻傻地一个个去创建。我们可以使用Pool
模块来搞定。Pool 常用的方法如下:
具体用法见示例代码:
#coding: utf-8
import multiprocessing
import timedef func(msg):print("msg:", msg)time.sleep(3)print("end")if __name__ == "__main__":# 维持执行的进程总数为processes,当一个进程执行完毕后会添加新的进程进去pool = multiprocessing.Pool(processes = 3)for i in range(5):msg = "hello %d" %(i)# 非阻塞式,子进程不影响主进程的执行,会直接运行到 pool.join()pool.apply_async(func, (msg, )) # 阻塞式,先执行完子进程,再执行主进程# pool.apply(func, (msg, )) print("Mark~ Mark~ Mark~~~~~~~~~~~~~~~~~~~~~~")# 调用join之前,先调用close函数,否则会出错。pool.close()# 执行完close后不会有新的进程加入到pool,join函数等待所有子进程结束pool.join() print("Sub-process(es) done.")
- 如上,进程池Pool被创建出来后,即使实际需要创建的进程数远远大于进程池的最大上限,p.apply_async(test) 代码依旧会不停的执行,并不会停下等待;相当于向进程池提交了10个请求,会被放到一个队列中;
- 当执行完p1 = Pool(5)这条代码后,5条进程已经被创建出来了,只是还没有为他们各自分配任务,也就是说,无论有多少任务,实际的进程数只有5条,计算机每次最多5条进程并行。
- 当Pool中有进程任务执行完毕后,这条进程资源会被释放,pool会按先进先出的原则取出一个新的请求给空闲的进程继续执行;
- 当Pool所有的进程任务完成后,会产生5个僵尸进程,如果主线程不结束,系统不会自动回收资源,需要调用 join函数 去回收。
- join函数是主进程等待子进程结束回收系统资源的,如果没有join,主程序退出后不管子进程有没有结束都会被强制杀死;
- 创建Pool池 时,如果不指定进程最大数量,默认创建的进程数为系统的内核数量.
3.4 选择多线程还是多进程
在这个问题上,首先要看下你的程序是属于哪种类型的。一般分为两种:CPU密集型和I/O密集型。
-
CPU 密集型:程序比较偏重于计算,需要经常使用CPU来运算。例如科学计算的程序,机器学习的程序等。
-
I/O 密集型:顾名思义就是程序需要频繁进行输入输出操作。爬虫程序就是典型的I/O密集型程序。
如果程序是属于CPU密集型,建议使用多进程。而多线程就更适合应用于I/O密集型程序。
四 并发和并行
并发是指系统能够同时处理多个任务,并且在这些任务之间进行切换,以实现每个任务都有机会被执行的能力。并发通常是通过操作系统中的进程或线程来实现的。在并发系统中,每个任务通常都有自己的执行流程,并且它们共享系统资源(如内存和CPU时间片),所以当多个任务同时运行时,就需要考虑如何协调它们之间的访问,以避免出现竞争条件和死锁等问题。
与此相反, 并行是指系统能够同时执行多个任务,即在同一时刻,多个任务可以在不同的处理器上运行。并行系统的主要目的是提高系统性能,因为使用多个处理器可以使得多个任务同时进行而不会相互干扰,从而缩短了处理时间。与并发不同的是,在并行系统中,每个任务都有自己的执行流程和资源,不需要考虑资源的共享和协调。
需要注意的是,并发和并行并不是完全独立的概念。在现代计算机系统中,往往会同时存在并发和并行的情况,例如,在一个多核心 CPU 上运行多个进程或线程。