01- 线程和进程,并发和并行(Windows系统)

要点:

  • 进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。

参考文章: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提供两个模块进行多线程的操作,分别是threadthreading,前者是比较低级的模块,用于更底层的操作,一般应用级别的开发不常用。

  • 方法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 threading
 
class MyThread(threading.Thread):
    def __init__(self, n):
        super(MyThread, self).__init__()  # 重构run函数必须要写
        self.n = n
 
    def 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 threading
 
def count(n):
    while n > 0:
        n -= 1
 
if __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 time
 
num = 0
mutex = threading.Lock()
 
class MyThread(threading.Thread):
    def run(self):
        global num 
        time.sleep(1)
 
        if mutex.acquire(1):  
            num = num + 1
            msg = 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 Timer
 
def 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 time
 
class MyProcess(Process):
    def __init__(self, name):
        super(MyProcess, self).__init__()
        self.name = name
 
    def 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是多进程安全的队列,可以实现多进程之间的数据传递。它主要有两个函数putget

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, Queue
 
def 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, Pipe
 
def 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 time
 
def 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 上运行多个进程或线程。

猜你喜欢

转载自blog.csdn.net/March_A/article/details/130958738
今日推荐