进程&线程(一)——multiprocessing,threading

本节内容为①进程线程的基础知识;②在Python的实现方法;

学习总结自:

一文看懂Python多进程与多线程编程(工作学习面试必读) - 知乎

 multiprocessing 官方文档

1、进程线程基础

什么是进程、线程?

①进程:Process;线程:Thread;

②进程是OS分配资源的最小单元,线程是OS调度的最小单元;

③一个程序至少包括一个进程,一个进程至少包括一个线程;线程的尺度更小

④进程执行过程中拥有独立内存单元,不同进程间的内存单元互不干涉;

一个进程中的多个线程在执行过程中共享内存。

2、进程在Python中的实现

1)多进程编程与multiprocessing模块

Python多进程编程主要依靠multiprocessing模块。为了直观理解多进程的优势,我们可以看以下一个例子:

模拟一个非常耗时的任务,计算8的20次方,为了使这个任务显得更加耗时,我们中途还sleep 2s。第一段代码是单进程,我们按照顺序执行代码,重复计算两次,并打印出总共耗时。

复制代码
import time
import os
def long_time_task():
    print('当前进程:{}'.format(os.getpid()))
    time.sleep(2)
    print('8^20={}'.format(8**20))

if __name__=='__main__':
    print('当前父进程:{}'.format(os.getpid()))
    start=time.time()
    for i in range(2):
        long_time_task()
    end=time.time()
    print('程序耗时{}s'.format(end-start))
复制代码

输出结果如下:

当前父进程:14956
当前进程:14956
8^20=1152921504606846976
当前进程:14956
8^20=1152921504606846976
程序耗时4.010442018508911s

可以看出来,总共耗时4s,且自始至终只有一个进程14956。说明计算机计算8^20并不耗时。

 

第二段代码是多进程计算代码,我们利用multiprocessing模块的Process()方法创建了两个新的进程P1与P2进行并行计算。Process方法接收两个参数,第一个是target,一般指向某个函数,表明该进程执行的任务;第二个是args,即需要向函数传递的参数。此外,还有两个方法start()join()

start()方法之后,进程开始执行;

join()方法用于阻塞父进程,等待子进程结束后继续执行父进程,通常用于进程间的同步。

复制代码
from multiprocessing import Process
import os
import time

def long_time_task(i):
    print('当前进程:{} - 任务{}'.format(os.getpid(),i))
    time.sleep(2)
    print('8^20={}'.format(8**20))

if __name__=='__main__':
    print('当前父进程:{}'.format(os.getpid()))
    start=time.time()
    P1=Process(target=long_time_task,args=(1,))
    P2=Process(target=long_time_task,args=(2,))
    print('等待所有子进程完成。')
    P1.start()
    P2.start()
    P1.join()
    P2.join()
    end=time.time()
    print('总共用时{}s'.format(end-start))
复制代码

输出结果:

当前父进程:3544
等待所有子进程完成。
当前进程:3968 - 任务2
当前进程:9028 - 任务1
8^20=1152921504606846976
8^20=1152921504606846976
总共用时2.1800246238708496s

耗时2s,时间减少了一半。另外,尽管我们创建了两个进程,但是在实际运行过程中却是一个父进程、2个子进程。这一点怎么看出来呢,可以在join方法之后添加一句打印父进程id的语句,可以看到这一句话并不是立刻打印出来的,而是子进程执行完毕后才继续执行的。说明了这两个进程并不是与父进程并列的,而是父进程的子进程。

父进程在所有子进程的join方法之后继续执行。

知识点

  • 进程的创建切换需要耗费资源,所以平时工作中的进程数不能太多。
  • 同时运行的进程数(并行)一般受制于CPU的核数
  • 除了使用Process方法创建进程,还可以使用Pool

2)利用multiprocessing模块的Pool类创建多进程

很多时候系统都需要创建多个进程以提高CPU利用率,当数量较少时,可以手动生成一个个Process实例。当进程数量很多时,可以利用循环,但是这需要程序员手动管理系统中并发进程的数量,有时会很麻烦。这时进程池Pool就可以发挥其功效了。可以通过传递参数来限制并发进程的数量,默认为CPU的核数

原理

