线程与进程
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 密集型。
来源:CSDN
作者:一个支点a
链接:https://blog.csdn.net/weixin_41321563/article/details/104092359