Python的多线程与多进程

 1.前言

先需要理解-什么是进程(process)和线程(thread)

进程是操作系统分配资源的最小单元, 线程是操作系统调度的最小单元。 一个应用程序至少包括1个进程,而1个进程包括1个或多个线程,线程的尺度更小。 每个进程在执行过程中拥有独立的内存单元,而一个进程的多个线程在执行过程中共享内存。

所以说多个进程之间,不共享全局变量,具体的代码示例可以参考 

 代码示例1

import multiprocessing

a = 1

def demo1():
    global a
    a += 1

def demo2():
    # 打印出来的值是2 说明是共享的 线程是共享
    # 打印结果是1 进程是不共享的
    print(a)

def main():
    t1 = multiprocessing.Process(target=demo1)
    t2 = multiprocessing.Process(target=demo2)

    t1.start()
    t2.start()

if __name__ == '__main__':
    main()

# 运行结果
# >>> 1

代码示例2

import multiprocessing as mul_p
import time

egg1 = 1

def write(egg2, q):
    global egg1
    print(f"write全局变量彩蛋{egg1}...")
    print(f"write彩蛋{egg2}...")
    egg1 -= 1
    print(f"write全局变量彩蛋{egg1}...原来的彩蛋1")
    
    q.put(egg1)  # 将修改后的彩蛋1的值放入队列中去

def read(egg2, q):
    global egg1
    print(f"read全局变量彩蛋{egg1}...")
    print(f"read彩蛋{egg2}...")
    while True:
        # 从队列中取出 p1 子进程中的 全局变量彩蛋1 的值
        egg1 = q.get()
        print(f"read接收到的write中的全局变量彩蛋1的值:{egg1}")
        if q.empty():
            print("接收完毕...")
            break

def main():
    # 假设两个进程都需要打印下面这个彩蛋2
    egg2 = 2

    # ① 创建一个队列,可以不填,队列就可以很大,但有个极限,我们不去考虑它
    #   如果填了数字为 x ,则这个队列可以存储 x 个数据
    q = mul_p.Queue()

    # ② 创建两个进程对象
    p1 = mul_p.Process(target=write, args=(egg2, q,))
    p2 = mul_p.Process(target=read, args=(egg2, q,))

    # ③ 让两个子进程开始工作
    p1.start()
    # 先让主进程 休息1s 让 p1 子进程先执行完,不然两个子进程 争着执行 打印输出会乱套
    time.sleep(1)
    p2.start()

if __name__ == "__main__":
    main()

代码示例2运行结果如下:

write全局变量彩蛋1...
write彩蛋2...
write全局变量彩蛋0...原来的彩蛋1
read全局变量彩蛋1...
read彩蛋2...
read接收到的write中的全局变量彩蛋1的值:0
接收完毕...

总结:

1.各个进程间全局变量的值是 不共享 的,这是因为每创建一个进程就会 copy 一份原始代码(全局变量还是初值)给自己使用,所以进程间的代码是一样的,但 变量和数据是独立

2.各个进程间可以通过 Queue 创建的队列来传递变量,列表,字符串值(包括全局变量的值)

3.每个进程任务里的参数,除了 全局变量(以及函数局部变量),其余的参数都需要通过外部实参,传入到内部形参。尤其是上面例子的 队列 q 要作为 实参传给两个进程,这样才能实现两个进程间的通信,这种通过队列的形式进行通信的方式叫做消息队列

4.进程与线程之间的关系与区别:

  • 进程 包含 多个线程,
  • 进程间 不共用 变量与资源;
  • 线程间 共用 变量与资源,
  • 使用 time.sleep() ,可以停下当前的线程,若一个程序(进程)只有一个线程(主线程),则time.sleep()会停下(睡眠)整个进程

测试代码示例:

import time
from threading import Thread

class worker(Thread):
    def run(self):
	    for x in xrange(0,11):
		    print x
	    time.sleep(1)

class waiter(Thread):
	def run(self):
	    for x in xrange(100,103):
		    print x
	    time.sleep(5)

def run():
	worker().start()
	waiter().start()

运行结果

>>> thread_test.run()
0
100
>>> 1
2
3
4
5
101
6
7
8
9
10
102

这里再介绍一种进程间的通信方式,共享内存,在python里面multiprocessing模块给我们提供了Value(通常放一个整数)和Array(存放数组)模块,他们可以在不同的进程中,实现资源共享。除此之外还可以用mmap模块实现共享内存
更多详情访问Python 中的多进程(进程之间的通信)_python多进程通信_佛系的老肖的博客-CSDN博客

import time,multiprocessing