Pool类可以提供指定数量的进程供用户调用,当有新的请求提交到Pool时,如果Pool没满,就会创建一个新的进程来执行请求。如果池满,请求就会告知先等待,直到池中有进程结束,才会创建新的进程来执行这些请求。

方法

apply_async

向Pool提交需要执行的函数及参数,每提交一项相当于创建了一个待入池的进程。

各进程间异步执行,互不影响,这是默认方式。

map

用法与内置的map函数一致。

会使进程阻塞直到结果返回,即各进程执行顺序同步。

map_async 用法同map,区别在于不会阻塞进程,即各进程异步执行,互不妨碍。
close 关闭Pool,不再接受新任务。
terminate 结束工作进程,不再处理未处理的任务
join 阻塞主进程。join方法必须要在close和terminate之后使用

例子

笔者CPU是8核的,所以一次最多可以同时运行8个进程,所以我开启了一个容量为4的进程池。

8个进程需要计算9次,所以可以想像过程:8个进程并行执行8次计算任务后,还剩一次计算任务没有完成,系统会等待8个进程完成后(而不是完成一个之后)重新安排一个进程来计算。

复制代码
from multiprocessing import Pool,cpu_count
import os
import time

def long_time_task(i):
    print('当前子进程:',os.getpid())
    time.sleep(2)
    print('8^20=%s\n'%(8**20))

if __name__=='__main__':
    print('当前父进程:',os.getpid())
    print('CPU核数:',cpu_count())
    start=time.time()
    p=Pool(cpu_count())
    for i in range(cpu_count()+1):
        p.apply_async(long_time_task,args=(i,))
    print('所有子进程运行完毕')
    p.close()
    p.join()
    end=time.time()
    print('总共用时:',end-start)
复制代码

输出结果如下:

复制代码
当前父进程: 10808
CPU核数: 8
所有子进程运行完毕
当前子进程: 8424
当前子进程: 10756
当前子进程: 9464
当前子进程: 8532
当前子进程: 3172
当前子进程: 5268
当前子进程: 4568
当前子进程: 9604
8^20=1152921504606846976
当前子进程: 8424
8^20=1152921504606846976
8^20=11529215046068469768^20=1152921504606846976
8^20=11529215046068469768^20=1152921504606846976
8^20=1152921504606846976
8^20=1152921504606846976
8^20=1152921504606846976
总共用时: 4.21990966796875
复制代码

由于9个进程并发执行了两轮,所以总用时只用时了4s,而不是2*9=18s。

知识点

  • 对Pool对象调用join方法会等待所有子进程执行完毕,之后才会执行主进程(这一点和之前用Process创建单个子进程时的用法相同);
  • 调用join之前必须先调用closeterminate方法,让其不再接受新的Process;
  •  常用for循环加apply_async方法,往Pool中添加Process;
  • Python解释器中存在GIL(全局解释器锁),其作用是保证同一时刻只有一个线程可以执行代码。由于GIL的存在,Python中的多线程并不是实际的多线程,如果想要充分地使用多核CPU的资源,在Python中大部分情况需要使用多进程。但这并不意味着Python多线程编程没有意义,关于多线程的部分可以看第3节。

3)多进程间的数据共享与通信

通常,进程之间是相互独立的,每个进程都有独立的内存。通过共享内存(nmap模块),进程之间可以共享对象,使多个进程可以访问同一个变量(地址相同变量名可以不同)。多进程共享资源必然会导致进程之间的相互竞争,所以应尽最大可能防止使用共享状态。还有一种方式是使用Queue来实现不同进程间的通信或数据共享,这点和多线程编程类似。

例子

下面的代码中创建了两个独立进程,一个负责写(pw),另一个负责读(pr),实现了共享一个队列Queue:

复制代码
from multiprocessing import Process,Queue
import os , time , random

#写数据
def write(q):
    print('Process to write:{}'.format(os.getpid()))
    for value in ['A','B','C']:
        print('Put %s to queue...'%value)
        q.put(value)
        time.sleep(random.random())

#读数据
def read(q):
    print('Process to read:{}'.format(os.getpid()))
    while True:
        value=q.get()
        print('Get %s from queue.'%value)

if __name__=='__main__':
    #父进程创建Queue,并传给各个子进程:
    q=Queue()
    pw=Process(target=write,args=(q,))
    pr=Process(target=read,args=(q,))

    pw.start()
    pr.start()
    #等待pw结束
    pw.join()
    # pr进程中是死循环,无法等待其结束,只能强行终止
    pr.terminate()
