- 一、多进程
- 1.创建多进程的第一种方法
- 2.创建多进程的第二种方法(进程类)
- 3.查看进程id
- 4.进程池
- 二、多线程
- 1.基本概念
- 2.threading线程模块
- 3.创建多线程的第一种方法
- 4.创建多线程的第二个方法(线程类)
- 5.互斥锁
- 6.死锁
- 7.线程中的local
- 三、进程与进程、线程与线程之间的通信
- 四、多线程和多进程区别
from multiprocessing import Process
import time
def sing():
for i in range(1, 6):
print('我在唱歌第{}句歌词'.format(i))
time.sleep(1)
def dance():
for i in range(1, 6):
print('我在跳舞第{}段舞蹈'.format(i))
time.sleep(1)
if __name__ == '__main__':
t1 = Process(target=sing) # ------------target=函数名
t2 = Process(target=dance)
t1.start() # -------------------------启动该进程
t2.start()
2.创建多进程的第二种方法(进程类)
from multiprocessing import Process
import os, time
class SubProcess(Process):
def __init__(self, x):
super().__init__()
self.x = x
def run(self): # 将父类的run函数重写,进程启动时候调用此方法
for i in range(3):
print('启动进程', i, os.getpid())
time.sleep(1)
if __name__ == '__main__':
p = SubProcess(3)
p.start()
p1 = SubProcess(3)
p1.start()
'''
启动进程 0 22064
启动进程 0 10628
启动进程 1 22064
启动进程 1 10628
启动进程 2 22064
启动进程 2 10628
'''
3.查看进程id
import os
from multiprocessing import Process
def fun1(n):
print('参数为', n, '进程id为', os.getpid(), '父进程id为', os.getppid()) # 主进程id使用getppid方法
def fun2(n):
print('参数为', n, '进程id为', os.getpid(), '父进程id为', os.getppid()) # 子进程采用getpid方法
if __name__ == '__main__':
print('父进程:', os.getpid())
t1 = Process(target=fun1, args=(32,))
t2 = Process(target=fun2, args=(3232432,)) # 传入参数时候是按元组方式进行传入
t1.start()
t2.start()
"""
父进程: 22624
参数为 32 进程id为 23272 父进程id为 22624
参数为 3232432 进程id为 11100 父进程id为 22624
"""
4.进程池
4.1 为什么使用进程池
由于进程启动的开销比较大,使用多进程的时候会导致大量内存空间被消耗。为了防止这种情况发生可以使用进程池,(由于启动线程的开销比较小,所以不需要线程池这种概念,多线程只会频繁得切换cpu导致系统变慢,并不会占用过多的内存空间)
4.2 进程池中常用方法
函数名作用apply()同步执行(串行)apply_async()异步执行(并行)join()主进程等待所有子进程执行完毕。必须在close或terminate()之后。close()等待所有进程结束后,才关闭进程池。4.3 举例使用
from multiprocessing import Pool
import time
def downLoad(movie):
for i in range(5):
print(movie, '下载进度%.2f%%' % ((i + 1) / 5 * 100))
time.sleep(1)
return movie
def alert(name):
print(name, '下载完成!')
if __name__ == '__main__':
movies = ['蔡徐坤吃饭视频', '蔡徐坤洗澡视频', '蔡徐坤化妆视频', '蔡徐坤擦口红视频']
p = Pool(2)
for movie in movies:
p.apply_async(downLoad, args=(movie,), callback=alert) # -----------callback函数回调,执行完第一个函数毁掉
p.close()
p.join() # ----------------------------使主进程堵塞知道p进程执行完毕
"""
蔡徐坤吃饭视频 下载进度20.00%
蔡徐坤洗澡视频 下载进度20.00%
蔡徐坤吃饭视频 下载进度40.00%
蔡徐坤洗澡视频 下载进度40.00%
蔡徐坤洗澡视频 下载进度60.00%
蔡徐坤吃饭视频 下载进度60.00%
蔡徐坤洗澡视频 下载进度80.00%
蔡徐坤吃饭视频 下载进度80.00%
蔡徐坤吃饭视频 下载进度100.00%
蔡徐坤洗澡视频 下载进度100.00%
蔡徐坤化妆视频 下载进度20.00%
蔡徐坤洗澡视频 下载完成!
蔡徐坤擦口红视频 下载进度20.00%
蔡徐坤吃饭视频 下载完成!
蔡徐坤擦口红视频 下载进度40.00%
蔡徐坤化妆视频 下载进度40.00%
蔡徐坤擦口红视频 下载进度60.00%
蔡徐坤化妆视频 下载进度60.00%
蔡徐坤化妆视频 下载进度80.00%
蔡徐坤擦口红视频 下载进度80.00%
蔡徐坤化妆视频 下载进度100.00%
蔡徐坤擦口红视频 下载进度100.00%
蔡徐坤化妆视频 下载完成!
蔡徐坤擦口红视频 下载完成!
"""
二、多线程
1.基本概念
- 定义:多线程是指一个程序包含多个并行的线程来完成不同的任务
- 优点:可以提高cpu的利用率
在这里我们使用threading模块使用线程
import threading
该模块的一些常见方法
函数名作用threading.currentThread()返回当前的线程变量threading.enumerate()返回一个包含正在运行的现成的listthreading.activeCount ()求正在运行的线程的列表长度 3.创建多线程的第一种方法3.1 步骤
import threading
t=threading.Thread(
target= 函数名, # 需要执行的函数的名字
args=(参数1,参数2...), # 上述函数的参数,是个元组,无参数可不写,单个参数记住加,
callback=回调函数 # 不需要回调可不写
)
t.start() # 线程启动
3.2 举例:
import threading
import time
def sing():
for i in range(1, 6):
print('我在唱歌第{}句歌词'.format(i))
time.sleep(1)
def dance():
for i in range(1, 6):
print('我在跳舞第{}段舞蹈'.format(i))
time.sleep(1)
if __name__ == '__main__':
t1 = threading.Thread(target=sing)
t2 = threading.Thread(target=dance)
t1.start()
t2.start()
我们使用线程类的方法,必须要继承父类的init方法,这里我们有两种继承方式
调用父类的init方法有两种:
#方法1:
super().init()
#方法2:
super(MyThread, self).__init__()
#方法3:
threading.Thread.__init__(self)
然后我们需要重写主方法,run方法,最后用start调用
# -*- coding: utf-8 -*-
import time
from threading import Thread
class Test(Thread):
def __init__(self):
super().__init__()
def sing(self):
time.sleep(3)
print('鸡你太美~~~~~')
def run(self):
self.sing()
if __name__ == '__main__':
# 五个线程
for i in range(5):
Test().start()
该程序运行后,理论上五次循环需要15s左右,但是使用该线程类,会实现并发运行,约等于3s左右运行
5.互斥锁多个线程对公有变量处理时,容易造成数据的混乱,造成线程不安全
5.1 概念
互斥锁:当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制。线程同步能够保证多个线程安全访问“竞争资源”,最简单的同步机制就是引用互斥锁。互斥锁为资源引入一个状态:锁定/非锁定状态。某个线程要更改共享数据时,先将其锁定,此时资源状态为“锁定”,其它线程不能更改;直到当前线程释放资源,将资源变成"非锁定"状态,其它的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行“写操作”,从而保证多个线程数据正确性。
5.2 锁的特点
- 锁的好处:
- (1)确定了某段代码只能由一个线程从头到尾完整地执行。
- (2)全局变量的安全
- 锁的坏处:
- (1)阻止了多线程的并发执行,包含锁的某段代码实际上只能以单线程模块执行,效率大大地下降了。
- (2)由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁的时,可能会造成“死锁”。
5.3 使用步骤
from threading import Lock
l=LOCK() #创建方式
l.acquire()# 上锁方式
l.release()#-解锁方式
5.4 使用举例
举例:假设两个线程t1和t2都要对num进行+1操作,t1和t2都各自对num修改10次,num最终的值应该为20。紧接着我们把10次改为100000000次,由于多线程并发访问,有可能产生不一样的结果。 代码示例:
from threading import Thread
g_num = 0
def test1():
global g_num
for i in range(1000000):
# g_num += 1
b = g_num + 1
g_num = b
print("---test1---g_num=%d"%g_num)
def test2():
global g_num
for i in range(1000000):
a = g_num + 1
g_num = a
print("---test2---g_num=%d"%g_num)
if __name__ == '__main__':
p1 = Thread(target=test1)
p1.start()
p2 = Thread(target=test2)
p2.start()
#--------------------运行结果------------------------
---test2---g_num=1559989
---test1---g_num=1516811
问题分析:
问题产生的原因就是没有控制多个线程对同一资源的访问,对数据造成破坏,使得线程运行的结果达不到预期。这种现象我们称为“线程不安全”,解决思路(互斥锁):
- (1)t1被调用的时候,获取g_num=0,然后上一把锁,即不允许其它线程操作num。
- (2)对num进行加1
- (3)解锁,g_num = 1,其它的线程就可以使用g_num的值,而且g_num的值是而不是原来的0
- (4)同理其它线程在对num进行修改时,都要先上锁,处理完成后再解锁。在上锁的整个过程中,不允许其它线程访问,保证了数据的正确性。
加入互斥锁后可以得到:
import threading,time
g_num = 0#全局变量
def w1():
global g_num
for i in range(10000000):
mutexFlag = mutex.acquire(True)#上锁
if mutexFlag:
g_num+=1
mutex.release()#解锁
print("test1---g_num=%d"%g_num)
def w2():
global g_num
for i in range(10000000):
mutexFlag = mutex.acquire(True)# 上锁
if mutexFlag:
g_num+=1
mutex.release()# 解锁
print("test2---g_num=%d" % g_num)
if __name__ == "__main__":
mutex = threading.Lock()#创建锁
t1 = threading.Thread(target=w1)
t1.start()
t2 = threading.Thread(target=w2)
t2.start()
6.死锁
6.1 死锁产生的两种情况:
- (1)同一个线程先后两次调用lock,在第二次调用时,由于锁已经被自己占用,该线程会挂起等待自己释放锁,由于该线程已被挂起而没有机会释放锁,因此 它将一直处于挂起等待状态,变为死锁;
- (2)线程A获得了锁1,线程B获得了锁2,这时线程A调用lock试图获得锁2,结果是需要挂起等待线程B释放锁2,而这时线程B也调用lock试图获得锁1,结果是需要挂起等待线程A释放锁1,于是线程A和B都在等待对方释放自己才释放,从而造成两个都永远处于挂起状态,造成死锁。
6.2 举例
1.A拿了一个苹果
2.B拿了一个香蕉
3.A现在想再拿个香蕉,就在等待B释放这个香蕉
4.B同时想要再拿个苹果,这时候就等待A释放苹果
5.这样就是陷入了僵局,这就是生活中的死锁
6.3 概念
在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源时,就会造成死锁。尽管死锁很少发生,但一旦发生就会造成应用的停止响应。
6.4 产生死锁的必要条件
- ① 互斥条件:指线程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个线程占用。如果此时还有其它线程程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
- ② 请求和保持条件:指线程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求线程阻塞,但又对自己已获得的其它资源保持不放。
- ③不剥夺条件:指线程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
- ④环路等待条件:指在发生死锁时,必然存在一个线程——资源的环形链,即线程集{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。
6.5 死锁避免
理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和解除死锁。所以,在系统设计、线程调度等方面注意如何能够不让这四个必要条件成立,如何确定资源的合理分配算法,避免线程永久占据系统资源。此外,也要防止线程在处于等待状态的情况下占用资源,在系统运行过程中,对线程发出的每一个系统能够满足的资源申请进行动态检查,并根据检查结果决定是否分配资源,若分配后系统可能发生死锁,则不予分配,否则予以分配。因此,对资源的分配要给予合理的规划。
7.线程中的local线程里的local 线程依次取到值,执行函数复制
from threading import Thread
n = 0
def change_n(i):
global n
n = i
print(n)
if __name__ == '__main__':
for i in range(10):
t = Thread(target=change_n, args=(i,))
t.start()
# 结果:0 1 2 3 4 5 6 7 8 9
如果我们给函数加个等待时间,那么线程中都先取到n并且修改了n,当最后一个拿到值的会被输出
from threading import Thread
import time
n = 0
def change_n(i):
global n
n = i
time.sleep(1)
print(n)
if __name__ == '__main__':
for i in range(10):
t = Thread(target=change_n, args=(i,))
t.start()
#结果:9 9 9 9 9 9 9 9 9
threading.local的作用就是给每个线程单独的取进程中的一块儿地址空间进行数据存储
from threading import Thread,local
import time
n = local()
def change_n(i):
global n
n.value=i
time.sleep(1)
print(n.value)
if __name__ == '__main__':
for i in range(10):
t = Thread(target=change_n, args=(i,))
t.start()
# 0 8 5 2 3 7 1 4 9 6
三、进程与进程、线程与线程之间的通信
进程之间是各个独立的,我们如何让其通信呢?接下来我们使用Queue队列(是一种先进先出的存储数据结构)
举例:
from multiprocessing import Queue
a=Queue() # 队列的最大容纳量
a.put(1)#传入数据
a.put(2)
a.put(3)
while a.qsize()>0:#求出队列现在含有数据的长度,每取出一个数据长度会-1
print(a.get(),end=' ')#取出数据
# empty和full方法
b=Queue(3)
print(b.empty())#队列空返回True
b.put(1)
b.put(1)
b.put(1)
print(b.full())#队列满返回True
结果:1 2 3 True True
- 关于线程与线程之间可以借助
from queue import Queue
来实现,此处不举例。 - 进程与进程,线程与线程之间的通信借助队列来实现,一般常常配合redis来实现。
简略概括:线程的执行开销小,但不利于资源的管理和保存。进程正好相反。
-
①多线程的优点:
-
程序逻辑和控制方式复杂;
-
所有线程可以直接共享内存和变量;
-
线程方式消耗的总资源比进程方式好。
-
-
② 多线程缺点:
- 每个线程与主程序共用地址空间,受限于2GB地址空间;
- 线程之间的同步和加锁控制比较麻烦;
- 一个线程的崩溃可能影响到整个程序的稳定性;
-
③ 多进程优点:
- 每个进程互相独立,不影响主程序的稳定性,子进程崩溃没关系;
- 通过增加CPU,就可以容易扩充性能;
- 每个子进程都有2GB地址空间和相关资源,总体能够达到的性能上限非常大 。
-
④多线程缺点:
-
逻辑控制复杂,需要和主程序交互;
-
需要跨进程边界,如果有大数据量传送,就不太好,适合小数据量传送、密集运算 多进程调度开销比较大。
在实际开发中,选择多线程和多进程应该从具体实际开发来进行选择。最好是多进程和多线程结合,即根据实际的需要,每个CPU开启一个子进程,这个子进程开启多线程可以为若干同类型的数据进行处理。
-