from multiprocessing import Value
def add1(val,number):
        print(f"start add1 number={number.value}")
        for i in range(1,5):
            number.value += val
            time.sleep(0.3)
            print(f"number={number.value}")

def add3(val,number):
    print(f"start add3 number={number.value}")
    try:
        for i in range(1,5):
            number.value += val
            time.sleep(0.3)
            print(f"number={number.value}")
    except Exception as e:
        raise e


if __name__ == '__main__':
    print("star main")
    number=Value('d',0)  #在内存分配一个空间  d表示数字类型,可支持的数据类型如下表
   #number= Array('i',3)  #表示开辟的共享内存容量为3,当再超过3时就会报错
    p1 = multiprocessing.Process(target=add1,args=(1,number))
    p3 = multiprocessing.Process(target=add3,args=(3,number))
    p1.start()
    p3.start()
    print("end main")

Value、Array所支持的数据类型

32fdd8f7b4c3436cbd9e657afa30c245.png

 下面切入多进程和多线程的正题

计算机如果是单核CPU,那么同一时刻,只能运行一个进程中的一个线程,这个叫做并发

多核CPU,可以在同一时刻并行执行一个或多个进程中的多个线程

f35a3e052acb49239d3b46bd94fc328e.png

python中实现多进程的模块是multiprocessing,下面将介绍如何用multiprocessing创建多个进程并发执行程序。下面的示例代码创建了两个进程,进程并发执行

from multiprocessing import Process
import os
import time

def long_time_task(i):
    print(f'子进程: {os.getpid()} - 任务{i}')
    time.sleep(2)
    print("结果: {}".format(8 ** 20))


