python多线程与多进程

六月ゝ 毕业季﹏ 提交于 2020-01-31 00:58:40

线程与进程

1.1 简介

说到线程就不得不提与之相关的另一概念:进程,那么什么是进程?与线程有什么关系呢?简单来说一个运行着的应用程序就是一个进程,比如:我启动了自己手机上的网易云音乐播放器,这就是一个进程,然后我随意点了一首歌曲进行播放,此时酷猫启动了一条线程进行音乐播放,听了一部分,我感觉歌曲还不错,于是我按下了下载按钮,此时网易云音乐又启动了一条线程进行音乐下载,现在网易云同时进行着音乐播放和音乐下载,此时就出现了多线程,音乐播放线程与音乐下载线程并行运行,说到并行,你一定想到了并发吧,那并行与并发有什么区别呢?并行强调的是同一时刻,并发强调的是一段时间内。线程是进程的一个执行单元,一个进程中至少有一条线程,进程是资源分配的最小单位,线程是 CPU 调度的最小单位。

线程一般会经历新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)5 种状态,当线程被创建并启动后,并不会直接进入运行状态,也不会一直处于运行状态,CPU 可能会在多个线程之间切换,线程的状态也会在就绪和运行之间转换。

1.2 python多线程与多进程模块

Python 提供了 _thread(Python3 之前名为 thread ) 和 threading 两个线程模块。_thread 是低级、原始的模块,threading 是高级模块,对 _thread 进行了封装,增强了其功能与易用性,绝大多数时候,我们只需使用 threading 模块即可。
Python 提供了 multiprocessing 模块对多进程进行支持,它使用了与 threading 模块相似的 API 产生进程,除此之外,还增加了新的 API,用于支持跨多个输入值并行化函数的执行及跨进程分配输入数据。

1.3 _thread模块和Threading模块

(1)_thread模块

_thread 模块是一个底层模块,功能较少,当主线程运行完毕后,如果不做任何处理,会立刻把子线程给结束掉,现实中几乎很少使用该模块。这里作为理解给出一个基本例子:

import time
import _thread

def worker(n):
    print('函数执行开始于:{}'.format(time.ctime()))
    time.sleep(n)
    print(f'函数执行结束于:{time.ctime()}')

def main():
    print(f'主函数执行开始于{time.ctime()}')
    _thread.start_new_thread(worker,(4,))
    _thread.start_new_thread(worker,(2,))
    print(f'主函数执行结束于{time.ctime()}')

if __name__=='__main__':
    main()
    
运行结果:
主函数执行开始于Tue Jan 28 12:50:35 2020
主函数执行结束于Tue Jan 28 12:50:35 2020
函数执行开始于:Tue Jan 28 12:50:35 2020函数执行开始于:Tue Jan 28 12:50:35 2020

由上面的程序可以看到,新线程只是开始了运行,不做其他处理时随着主线程的结束马上终止了。因此对于多线程开发推荐使用 threading 模块,threading 模块兼具了 _thread 模块的现有功能,又扩展了一些新的功能,具有十分丰富的线程操作功能。

(2)threading模块

1、创建线程

使用 threading 模块创建线程通常有两种方式:1)使用 threading 模块中 Thread 类的构造器创建线程,即直接对类 threading.Thread 进行实例化,并调用实例化对象的 start 方法创建线程;2)继承 threading 模块中的 Thread 类创建线程类,即用 threading.Thread 派生出一个新的子类,将新建类实例化,并调用其 start 方法创建线程。

1.1 构造器方式

调用 threading.Thread 类的如下构造器创建线程:

threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)
group:指定该线程所属的线程组,目前该参数还未实现,为了日后扩展 ThreadGroup 类实现而保留。
target:用于 run() 方法调用的可调用对象,默认是 None,表示不需要调用任何方法。
args:是用于调用目标函数的参数元组,默认是 ()。
kwargs:是用于调用目标函数的关键字参数字典,默认是 {}。
daemon:如果 daemon 是 True,该子线程将被显式的设置为守护模式,即随着主线程结束自动结束并退出程序进程。如果是 None (默认值)或False,该子线程将完成后再退出进程。

示例如下:

import threading
import time


def worker(n):
    print('{}函数执行开始于:{}'.format(threading.current_thread().name,time.ctime()))
    time.sleep(n)
    print(f'{threading.current_thread().getName()}函数执行结束于:{time.ctime()}')


def main():
    print(f'主函数执行开始于{time.ctime()}')
    threads = []
    t1 = threading.Thread(target=worker, args=(4,))
    threads.append(t1)
    t2 = threading.Thread(target=worker, args=(2,))
    threads.append(t2)

    for t in threads:
        t.start()

    for t in threads:
        t.join()

    print(f'主函数执行结束于{time.ctime()}')


if __name__ == '__main__':
    main()

