python 多任务

一些概念

1.多任务

简单地说,就是同时可以运行多个任务。打个比方,你一边在用浏览器上网,一边在听MP3,一边在用Word赶作业,这就是多任务。

2.并行

指的是任务数小于等于cpu核数,在一段时间内真正的同时一起执行多个任务。每个核同时处理不同的任务,即任务真的是同时执行的

3.并发

在一段时间内交替去执行多个任务,对于单核CPU处理多任务,操作系统轮流让各个任务在cpu上交替执行(多个任务看起来是同时运行的

真正的"并行"只能在多核CPU上实现,现实中由于任务数量远远多于CPU的核心数量,所以基本上都是“并发”。 操作系统会自动把很多任务轮流调度到每个核心上执行。

4.串行

多个任务时,运行完一个再运行下一个

python实现多任务的方法

1.多线程

线程是执行程序的最小单位

使用threading模块

import threading
import time
 
def say_sorry():
    print("亲爱的,我错了,我能吃饭了吗?")
    time.sleep(1)
 

for i in range(5):
    t = threading.Thread(target=say_sorry)
    t.start()  # 启动线程,即让线程开始执行

1.1同时执行多个不同的任务:

1.如果在一个程序中需要有多个任务一起执行,可以将每个任务单独放到一个函数中
2.使用threading.Thread创建一个对象,注意实参target需要指定为刚刚定义的函数名(不要写上小括号,那表示调用函数了)
3.调用threading.Thread返回的对象中的start方法(会让这个线程开始运行)

import threading
from time import sleep, ctime


def sing():
    for i in range(3):
        print("正在唱歌...%d" % i)
        sleep(1)

        
def dance():
    for i in range(3):
        print("正在跳舞...%d" % i)
        sleep(1)

        

print('---开始---:%s' % ctime())
t1 = threading.Thread(target=sing)
t2 = threading.Thread(target=dance)
t1.start()
t2.start()
#sleep(5)  # 屏蔽此行代码,试试看,程序是否会立马结束?
print('---结束---:%s' % ctime())

1.2多线程执行的顺序不确定

当python程序中有多个任务需要被执行时,这些任务需要等待操作系统的调度(即操作系统安排接下来要执行哪个任务),因为每次运行程序时的环境(例如上次运行时 除了这个python程序之外还有QQ、微信在运行,而这次运行时没有QQ只有微信在运行都会影响操作系统的调度策略)不一样,所以多次运行同一个python程序时任务执行的先后顺序是不同的

1.3多线程-共享全局变量

1.在一个进程内的所有线程共享全局变量,很方便在多个线程间共享数据
2.缺点是:线程是对全局变量随意遂改可能造成多线程之间对全局变量的混乱(即线程非安全)

1.4互斥锁

  1. 为什么要用互斥锁?
    当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制
    线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。

  2. 互斥锁的作用
    互斥锁为资源引入一个状态:锁定/非锁定
    某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。

threading模块中定义了Lock类,可以方便的使用:

# 创建锁
mutex = threading.Lock()
# 锁定
mutex.acquire()
# 释放
mutex.release()

注意:
如果这个锁之前是没有上锁的,那么acquire不会堵塞(堵塞:理解为程序卡在这里等待某个条件满足)
如果在调用acquire对这个锁上锁之前 它已经被 其他线程上了锁,那么此时acquire会堵塞,直到这个锁被解锁为止

锁的好处:
确保了某段关键代码同时只能由一个线程从头到尾完整地执行

锁的坏处:
阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了
由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁

2.多进程

进程(Process) 是资源分配的最小单位,它是操作系统进行资源分配和调度运行的基本单位,
通俗理解: 一个正在运行的程序就是一个进程。例如:正在运行的qq,微信等,他们都是一个进程。
注意:一个正在运行的程序才叫进程,而没有运行的程序,只能叫程序,不能叫进程。同时,一个程序可以有一个或者多个进程。

多进程的作用:
同时执行多个函数,提升效率

进程注意事项:
主进程会等待所有的子进程完成才结束
#(2)设置守护主进程:每一个子进程都守护主进程,当主进程结束了之后,子进程直接结束,也就是被销毁。

2.1python多进程的实现

multiprocessing模块是跨平台版本的多进程模块,提供了一个Process类来创建一个进程对象,这个对象可以理解为是一个独立的进程,可以执行另外的事情

通过额外创建一个进程,可以实现多任务
使用进程实现多任务的流程:
创建一个Process对象,且在创建时通过target指定一个函数的引用
当调用start时,会真正的创建一个子进程

from multiprocessing import Process
import time
 
def test():
    """子进程单独执行的代码"""
    while True:
        print('---test---')
        time.sleep(1)
 
if __name__ == '__main__':
    p=Process(target=test)
    p.start()
    # 主进程单独执行的代码
    while True:
        print('---main---')
        time.sleep(1)

2.2进程不共享全局变量

进程间是相互独立的,数据不共享,但有时需要数据共享,就需要进程间通信(IPC)

2.3进程间通信-Queue

可以使用multiprocessing模块的Queue实现多进程之间的数据传递

from multiprocessing import Queue
q = Queue(3)  # 初始化一个Queue对象,最多可接收三条put消息
q.put("消息1")
q.put("消息2")
print(q.full())  # False
q.put("消息3")
print(q.full())  # True

# 因为消息列队已满,所以会导致下面的try都会抛出异常,
# 第一个try会等待2秒后再抛出异常
# 第二个Try会立刻抛出异常
try:
    q.put("消息4", True, 2)
except:
    print("消息列队已满,现有消息数量:%s" % q.qsize())

try:
    q.put_nowait("消息4")
except:
    print("消息列队已满,现有消息数量:%s" % q.qsize())

# 推荐的方式,先判断消息列队是否已满,再写入
if not q.full():
    q.put_nowait("消息4")

# 读取消息时,先判断消息列队是否为空,再读取
if not q.empty():
    for i in range(q.qsize()):
        print(q.get_nowait())

3.协程

协程,又称微线程

协程和线程差异
在实现多任务时, 线程切换从系统层面远不止保存和恢复 CPU上下文这么简单。 操作系统为了程序运行的高效性每个线程都有自己缓存Cache等等数据,操作系统还会帮你做这些数据的恢复操作。 所以线程的切换非常耗性能。但是协程的切换只是单纯的操作CPU的上下文,所以一秒钟切换个上百万次系统都抗的住。

简单实现协程

import time

def work1():
    while True:
        print("----work1---")
        yield
        time.sleep(0.5)

def work2():
    while True:
        print("----work2---")
        yield
        time.sleep(0.5)

def main():
    w1 = work1()
    w2 = work2()
    while True:
        next(w1)
        next(w2)

if __name__ == "__main__":
    main()

4.线程、进程对比

4.1关系对比

1.线程是依附在进程里的,没有进程就没有线程
2.一个进程默认提供一条线程,进程可以创建多个线程

4.2区别对比

1.创建进程的资源开销比创建线程的资源开销要大
2.进程是操作系统资源分配的进本单位,线程是cpu调度的基本单位(程序执行的最小单位)
3.线程不能独立执行,必须依附在进程中

4.3优缺点

1.进程优点:可以用多核
缺点:资源开销大

2.线程优点:资源开销小
缺点:不能使用多核

4.4多线程的优点:

无需跨进程边界;
程序逻辑和控制方式简单;
所有线程可以直接共享内存和变量等;
线程方式消耗的总资源比进程方式好;

4.5多线程缺点:

每个线程与主程序共用地址空间,受限于2GB地址空间;
线程之间的同步和加锁控制比较麻烦;
一个线程的崩溃可能影响到整个程序的稳定性;
到达一定的线程数程度后,即使再增加CPU也无法提高性能,例如Windows
Server
2003,大约是1500个左右的线程数就快到极限了(线程堆栈设定为1M),如果设定线程堆栈为2M,还达不到1500个线程总数;
线程能够提高的总性能有限,而且线程多了之后,线程本身的调度也是一个麻烦事儿,需要消耗较多的CPU

4.6 多进程优点:

每个进程互相独立,不影响主程序的稳定性,子进程崩溃没关系;
通过增加CPU,就可以容易扩充性能;
可以尽量减少线程加锁 / 解锁的影响,极大提高性能,就算是线程运行的模块算法效率低也没关系;
每个子进程都有2GB地址空间和相关资源,总体能够达到的性能上限非常大

4.7多线程缺点:

逻辑控制复杂,需要和主程序交互;
需要跨进程边界,如果有大数据量传送,就不太好,适合小数据量传送、密集运算
多进程调度开销比较大;
最好是多进程和多线程结合,即根据实际的需要,每个CPU开启一个子进程,这个子进程开启多线程可以为若干同类型的数据进行处理。当然你也可以利用多线程 + 多CPU + 轮询方式来解决问题……

补充

GIL 全局解释器锁
python有了GIL,为什么还有线程锁(互斥锁)?
GIL是限制同一个进程中只有一个线程进入Python解释器。。。。。
而线程锁是由于在线程进行数据操作时保证数据操作的安全性(同一个进程中线程之间可以共用信息,如果同时对数据进行操作,则会出现公共数据错误)
其实线程锁完全可以替代GIL,但是Python的后续功能模块都是加在GIL基础上的,所以无法更改或去掉GIL,这就是Python语言最大的bug…只能用多进程或协程改善,或者直接用其他语言写这部分

你可能感兴趣的:(python,多任务,进程,线程)