复制代码

运行结果:

Process to write:1720
Put A to queue...
Process to read:9500
Get A from queue.
Put B to queue...
Get B from queue.
Put C to queue...
Get C from queue.

知识点

  • 上文代码中,在主进程创建Queue,作为参数q传入子进程
  • 在子进程中进行出队入队——写入数据时入队,读取数据时出队;入队——q.put(value);出队——value=q.get()
  • 子进程的开始,依然是用方法start()
  • 如果进程可能无法结束,就要用terminate方法,而不是join等待

3、多线程编程与threading模块

创建新线程与创建新进程的方式类似。threading.Thread方法接收两个参数:target——线程执行函数;args——向函数传递的参数。对新创建的线程,用start()方法让其开始,join()方法阻塞主线程。我们还可以使用current_thread().name方法打印出当前线程的名字。

例子

还是之前的例子,计算8^20,并等待2s。这里使用线程threading.Thread实现

 

复制代码
import threading
import time

def long_time_task(i):
    print('当前子线程:{}——任务:{}'.format(threading.current_thread().name,i))
    time.sleep(2)
    print('8^20={}'.format(8**20))

if __name__=='__main__':
    start=time.time()
    print('主线程:{}'.format(threading.current_thread().name))
    t1=threading.Thread(target=long_time_task,args=(1,))
    t2=threading.Thread(target=long_time_task,args=(2,))
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    end=time.time()
    print('程序运行时间:%ss'%(end-start))
复制代码

 

结果:

主线程:MainThread
当前子线程:Thread-3——任务:1
当前子线程:Thread-4——任务:2
8^20=11529215046068469768^20=1152921504606846976
程序运行时间:2.01711368560791s

1)主线程、子线程同步

当我们设置多线程时,主线程会创建多个子线程,在Python中,默认情况下主、子线程异步运行互不干涉。如果需要主线程等待子线程实现线程的同步,需要join方法,这点和多进程倒是类似,如果我们主线程结束时不再执行子线程,我们可以使用Thread.setDaemon(True),代码示例如下:

复制代码
import threading
import time


def long_time_task():
    print('当子线程: {}'.format(threading.current_thread().name))
    time.sleep(2)
    print("结果: {}".format(8 ** 20))


if __name__=='__main__':
    start = time.time()
    print('这是主线程:{}'.format(threading.current_thread().name))
    for i in range(5):
        t = threading.Thread(target=long_time_task, args=())
        t.setDaemon(True)
        t.start()

    end = time.time()
    print("总共用时{}秒".format((end - start)))
复制代码

 

setDaemon(True):设置该线程为守护线程,表明该线程是不重要的,进程退出时不需要等待这个线程执行完毕。

这样做的意义在于:避免子线程死循环,导致退不出程序。

2)通过继承Thread类重写run方法创建新线程

除了使用Thread()方法创建新的线程外,还以通过继承Thread类,重写run方法创建新的线程,这种方法更加灵活。

例子

自定义Thread类MyThread,重写run方法。通过该类的实例化创建2个子线程:

复制代码
import threading
import time

def long_time_task(i):
    print('当前子线程{}——任务{}'.format(threading.current_thread().name,i))
    time.sleep(2)
    print('8^20=%s,%d'%(8**20,i))

class MyThread(threading.Thread):
    def __init__(self,func,args,name=''):
        threading.Thread.__init__(self)
        self.func=func
        self.args=args
        self.name=name
        self.result=None
    def run(self):
        self.func(self.args[0])
if __name__=='__main__':
    start=time.time()
    threads=[]
    for i in range(1,3):
        t=MyThread(long_time_task,(i,),str(i))
        threads.append(t)
    for t in threads:
        t.start()
    for t in threads:
        t.join()
        print('结束子进程',t.name)
    end=time.time()
    print('总用时%ss'%(end-start))
复制代码

输出结果如下:

当前子线程1——任务1
当前子线程2——任务2
8^20=1152921504606846976,2
8^20=1152921504606846976,1
结束子进程 1
结束子进程 2
总用时2.020406723022461s

3)不同线程间的数据共享

①加锁lock