运行结果:
主函数执行开始于Tue Jan 28 13:27:15 2020
Thread-1函数执行开始于:Tue Jan 28 13:27:15 2020
Thread-2函数执行开始于:Tue Jan 28 13:27:15 2020
Thread-2函数执行结束于:Tue Jan 28 13:27:17 2020
Thread-1函数执行结束于:Tue Jan 28 13:27:19 2020
主函数执行结束于Tue Jan 28 13:27:19 2020

上述示例中,start()方法开启线程,join()方法阻塞主线程,等待子线程结束后再继续运行主线程。threading.current_thread()的name属性或者getName()方法获取当前子线程名称。

1.2 继承方式

import threading
import time


def worker(n):
    print('{}函数执行开始于:{}'.format(threading.current_thread().name, time.ctime()))
    time.sleep(n)
    print(f'{threading.current_thread().getName()}函数执行结束于:{time.ctime()}')


class Mythread(threading.Thread):
   #重写__init__方法
    def __init__(self, func, args):
        threading.Thread.__init__(self)
        self.func = func
        self.args = args

    # 重写run方法,将self.args元组解包
    def run(self):
        self.func(*self.args)


def main():
    print(f'主函数执行开始于{time.ctime()}')
    threads = []
    t1 = Mythread(worker, (4,))
    threads.append(t1)
    t2 = Mythread(worker, (2,))
    threads.append(t2)

    for t in threads:
        t.start()

    for t in threads:
        t.join()

    print(f'主函数执行结束于{time.ctime()}')

运行结果
主函数执行开始于Tue Jan 28 13:50:55 2020
Thread-1函数执行开始于:Tue Jan 28 13:50:55 2020
Thread-2函数执行开始于:Tue Jan 28 13:50:55 2020
Thread-2函数执行结束于:Tue Jan 28 13:50:57 2020
Thread-1函数执行结束于:Tue Jan 28 13:50:59 2020
主函数执行结束于Tue Jan 28 13:50:59 2020

(3) 同步原语之锁

在不同线程使用同一共享内存时,能够确保线程之间互不影响,使用threading模块锁的机制,即在每个线程执行运算修改共享内存之前,执行lock.acquire()将共享内存上锁, 确保当前线程执行时,内存不会被其他线程访问,执行运算完毕后,使用lock.release()将锁打开, 保证其他的线程可以使用该共享内存。

import random
import threading
import time

eggs = []
lock = threading.Lock()


def put_egg(n, lst):
    #lock.acquire()
    with lock:
	    for i in range(1, n + 1):
	        time.sleep(random.randint(0, 2))
	        lst.append(i)
    #lock.release()


def main():
    threads = []
    for i in range(3):
        t = threading.Thread(target=put_egg, args=(5, eggs))
        threads.append(t)
    for t in threads:
        t.start()
    for t in threads:
        t.join()
    print(eggs)


if __name__ == '__main__':
    main()

运行结果
[1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5]

(4)队列

1、简介

Python 中的 Queue 模块实现了多生产者和多消费者模型,当需要在多线程编程中非常实用。而且该模块中的 Queue 类实现了锁原语,不需要再考虑多线程安全问题。该模块内置了三种类型的 Queue,分别是queue.Queue(maxsize=0),queue.LifoQueue(maxsize=0) 和 queue.PriorityQueue(maxsize=0)。其中 maxsize 是个整数,用于设置可以放入队列中的任务数的上限。当达到这个大小的时候,插入操作将阻塞至队列中的任务被消费掉。如果 maxsize 小于等于零,则队列尺寸为无限大。它们三个的区别仅仅是取出时的顺序不一致。

Queue 是一个 FIFO(先进先出) 队列,任务按照添加的顺序被取出。
LifoQueue 是一个 LIFO(后进先出)队列,类似堆栈,后添加的任务先被取出。
PriorityQueue 是一个优先级队列,队列里面的任务按照优先级排序,优先级高的先被取出。
1、如果是内置类型,比如数值或者字符串,则按照自然顺序来比较排序。
2、如果是列表或者元组,则先比较第一个元素,然后比较第二个,以此类推,直到比较出结果。直到在比较出结果之前,对应下标位置的元素类型都是需要一致的。

2、常用操作

import queue
q=queue.Queue()
#添加操作
q.put(100,block=True,timeout=None)
#获取任务
q.get(block=True,timeout=None)

block默认为True,如果 timeout 是正数,则最多阻塞 timeout 秒,如果这段时间内还没有空余的位置出来,则会引发 Full 异常。当 block 为 false 时,timeout 参数将失效。同时如果队列中没有任务可获取则会立刻引发 Full或Empty 异常,否则会直接获取一个任务并返回,不会阻塞。

队列运用—生产者消费者模型

