目录
1. 并行与并发
- 并发指的是 CPU 交替执行不同任务的能力,把任务在不同的时间点交给处理器进行处理,在同一时间点,任务并不会同时运行
- 并行指的是多个核心同时执行多个任务的能力,把每一个任务分配给每一个处理器独立完成,在同一时间点,任务一定是同时运行
- 单核 CPU 只能并发,无法并行,换句话说,并行只可能发生在多核 CPU 中;在多核 CPU 中,并发和并行一般都会同时存在,它们都是提高 CPU 处理任务能力的重要手段
2. 同步与异步
- 同步是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,直到收到返回信息才继续执行下去。通过单个线程调用服务;该线程发送请求,在服务运行时阻塞,并且等待响应
- 异步是指进程不需要一直等下去,而是继续执行其它操作,不管其他进程的状态。当有消息返回时系统会通知进程进行处理,这样可以提高执行的效率。通过两个线程调用服务;一个线程发送请求,而另一个单独的线程接收响应
3. 进程与线程
- 进程是系统进行资源分配和调度的基本单位,是操作系统结构的基础,通俗的讲,一个进程对应一个程序,而这个程序有它的运行空间和系统资源,在计算机内部,每个进程使用的数据和状态都是独立的,也可以称一个进程就是一个任务
- 线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务
3.1 进程和线程的区别
- 进程是资源分配的最小单位,线程是程序执行的最小单位
- 线程必须在某个进程中执行
- 一个进程可包含多个线程,其中有且只有一个主线程
- 进程间相互独立,同一进程的各线程间共享资源,某进程内的线程在其它进程不可见
- 进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信
- 线程上下文切换比进程上下文切换要快得多
3.2 线程的类型
主线程、子线程、守护线程(后台线程)、前台线程
3.3 线程和进程的优劣
- 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信是以通信的方式进行
- 多进程程序更为安全,多线程程序中有一个线程出现问题,整个进程也就死掉了,而多进程的程序保障了一个进程死掉的时候不会影响到另外的一个进程
4. 进程
4.1 创建进程
进程的创建模块:模块multiprocessing的Process类
multiprocessing.Process(target, args, kwargs, name, group)
参数:
- group:该参数未进行实现,不需要传参
- target:为新建进程指定执行任务,指定一个函数
- name:为新建进程设置名称
- args:为 target 参数指定的参数传递非关键字参数
- kwargs:为 target 参数指定的参数传递关键字参数
方式1:通过Process类创建进程对象,使用start开启进程
import os
import multiprocessing
def test1():
print(f'test1, 进程号是:{os.getpid()}')
def test2():
print(f'test2, 进程号是:{os.getpid()}')
if __name__ == '__main__':
t1 = multiprocessing.Process(target = test1)
t1.start()
t2 = multiprocessing.Process(target = test2)
t2.start()
方式2:通过子类继承Process类
import os
from multiprocessing import Process
class Test(Process):
# 因为Process类本身也有__init__方法,这个子类相当于重写了这个方法
# 但这样就会带来一个问题,没有完全的初始化一个Process类,所以就不能使用从这个类继承的一些方法和属性
# 最好的方法就是将继承类本身传递给Process.__init__方法,完成这些初始化操作
def __init__(self, name, age, like):
Process.__init__(self)
self.name = name
self.age = age
self.like = like
def run(self):
print(f'name={self.name},age={self.age},like={self.like},进程号是:{os.getpid()}')
if __name__ == '__main__':
t = Test('张三', 18, '跑步')
t.start()
4.2 Process类常用属性和方法
- is_alive():判断进程实例是否还在执行
- join([timeout]): 是否等待进程实例执行结束,或等待多少秒
- start():启动进程实例(创建子进程)
- run():如果没有给定target参数,对这个对象调用start()方法时,就将执行对象中的run()方法
- terminate():中断该进程
- name:当前进程实例别名,默认为Process-N N为从1开始递增的整数
- daemon:和守护线程类似,通过设置该属性为 True,可将新建进程设置为“守护进程”
- pid:返回进程的 ID 号,大多数操作系统都会为每个进程配备唯一的 ID 号
例1:使用单进程
import os
import time
def test1():
for i in range(3):
print(f'a={i},进程号是:{os.getpid()}')
time.sleep(1)
def test2():
for i in range(3):
print(f'b={i},进程号是:{os.getpid()}')
time.sleep(1)
if __name__ == '__main__':
start_time = time.time() # 开始时间
test1()
test2()
now_time = time.time() # 结束时间
time = now_time - start_time
print(f'时间间隔:{time},进程号是:{os.getpid()}')
运行结果:进程号始终没变,说明是单进程执行的
例2:函数test1创建一个进程
import os
import time
import multiprocessing
def test1():
for i in range(3):
print(f'test1={i},进程号是:{os.getpid()}')
time.sleep(1)
def test2():
for i in range(3):
print(f'test2={i},进程号是:{os.getpid()}')
time.sleep(1)
if __name__ == '__main__':
start_time = time.time()
a = multiprocessing.Process(target=test1) # 创建一个进程
a.start() # 开启线程
test2()
now_time = time.time()
time = now_time - start_time
print(f'时间间隔:{time},进程号是:{os.getpid()}')
运行结果:进程94856执行了开始时间、test2函数、结束时间、最后打印print,进程94858执行了test1函数,这2个进程是同时执行的,所以最后的的间隔时间大概是3s
例3:test1、test2分别设置进程
import os
import time
import multiprocessing
def test1():
for i in range(3):
print(f'test1={i},进程号是:{os.getpid()}')
time.sleep(1)
def test2():
for i in range(3):
print(f'test2={i},进程号是:{os.getpid()}')
time.sleep(1)
if __name__ == '__main__':
start_time = time.time()
a = multiprocessing.Process(target=test1) # 创建2个进程
b = multiprocessing.Process(target=test2)
for i in (a, b): # 执行进程a和b
i.start()
now_time = time.time()
time = now_time - start_time
print(f'时间间隔:{time},进程号是:{os.getpid()}')
运行结果:进程94913执行了开始时间和结束时间,以及最后的打印时间间隔;进程94915执行了test1函数;进程94916执行了test2函数;这3个进程是同时执行的,所以打印的时间接近0s
例4:阻塞其中一个子进程,等待它执行结束后再执行其他进程
import os
import time
import multiprocessing
def test1():
for i in range(3):
print(f'test1={i},进程号是:{os.getpid()}')
time.sleep(1)
def test2():
for i in range(3):
print(f'test2={i},进程号是:{os.getpid()}')
time.sleep(1)
if __name__ == '__main__':
start_time = time.time()
a = multiprocessing.Process(target=test1)
a.start()
a.join() # 阻塞进程
b = multiprocessing.Process(target=test2)
b.start()
now_time = time.time()
time = now_time - start_time
print(f'时间间隔:{time},进程号是:{os.getpid()}')
运行结果:进程95178执行了test1函数,进程95176和进程95182是在进程95178执行结束后才开始同时执行的
例5:阻塞2个进程,等待他们执行结束后再执行其他进程
import os
import time
import multiprocessing
def test1():
for i in range(3):
print(f'test1={i},进程号是:{os.getpid()}')
time.sleep(1)
def test2():
for i in range(3):
print(f'test2={i},进程号是:{os.getpid()}')
time.sleep(1)
if __name__ == '__main__':
start_time = time.time()
a = multiprocessing.Process(target=test1)
b = multiprocessing.Process(target=test2)
for i in (a, b): # 同时开始执行2个子进程
i.start()
for j in (a, b): # 阻塞2个进程
i.join()
now_time = time.time()
time = now_time - start_time
print(f'时间间隔:{time},进程号是:{os.getpid()}')
运行结果:进程95278和进程95279是同时执行的,2个子进程执行结束后才开始执行进程95276
例6:杀死一个进程
import os
import time
import multiprocessing
def test1():
for i in range(3):
print(f'test1={i},进程号是:{os.getpid()}')
time.sleep(1)
def test2():
for i in range(3):
print(f'test2={i},进程号是:{os.getpid()}')
time.sleep(1)
if __name__ == '__main__':
start_time = time.time()
a = multiprocessing.Process(target=test1)
b = multiprocessing.Process(target=test2)
for i in (a, b): # 同时开始执行2个子进程
i.start()
a.kill() # 杀死进程
now_time = time.time()
time = now_time - start_time
print(f'时间间隔:{time},进程号是:{os.getpid()}')
运行结果:执行test1的进程被杀死,没有执行
4.3 创建多进程
# 方式1
from multiprocessing import Process
def show(name):
print("Process name is " + name)
if __name__ == "__main__":
process_1 = Process(target=show, args=('subprocess',))
process_1.start()
process_1.join()
# 方式2
from multiprocessing import Process
import time
class MyProcess(Process):
def __init__(self, name):
super(MyProcess, self).__init__()
self.name = name
def run(self):
print('process name :' + str(self.name))
time.sleep(1)
if __name__ == '__main__':
for i in range(3):
p = MyProcess(str(i))
p.start()
for i in range(3):
p.join()
4.4 多进程通信
进程之间不共享数据的。如果进程之间需要进行通信,则要用到Queue模块或者Pipe模块来实现
1)Queue
- Queue是多进程安全的队列,可以实现多进程之间的数据传递,它主要有两个函数put()和get()
- put() 用以插入数据到队列中,put有两个可选参数:blocked 和timeout。如果blocked为 True(默认值),并且timeout为正值,该方法会阻塞timeout指定的时间,直到该队列有剩余的空间。如果超时,会抛出Queue.Full异常。如果blocked为False,但该Queue已满,会立即抛出Queue.Full异常
- get()可以从队列读取并且删除一个元素,同样get有两个可选参数:blocked和timeout。如果blocked为True(默认值),并且 timeout为正值,那么在等待时间内没有取到任何元素,会抛出Queue.Empty异常。如果blocked为False,有两种情况存在,如果Queue有一个值可用,则立即返回该值,否则,如果队列为空,则立即抛出Queue.Empty异常
from multiprocessing import Process, Queue
def put(queue):
queue.put('Queue')
if __name__ == '__main__':
queue = Queue()
pro = Process(target=put, args=(queue,))
pro.start()
print(queue.get())
pro.join()
2)Pipe
- Pipe的本质是进程之间的用管道数据传递,而不是数据共享,pipe() 返回两个连接对象分别表示管道的两端,每端都有send()和recv()函数。如果两个进程试图在同一时间的同一端进行读取和写入,可能会损坏管道中的数据
from multiprocessing import Process, Pipe
def show(conn):
conn.send('Pipe')
conn.close()
if __name__ == '__main__':
parent_conn, child_conn = Pipe()
pro = Process(target=show, args=(child_conn,))
pro.start()
print(parent_conn.recv())
pro.join()
4.5 进程池
使用Pool模块,Pool 常用的方法如下:
- apply():同步执行(串行)
- apply_async():异步执行(并行)
- terminate():立刻关闭进程池
- join():主进程等待所有子进程执行完毕。必须在close或terminate()之后使用
- close():等待所有进程结束后,才关闭进程池
import multiprocessing
import time
def func(msg):
print("msg:", msg)
time.sleep(3)
print("end")
if __name__ == "__main__":
pool = multiprocessing.Pool(processes = 3) # 维持执行的进程总数为processes,当一个进程执行完毕后会添加新的进程进去
for i in range(5):
msg = "hello %d" %(i)
pool.apply_async(func, (msg, )) # 非阻塞式,子进程不影响主进程的执行,会直接运行到 pool.join()
# pool.apply(func, (msg, )) # 阻塞式,先执行完子进程,再执行主进程
print("~~~~~~~~~~~~~~~~~~~~~~")
pool.close() # 调用join之前,先调用close函数,否则会出错
pool.join()
print("Sub-process(es) done.") # 执行完close后不会有新的进程加入到pool,join函数等待所有子进程结束
5. 线程
- 线程也叫轻量级进程,是操作系统能够进行运算调度的最小单位,它被包涵在进程之中,是进程中的实际运作单位
- 线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其他线程共享进程所拥有的全部资源。一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行
- 添加线程可以使程序运行更快,它可与同属一个进程的其它线程共享进程所拥有的全部资源
- 主线程会等待所有的子线程结束后才结束
5.1 创建线程
方式1: 通过threading.Thread类创建线程
使用 threading 模块中 Thread 类的构造器创建线程,即直接对类 threading.Thread 进行实例化创建线程,并调用实例化对象的 start() 方法启动线程
Thread 类提供了如下的 __init__() 构造器,可以用来创建线程:
__init__(self, group=None, target=None, name=None, args=(), kwargs=None, *,daemon=None)
以上所有参数都是可选参数,即可以使用,也可以忽略。其中各个参数的含义如下:
- group:指定所创建的线程隶属于哪个线程组
- target:指定所创建的线程要调度的目标方法
- args:以元组的方式,为 target 指定的方法传递参数
- kwargs:以字典的方式,为 target 指定的方法传递参数
- daemon:指定所创建的线程是否为后代线程
import threading
import time
def test():
time.sleep(1)
print("ident:{}".format(threading.get_ident()))
if __name__ == "__main__":
thread = threading.Thread(target=test)
thread.start()
time.sleep(1)
方式2: 继承Thread类创建线程
继承 threading 模块中的 Thread 类创建线程类。即用 threading.Thread 派生出一个新的子类,将新建类实例化创建线程,并调用其 start() 方法启动线程
import threading
import time
class TestThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self):
for i in range(3):
print("ident:{}".format(threading.get_ident()))
time.sleep(1)
if __name__ == "__main__":
thread=TestThread()
thread.start()
for i in range(3):
print("ident:{}".format(threading.get_ident()))
time.sleep(1)
5.2 Thread类常用属性和方法
(manual:https://docs.python.org/3/library/threading.html)
- start():启动线程,等待CPU调度
- run():线程被cpu调度后自动执行的方法
- getName()、setName()和name:用于获取和设置线程的名称
- setDaemon():设置为后台线程(守候线程)或前台线程(默认是False,前台线程)。如果是后台线程,主线程执行过程中,后台线程也在进行,主线程执行完毕后,后台线程不论成功与否,均停止。如果是前台线程,主线程执行过程中,前台线程也在进行,主线程执行完毕后,等待前台线程执行完成后,程序才停止
- isDaemon()方法和daemon属性:是否为守护线程
- ident:获取线程的标识符,线程标识符是一个非零整数,只有在调用了start()方法之后该属性才有效,否则它只返回None
- is_alive():判断线程是否是激活的(alive)。从调用start()方法启动线程,到run()方法执行完毕或遇到未处理异常而中断这段时间内,线程是激活的
- join([timeout]):调用该方法将会使主调线程堵塞,直到被调用线程运行结束或超时。参数timeout是一个数值类型,表示超时时间,如果未提供该参数,那么主调线程将一直堵塞到被调线程结束
- current_thread():返回当前线程对象
- main_thread() :返回主线程对象
- active_count():当前处于alive状态的线程个数
- enumerate():返回所有活着的线程的列表,不包括已经终止的线程和未开始的线程
- get_ident():返回当前线程的ID,非0整数
注意⚠️s️tart()和run() 方法的区别:
- start()方法会调用run()方法,而run()方法可以运行函数
- 使用start方法启动线程,启动了一个新的线程
- 使用run方法,并没有启动新的线程,就是在主线程中调用了一个普通的函数而已
5.3 线程状态
- 就绪(Ready):线程能够运行,但在等待被调度。可能线程刚刚创建启动,或刚刚从阻塞中恢复,或者被其他线程抢占
- 运行(Running):线程正在运行
- 阻塞(Blocked):线程等待外部事件发生而无法运行,如I/0操作
- 终止(Terminated):线程完成,或退出,或被取消
5.4 创建多线程
方法1:直接使用threading.Thread()
import threading
def test(n):
print("current task: ", n)
print("ident:{}".format(threading.get_ident()))
if __name__ == "__main__":
t1 = threading.Thread(target=test, args=("thread 1",))
t2 = threading.Thread(target=test, args=("thread 2",))
t1.start()
t2.start()
方法2:继承threading.Thread来自定义线程类,重写run方法
import threading
class MyThread(threading.Thread):
def __init__(self):
super(MyThread, self).__init__() # 重构run函数必须要写
def run(self):
print("ident:{}".format(threading.get_ident()))
if __name__ == "__main__":
t1 = MyThread()
t2 = MyThread()
t1.start()
t2.start()
5.5 设置守护线程
线程是程序执行的最小单位,Python在进程启动起来后,会自动创建一个主线程,之后使用多线程机制可以在此基础上进行分支,产生新的子线程。子线程启动起来后,主线程默认会等待所有线程执行完成之后再退出。但是我们可以将子线程设置为守护线程,此时主线程任务一旦完成,所有子线程将会和主线程一起结束(就算子线程没有执行完也会退出)。
- 设置方式1:setDaemon(True)
- 设置方式2:创建线程时,以参数的形式指定:thread = Thread(target=target, name="thread_1", daemon=True)
5.6 设置线程阻塞
- 用join()方法使主线程陷入阻塞,以等待某个线程执行完毕。这也是实现线程同步的一种方式。参数timeout 用来设置主线程陷入阻塞的时间,如果线程不是守护线程,即没有设置daemon为True,那么参数timeout 无效,主线程会一直阻塞,直到子线程执行结束
- join函数执行顺序是逐个执行每个线程,执行完毕后继续往下执行。主线程结束后,子线程还在运行,join函数使得主线程等到子线程结束时才退出
import threading
def count(n):
while n > 0:
n -= 1
print("ident:{}".format(threading.get_ident()))
if __name__ == "__main__":
t1 = threading.Thread(target=count, args=(3,))
t2 = threading.Thread(target=count, args=(3,))
t1.start()
t2.start()
t1.join()
t2.join()
5.7 线程间通信
线程之间共享同一块内存,子线程虽然可以通过指定target来执行一个函数,但是这个函数的返回值是没有办法直接传回主线程的。使用多线程一般是用于并行执行一些其他任务,因此获取子线程的执行结果十分有必要,直接使用全局变量虽然可行,但是资源的并发读写会引来线程安全问题。下面给出常用的两种处理方式:线程锁和queue模块(同步队列类)。
1)线程锁
当多个线程对同一份资源进行读写操作时,可以通过加锁来确保数据安全。Python中给出了多种锁的实现,例如:同步锁 Lock,递归锁 RLock,条件锁 Condition,事件锁 Event,信号量锁 Semaphore等。
# 同步锁
import threading
import time
num = 0
mutex = threading.Lock()
class MyThread(threading.Thread):
def run(self):
global num
time.sleep(1)
if mutex.acquire(1):
num = num + 1
msg = self.name + ': num value is ' + str(num)
print(msg)
mutex.release()
if __name__ == '__main__':
for i in range(5):
t = MyThread()
t.start()
# 递归锁
import threading
import time
mutex = threading.RLock() #创建RLock
class MyThread(threading.Thread):
def run(self):
if mutex.acquire(1):
print("thread " + self.name + " get mutex")
time.sleep(1)
mutex.acquire()
mutex.release()
mutex.release()
if __name__ == '__main__':
for i in range(5):
t = MyThread()
t.start()
注:死锁是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
2)queue模块(同步队列类)
Python中的queue模块实现了多生产者、多消费者队列,特别适用于在多线程间安全的进行信息交换。该模块提供了4种队列容器,分别Queue(先进先出队列)、LifoQueue(先进后出队列)、PriortyQueue(优先级队列)、SimpleQueue(无界的先进先出队列)。
5.8 线程池
- 在程序运行过程之中,临时创建一个线程需要耗费不小的代价(包括与操作系统的交互部分),尤其是我们只对一个线程分配一个简短的任务,此时,频繁的线程创建将会严重拖垮程序的执行的效率
- 在这种情形下,我们可以选择采用线程池技术,即通过预先创建几个空闲线程,在需要多线程来处理任务时,将任务分配给一个处于空闲状态的线程,该线程在执行完成后,将会回归空闲状态,而不是直接销毁;而如果申请从线程池中分配一个空闲线程时,遇到所有线程均处于运行状态,则当前线程可以选择阻塞来等待线程资源的空闲,这样程序对于线程的管理将会更加灵活
- Python通过concurrent.futures.ThreadPoolExecutor来调用线程池
from concurrent.futures import ThreadPoolExecutor
from time import sleep
tasklist = ["任务1", "任务2", "任务3", "任务4"]
def task(taskname: str):
sleep(5)
print(taskname + " 已完成\n")
return taskname + " 的执行结果"
executor = ThreadPoolExecutor(max_workers=3) # 创建线程池(是一个ThreadPoolExecutor对象),线程数为3
future_a = executor.submit(task, tasklist[0]) # 通过submit方法向线程池提交任务,返回一个对应的Future对象
future_b = executor.submit(task, tasklist[1])
future_c = executor.submit(task, tasklist[2])
future_d = executor.submit(task, tasklist[3]) # 如果提交时,线程池中没有空余线程,则该线程会进入等待状态,主线程不会阻塞
print(future_a.result(), future_b.result()) # 通过Future对象的result()方法获取任务的返回值,若没有执行完,则会陷入阻塞
5.9 多线程和多进程
1)为什么要使用多线程?
- 线程在程序中是独立的、并发的执行流。与分隔的进程相比,进程中线程之间的隔离程度要小,它们共享内存、文件句柄和其他进程应有的状态
- 因为线程的划分尺度小于进程,使得多线程程序的并发性高。进程在执行过程之中拥有独立的内存单元,而多个线程共享内存,从而极大的提升了程序的运行效率
- 线程比进程具有更高的性能,这是由于同一个进程中的线程都有共性,多个线程共享一个进程的虚拟空间。线程的共享环境包括进程代码段、进程的共有数据等,利用这些共享的数据,线程之间很容易实现通信
- 操作系统在创建进程时,必须为改进程分配独立的内存空间,并分配大量的相关资源,但创建线程则简单得多。因此,使用多线程来实现并发比使用多进程的性能高得要多
- python语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了python的多线程编程
2)GIL 全局解释器
- 在非python环境中,单核情况下,同时只能有一个任务执行。多核时可以支持多个线程同时执行。但是在python中,无论有多少个核同时只能执行一个线程。究其原因,这就是由于GIL的存在导致的
- GIL的全程是全局解释器,来源是python设计之初的考虑,为了数据安全所做的决定。某个线程想要执行,必须先拿到GIL,可以把GIL看做是“通行证”,并且在一个python进程之中,GIL只有一个。拿不到线程的通行证,并且在一个python进程中,GIL只有一个,拿不到通行证的线程,就不允许进入CPU执行。GIL只在cpython中才有,因为cpython调用的是c语言的原生线程,所以他不能直接操作cpu,而只能利用GIL保证同一时间只能有一个线程拿到数据。而在pypy和jpython中是没有GIL的。python在使用多线程的时候,调用的是c语言的原生过程
3)python针对不同类型的代码执行效率不同
- CPU密集型代码(各种循环处理、计算等),在这种情况下,由于计算工作多,ticks技术很快就会达到阀值,然后触发GIL的释放与再竞争(多个线程来回切换当然是需要消耗资源的),所以python下的多线程对CPU密集型代码并不友好,CPU密集型,建议使用多进程
- IO密集型代码(文件处理、网络爬虫等设计文件读写操作),多线程能够有效提升效率(单线程下有IO操作会进行IO等待,造成不必要的时间浪费,而开启多线程能在线程A等待时,自动切换到线程B,可以不浪费CPU的资源,从而能提升程序的执行效率),所以python的多线程对IO密集型代码比较友好