一个进程中的不同线程间共享内存,这就意味着任何一个变量都可以被任何一个线程修改,因此线程之间共享数据的最大危险在于多线程同时修改变量,把内容改乱了。如果不同线程间有共享的变量,其中一个方法就是在修改之前给其加锁lock,确保一次只有一个线程能够修改它。

threading.Lock()方法可以实现对一个共享变量的锁定,修改完后释放供其它线程使用。

例子

模拟存取钱,其中的账户余额balance是一个共享变量,使用lock可以使其避免错误改动:

复制代码
import threading
class Account:
    def __init__(self):
        self.balance=0

    #存取时第一步就是加锁
    #最后一步就是释放锁
    def save(self,lock):
        lock.acquire()
        for i in range(100000):
            self.balance+=1
        lock.release()

    def load(self,lock):
        lock.acquire()
        for i in range(100000):
            self.balance-=1
        lock.release()

if __name__=='__main__':
    account=Account()
    lock=threading.Lock()

    thread_save=threading.Thread(target=account.save,args=(lock,),name='Save')
    thread_load=threading.Thread(target=account.load,args=(lock,),name='Load')

    thread_save.start()
    thread_load.start()

    thread_save.join()
    thread_load.join()
    print('The final balance is ',account.balance)
复制代码

存取函数的第一步就是加锁,最后一步是释放锁

结果:

The final balance is  0

 ②queue队列

 另一种实现不同线程间数据共享的方法就是使用消息队列queue。

例子——生产者、消费者模型

下边的代码创建了两个线程,一个负责生产,另一个负责消费,所生成的产品放在Queue中,实现不同线程间沟通。

复制代码
from queue import Queue
import random,threading,time

class Producer(threading.Thread):
    def __init__(self,name,queue):
        threading.Thread.__init__(self,name=name)
        self.queue=queue
    def run(self):
        for i in range(1,5):
            print('{} is producing {} to the queue!'.format(self.getName(),i))
            self.queue.put(i)
            time.sleep(random.randrange(10)/5)
            print('%s finished!'%self.getName())

class Consumer(threading.Thread):
     def __init__(self,name,queue):
            threading.Thread.__init__(self,name=name)
            self.queue=queue
     def run(self):
            for i in range(1,5):
                val=self.queue.get()
                print('{} is consuming {} in the queue.'.format(self.getName(),val))
                time.sleep(random.randrange(10))
            print('%s finished!'%self.getName())

if __name__=='__main__':
    queue=Queue()
    producer=Producer('Producer',queue)
    consumer=Consumer('Consumer',queue)
    producer.start()
    consumer.start()
    producer.join()
    consumer.join()
    print('All threads finished!')
复制代码

队列Queue的put方法可以将一个对象放入队列中。如果队列已满,此方法将阻塞,直至Queue有空间可用为止。

Queue的get方法一次移除并返回队列中的一个成员。如果队列为空,此方法将阻塞,直至Queue中有成员可用为止。

此外,Queue同时还自带empty、full方法来判断一个队列是否为空或满,但这些方法并不可靠,因为多线程与多进程,在返回结果与使用结果之间,队列中可能添加/删除了成员。

 

4)多进程与多线程的使用场景

对于CPU密集型代码(比如循环计算)——多进程效率更高

对于IO密集型代码(如文件操作、爬虫)——多线程效率更高

对此的理解:

对IO密集型操作,大部分消耗的时间其实是等待时间,在等待时间中CPU是不需要工作的,因此在此期间即使提供更多的CPU资源也是用不上的。

对CPU密集型代码,两个CPU干活肯定比一个CPU快很多。

那么为什么多线程会对IO密集型代码有用呢?这是因为Python碰到等待时会释放GIL提供给新的线程使用,实现了线程间的切换。

4、常用方法与语句

os.getpid():当前进程的id

time.sleep(2):睡眠2s

from multiprocessing import cpu_count:cpu_count()获取CPU核数

threading.current_thread().name:当前线程名

Thread.setDaemon(True):守护线程

5、总结

1)多进程

利用Process产生单个进程

复制代码
from multiprocessing import Process
def func(args):
    ...#每个进程所执行的任务