import queue
import random
import threading
import time
def producer(data_queue):
    for i in range(5):
        time.sleep(0.5)
        item = random.randint(1, 100)
        data_queue.put(item)
        print(f'{threading.current_thread().name}在队列中放入数据:{item}')
    #生产者调用此方法进行阻塞,直到队列中所有的项目均被处理。阻塞将持续到队列中的每个项目均调用q.task_done()方法为止
	data_queue.join()

def consumer(data_queue):
    while True:
        item = data_queue.get(timeout=3)
        print(f'{threading.current_thread().name}从队列中移除了:{item}')
        #使用者使用此方法发出信号,表示q.get()的返回项目已经被处理。如果调用此方法的次数大于从队列中删除项目的数量,将引发ValueError异常
        data_queue.task_done()
def main():
    q = queue.Queue()
    threads = []
    p = threading.Thread(target=producer, args=(q,))
    p.start()

    for i in range(2):
        c = threading.Thread(target=consumer, args=(q,))
        #生产者线程被队列阻塞,消费者处理完队列项目才会解除阻塞,所以将消费者线程设置为守护线程。
        c.daemon=True
        threads.append(c)

    for t in threads:
        t.start()
    #阻塞主线程直至生产者线程结束
	p.join()

if __name__ == '__main__':
    main()
 #主线程等--->p等---->c
 #生产者线程结束了,证明消费者线程肯定全都收完了生产者线程发到队列的数据
 #因而c也没有存在的价值了,应该随着主进程的结束而结束,所以设置成守护线程

运行结果
Thread-1在队列中放入数据:71
Thread-2从队列中移除了:71
Thread-1在队列中放入数据:47
Thread-3从队列中移除了:47
Thread-1在队列中放入数据:66
Thread-2从队列中移除了:66
Thread-1在队列中放入数据:99
Thread-3从队列中移除了:99
Thread-1在队列中放入数据:1
Thread-2从队列中移除了:1

(5)多进程实现

Python 的多进程通过 multiprocessing 模块的 Process 类实现,它的使用基本与 threading 模块的 Thread 类一致。由于 threading 多线程模块无法充分利用电脑的多核优势,而在实际开发中会对系统性能有较高的要求,就需要使用多进程来充分利用多核 cpu 的资源。

多线程与多进程优劣势比较
(1)计算密集型任务
多线程执行:

from threading import Thread
import os,time

def task():
    ret = 0
    for i in range(100000000):
        ret *= i
if __name__ == '__main__':
    arr = []
    print('本机为',os.cpu_count(),'核 CPU')
    start = time.time()
    for i in range(5):
        p = Thread(target=task)
        arr.append(p)
        p.start()
    for p in arr:
        p.join()
    stop = time.time()
    print('多线程耗时 {}' .format(stop - start))

运行结果:
本机为 4 核 CPU
多线程耗时 24.358705043792725

多进程执行:

from multiprocessing import Process
import os,time

def task():
    ret = 0
    for i in range(100000000):
        ret *= i
if __name__ == '__main__':
    arr = []
    print('本机为',os.cpu_count(),'核 CPU')
    start = time.time()
    for i in range(5):
        p = Process(target=task)
        arr.append(p)
        p.start()
    for p in arr:
        p.join()
    stop = time.time()
    print('计算密集型任务,多进程耗时{}'.format(stop - start))

运行结果:
本机为 4 核 CPU
计算密集型任务,多进程耗时14.793508052825928

(2)I/O密集型
多线程

from threading import Thread
import os,time

def task():
    f = open('tmp.txt','w')
if __name__ == '__main__':
    arr = []
    print('本机为',os.cpu_count(),'核 CPU')
    start = time.time()
    for i in range(500):
        p = Thread(target=task)
        arr.append(p)
        p.start()
    for p in arr:
        p.join()
    stop = time.time()
    print('I/O 密集型任务,多线程耗时{}'.format(stop - start))

运行结果:
本机为 4 核 CPU
I/O 密集型任务,多进程耗时0.17698907852172852

多进程

from multiprocessing import Process
import os,time

def task():
    f = open('tmp.txt','w')
if __name__ == '__main__':
    arr = []
    print('本机为',os.cpu_count(),'核 CPU')
    start = time.time()
    for i in range(500):
        p = Process(target=task)
        arr.append(p)
        p.start()
    for p in arr:
        p.join()
    stop = time.time()
    print('I/O 密集型任务,多进程耗时{}'.format(stop - start))

运行结果:
本机为 4 核 CPU
I/O 密集型任务,多进程耗时94.58371019363403

可以看出,在CPython解释器的环境下,在执行计算密集型任务时,多进程效率高于多线程,而执行I/O密集型任务时,多线程效率高于多进程。对于一个运行的程序来说,随着 CPU 的增加执行效率必然会有所提高,因此大多数时候,一个程序不会是纯计算或纯 I/O,所以我们只能相对的去看一个程序是计算密集型还是 I/O 密集型。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!