if __name__=='__main__':
    print(f'当前母进程: {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(f"总共用时{(end - start)}秒")

start()方法开启进程,那么join方法的作用是什么呢

尽管我们只创建了两个进程,准确的来说是两个子进程。实际运行中包含1个母进程(主进程)和2个子进程。之所以我们使用join()方法就是为了让母进程阻塞,等待子进程都完成后才打印出总共耗时,否则输出时间只是母进程执行的时间。

2.多进程

利用multiprocess模块的Pool类创建多进程

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

下面介绍一下multiprocessing 模块下的Pool类的几个方法:

(1)apply_async

函数原型:apply_async(func[, args=()[, kwds={}[, callback=None]]])

其作用是向进程池提交需要执行的函数及参数, 各个进程采用非阻塞(异步)的调用方式,即每个子进程只管运行自己的,不管其它进程是否已经完成。

(2)map()

函数原型:map(func, iterable[, chunksize=None])

Pool类中的map方法,与内置的map函数用法行为基本一致,它会使进程阻塞直到结果返回。 注意:虽然第二个参数是一个迭代器,但在实际使用中,必须在整个队列都就绪后,程序才会运行子进程。

(3)map_async()

函数原型:map_async(func, iterable[, chunksize[, callback]]) 与map用法一致,但是它是非阻塞的。其有关事项见apply_async。

(4)close()

关闭进程池(pool),使其不在接受新的任务。

(5)terminate()

结束工作进程,不在处理未处理的任务。

(6)join()

主进程阻塞等待子进程的退出, join方法要在close或terminate之后使用。

示例代码

from multiprocessing import Pool, cpu_count
import os
import time

def long_time_task(i):
    print(f'子进程: {os.getpid()} - 任务{i}')
    time.sleep(2)
    print(f"结果: {8 ** 20}")


if __name__=='__main__':
    print(f"CPU内核数:{cpu_count()}")
    print(f'当前母进程: {os.getpid()}')
    start = time.time()
    p = Pool(4) 
    for i in range(5):
        p.apply_async(long_time_task, args=(i,))
    print('等待所有子进程完成。')
    p.close()      #注意close方法要在join方法之前调用
    p.join()
    end = time.time()
    print(f"总共用时{(end - start)}秒")

对Pool对象调用join()方法会等待所有子进程执行完毕,调用join()之前必须先调用close()或terminate()方法,让其不再接受新的Process了。

3.多线程

python解释器中存在GIL(全局解释器锁), 它的作用就是保证同一时刻只有一个线程可以执行代码。由于GIL的存在,很多人认为python中的多线程其实并不是真正的多线程,如果想要充分地使用多核CPU的资源,在python中大部分情况需要使用多进程。但多线程也有意义,对于计算密集型程序(比如循环计算),用多线程的效率一般低于多进程。对于IO密集型程序(比如文件操作,网络爬虫),多线程的效率一般高于多进程

为什么是这样呢?其实也不难理解。对于IO密集型操作,大部分消耗时间其实是等待时间,在等待时间中CPU是不需要工作的,那你在此期间提供双CPU资源也是利用不上的,相反对于CPU密集型代码,2个CPU干活肯定比一个CPU快很多。那么为什么多线程会对IO密集型代码有用呢?这时因为python碰到等待会释放GIL供新的线程使用,实现了线程间的切换。


Python的多线程编程与threading模块

Python的多线程实现是用threading模块,示例代码如下,注意实现主线程和子线程的同步(子线程执行完再执行主进程),我们必须使用join方法

import threading
import time

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


if __name__=='__main__':
    start = time.time()
    print(f'这是主线程:{threading.current_thread().name}')
    thread_list = []
    for i in range(1, 3):
        t = threading.Thread(target=long_time_task, args=(i, ))
        thread_list.append(t)

    for t in thread_list:
        t.start()

    for t in thread_list:
        t.join()

    end = time.time()
    print(f"总共用时{(end - start)}秒")

当我们设置多线程时,主线程会创建多个子线程,在python中,默认情况下主线程和子线程独立运行互不干涉。如果希望让主线程等待子线程实现线程的同步,我们需要使用join()方法。如果我们希望一个主线程结束时不再执行子线程,我们应该怎么办呢? 我们可以使用t.setDaemon(True),代码如下所示

import threading
import time

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


if __name__=='__main__':
    start = time.time()
    print(f'这是主线程:{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(f"总共用时{(end - start)}秒")
通过继承Thread类重写run方法创建新线程


除了使用Thread()方法创建新的线程外,我们还可以通过继承Thread类重写run方法创建新的线程,这种方法更灵活。下例中我们自定义的类为MyThread, 随后我们通过该类的实例化创建了2个子线程

import threading
import time

def long_time_task(i):
    time.sleep(2)
    return 8**20

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):
        print(f'开始子进程{self.name}')
        self.result = self.func(self.args[0],)
        print(f"结果: {self.result}")
        print(f'结束子进程{self.name}')


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()

    end = time.time()
    print(f"总共用时{(end - start)}秒")
线程池

Theading模块中,没有建立线程池的方法。可以使用threadpool模块和concurrent.futures模块(详情请看python 多线程 线程池的四种实现方式_threadpool python_伏逸的博客-CSDN博客

4.多进程和多线程的锁

多进程的锁,对于多个进程,适用对同一文件进行读写的场景

import time
import multiprocessing

def add1(lock, value, number):
    with lock:
        print(f"start add1 number= {number}")
        for i in range(1, 5):
            number += value
            time.sleep(0.3)
            print(f"number = {number}")


def add3(lock, value, number):
    lock.acquire()
    print(f"start add3 number= {number}")
    try:
        for i in range(1, 5):
            number += value
            time.sleep(0.3)
            print(f"number = {number}")
    except Exception as e:
        raise e
    finally:
        lock.release()
        pass


if __name__ == '__main__':
    print("start main")
    number = 0
    lock = multiprocessing.Lock()
    p1 = multiprocessing.Process(target=add1, args=(lock, 1, number))
    p3 = multiprocessing.Process(target=add3, args=(lock, 3, number))
    p1.start()
    p3.start()
    print("end main")

以上add1和add3方法使用锁,(with lock 和lock.acquire()),在主方法中,同时运行,谁先抢到进程,就把进程锁住,等待使用完毕后通过释放进程,其他方法才可以调整进程。

多线程的锁,对于多个线程,适用对同一变量进行操作的场景

一个进程所含的不同线程间共享内存,这就意味着任何一个变量都可以被任何一个线程修改,因此线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。如果不同线程间有共享的变量,其中一个方法就是在修改前给其上一把锁lock,确保一次只有一个线程能修改它。threading.lock()方法可以轻易实现对一个共享变量的锁定,修改完后release供其它线程使用。比如下例中账户余额balance是一个共享变量,使用lock可以使其不被改乱。

import threading

class Account:
    def __init__(self):
        self.balance = 0

    def add(self, lock):
        # 获得锁
        lock.acquire()
        for i in range(0, 100000):
            self.balance += 1
        # 释放锁
        lock.release()

    def delete(self, lock):
        # 获得锁
        lock.acquire()
        for i in range(0, 100000):
            self.balance -= 1
            # 释放锁
        lock.release()


if __name__ == "__main__":
    account = Account()
    lock = threading.Lock()
    # 创建线程
   thread_add = threading.Thread(target=account.add, args=(lock,), name='Add')
    thread_delete = threading.Thread(target=account.delete, args=(lock,), name='Delete')

    # 启动线程
   thread_add.start()
    thread_delete.start()

    # 等待线程结束
   thread_add.join()
    thread_delete.join()

    print(f'The final balance is: {account.balance}')

  • 26
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值