#利用Process创建单个进程
if __name__=='__main__':
    P1=Process(target=func,args=(x,)) #将参数x传入任务函数,构成一个进程
    P2=Process(target=func,args=(y,)) #将参数y传入任务函数,构成另一个进程
    P1.start() #启动进程1
    P2.start() #启动进程2
    P1.join() #阻塞主进程
    P2.join() #阻塞主进程

#利用Pool产生多个进程

复制代码

 

利用Pool产生进程池

复制代码
from multiprocessing import Pool,cpu_count
def func(args):
    #进程执行的方法
    ...

if __name__=='__main__':
    p=Pool(n)#池大小,即一次最多并行运行的进程数
    for i in range(m):#一共m个任务,将它们加入进程池中
        p.apply_async(func,args=(xm,))#xm是每个进程对应的传入变量
    p.close()
    p.join()
复制代码

不同进程之间的运行是并行的,所以会大幅减少运算时间。

 

进程间的数据共享与通信:Queue

复制代码
from multiprocessing import Process,Queue

def funcW(q):#存函数,把数据存入队列q,把q作为参数传入函数
    ...
    for x in X:
        q.put(x)#将一系列x存入队列

def funcR(q):#取函数,用于从队列q中取数据,把q作为参数传入函数
    ...
    while True:#由于不确定q中数据数量,所以要用无限循环的方式取数据
        v=q.get()

if __name__=='__main__':
    q=Queue()
    pw=Process(target=funcW,args=(q,))
    pr=Process(target=funcR,args=(q,))
    pw.start()
    pr.start()
    pw.join()
    #当进程中有死循环时,用方法terminate进行手动终结,
    #时间在pw进程运行完毕之后(即pw的join方法之后)
   pr.terminate()
复制代码

 

2)多线程

利用Thread创建多线程,用法与Process相同

复制代码
from threading import Thread

def func(arg):
    #线程执行的函数
    ...

if __name__=='__main__':
    t1=Thread(target=func,args=(x,))
    t2=Thread(target=func,args=(y,))
    t1.start()
    t2.start()
    t1.join()
    t2.join()
复制代码

 

通过继承Thread类重写run方法创建新线程

复制代码
from threading import Thread

def func(args):
    #线程执行函数
    ...

#自定义线程类
class MyThread(Thread):
    def __init__(self,func,args,name=''):
        Thread.__init__(self)
        self.func=func
        self.args=args
        self.name=name #这里的name就是之前current_thread().name的内容
    def run(self):
        self.func(self.args[0])

if __name__=='__main__':
    threads=[]
    #通过for循环构建多线程
    for i in range(n):
        t=MyThread(func,(x,),name)#调用函数,传入参数,线程名
        threads.append(t)
    
    for t in threads:
        t.start()
    for t in threads:
        t.join()
复制代码

 

不同线程间的数据共享

Lock

复制代码
from threading import Lock,Thread

def funcW(sum,lock): #写函数
    lock.acquire()#加锁
    ...
    lock.release()#解锁

def funcR(sum,lock): #读函数
    lock.acquire()#加锁
    ...
    lock.release()#解锁

if __name__=='__main__':
    sum=0
    lock=Lock()
    thread_W=Thread(target=funcW,args=(sum,lock),name='Save')
    thread_R=Thread(target=funcR,args=(sum,lock),name='Read')
    thread_W.start()
    thread_R.start()
    thread_W.join()
    thread_R.join()
复制代码

Queue

复制代码
from queue import Queue
from threading import Thread

#读写类中的queue实际上是同一个queue
class Producer(Thread):
    def __init__(self,name,queue):
        Thread.__init__(self,name=name)
        self.queue=queue
    def run(self):
        for x in range(X):
            #...
            self.queue.put(x)#往queue中写数据
            #...

class Consumer(Thread):
    def __init__(self,name,queue):
        Thread.__init__(self,name=name)
        self.queue=queue
    def run(self):
        while True:
            ...
            val=self.queue.get()
            ...

if __name__=='__main__':
    queue=Queue()#将队列传入读写类中
    producer=Producer('Producer',queue)
    consumer=Consumer('Consumer',queue)
    producer.start()
    consumer.start()
    producer.join()
    consumer.terminate()
复制代码

 

posted @   ShineLe  阅读(523)  评论(0编辑  收藏  举报
编辑推荐:
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
阅读排行:
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
点击右上角即可分享
微信分享提示