Python基础——1.10并发\行-多进程-队列和管道-多线程-作业调度算法

一、多任务

之前所学程序都为单任务,一个函数或方法执行完另一个再执行。实现程序同时执行须使用多任务技术。多任务就是同时执行多个任务,这能充分利用cpu资源而提高程序的执行效率。

1.并发

**当任务数量大于cpu核心数时执行并发。既在一段时间内交替的执行多个任务。**单核cpu处理多任务时操作系统轮流让各任务交替去执行,软件1执行0.01秒,然后软件2执行0.01秒……cpu执行速度快,人眼感觉是同时在执行,对于单核cpu是并发处理多任务。

2.并行

**当任务数量小于或等于cpu核心数时执行并行。在一段时间内同时执行多个任务。**对于多个cpu而言,操作系统会给每一个cpu内核安排一个任务,多个内核同时执行多个任务,对于并行来说做到了真正的同时执行多个任务。

二、多进程

在Python中实现多任务可以使用多进程来完成。

进程:操作系统提供的抽象概念,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。

程序是指令、数据及其组织形式的描述,进程是程序的实体。程序本身没有生命周期,只是存在磁盘上的一些指令,程序一旦运行就是进程。

每个进程都有单独地址空间,包括:

  • 文本区域(text region):存储处理器执行的代码
  • 数据区域(data region):存储变量和进程执行期间使用的动态分配的内存
  • 堆栈(stack region):存储着活动过程调用的指令和本地变量

利用multiprocessing实现多进程。multiprocessing是一个支持使用与threading模块类似的API来产生进程的包。 multiprocessing 包同时提供了本地和远程并发操作,通过使用子进程而非线程有效地绕过全局解释器锁。 因此multiprocessing 模块允许程序员充分利用给定机器上的多个处理器。Unix和Windows均可运行。

1.Process属性方法

run()

进程启动时运行的方法,正是它去调用target指定的函数,我们自定义类的类中一定要实现该方法 。

terminate()

强制终止进程,不会进行任何清理操作。如果该进程终止前,创建了子进程,那么该子进程在其强制结束后变为僵尸进程;如果该进程还保存了一个锁那么也将不会被释放,进而导致死锁。使用时,要注意。

is_alive()

判断某进程是否存活,存活返回True,否则False。

join([timeout])

主线程等待子线程终止。timeout为可选择超时时间;需要强调的是,p.join只能join住start开启的进程,而不能join住run开启的进程 。

daemon

默认值为False,如果设置为True,代表该进程为后台守护进程;当该进程的父进程终止时,该进程也随之终止;并且设置为True后,该进程不能创建子进程,设置该属性必须在start()之前

pid

进程pid

exitcode

进程运行时为None,如果为-N,表示被信号N结束了。

authkey

进程身份验证,默认是由os.urandom()随机生成32字符的字符串。这个键的用途是设计涉及网络连接的底层进程间的通信提供安全性,这类连接只有在具有相同身份验证才能成功。

2.实现多进程

  • 导入进程包——————————import multiprocessing
  • 通过进程类创建进程对象————进程对象 = multiprocessing.Process()
  • 启动进程执行目标任务—————进程对象.start()

Process(target(子进程方法名), args(给target函数按照位置传参), kwargs(给target函数按照字典传参))

Process([group [, target [, name [, args [, kwargs]]]]])
# 注意:
1. 必须使用关键字方式来指定参数。
2. args指定的为传给target函数的位置参数,是一个元祖形式,必须有逗号。

参数介绍

  • group:参数未使用,默认值为None。
  • target:表示调用对象,即子进程要执行的任务。
  • args:表示调用的位置参数元祖。
  • kwargs:表示调用对象的字典。如kwargs = {‘name’:Jack, ‘age’:18}。
  • name:子进程名称。
  • 返回值:实例化对象

方法区别

1)进程有延迟

start()延迟执行

2)join阻塞主进程

join()仅影响主进程

  • run():表示进程活动的方法。仅执行方法不产生进程。先执行run()开始程序,然后用start()启动进程活动。run()在所有进程结束后结束。

  • start():启动进程活动。这个方法每个进程对象最多只能调用一次。它会将对象的 run() 方法安排在一个单独的进程中调用。

    start()延迟执行

    主程序中若单有print则优先执行print语句(若执行语句够多,则可见子进程执行):

    p = Process(target=task, args=("常辛",))  # 创建一个进程对象
    p.start()
    # p.join()
    print(111)
    time.sleep(0.5)  # 停留时间过长,会出现“执行子进程的情况”
    print("主开始")
        
    # 111
    # 常辛 is running
    # 主开始
    # 常辛 is gone
    
  • join([timeout]):若可选参数为None(默认值),则阻塞主进程到调用join()的对象的进程(子进程)终止;若timeout为正则最多阻塞timeout秒,进程终止或方法超时时返回None(可检查进程的 exitcode 以确定是否终止)。一个进程可被 join 多次。进程无法join自身(会导致死锁)。不能在启动进程之前join进程。

    from multiprocessing import Process
    import time
    
    
    def task(name):
        print(f"{name} is running")
        time.sleep(2)
        print(f"{name} is gone")
    
    
    if __name__ == "__main__":
        p = Process(target=task, args=("常辛",))  # 创建一个进程对象
        p.start() # 子进程
        # p.join()  设置子进程进行完后再执行主进程
        print(111)
        print("主开始")
    
    注释掉p.join()
    # 111
    # 主开始
    # 常辛 is running
    # 常辛 is gone
    不注释p.join()
    # 常辛 is running
    # 常辛 is gone
    # 111      
    # 主开始
    

    join()仅影响主进程

    如果程序中有连续多个join函数则执行先执行这些有join函数的子进程:

    from multiprocessing import Process
    import time
    
    
    def task(name, sec):
        print(f"{name} is running")
        time.sleep(sec)
        print(f"{name} is gone")
    
    
    if __name__ == "__main__":
        start_time = time.time()
        p = Process(target=task, args=("常", 3))  # 创建一个进程对象
        p1 = Process(target=task, args=("辛", 2))  # 创建一个进程对象
        p2 = Process(target=task, args=("王", 5))  # 创建一个进程对象
        p.start()
        p1.start()
        p2.start()
        # join只针对主进程,若join下面多次join不影响其他子进程进行,只影响主进程,且与子进程执行顺序无关。
        # p.join()
        # p1.join()
        # p2.join()
        print(f"主{time.time()} - {start_time}")  
        # 0.02这只是主进程结束的时间
    
    注释后
    # 主1668600243.7327797 - 1668600243.6946826
    # 常 is running
    # 辛 is running
    # 王 is running
    # 辛 is gone
    # 常 is gone
    # 王 is gone
    注释前
    # 常 is running
    # 辛 is running
    # 王 is running
    # 辛 is gone
    # 常 is gone
    # 王 is gone
    # 主1668600522.1649804 - 1668600517.03816
    

    如果非连续多个,如:

    p.start()
    p.join()
    p1.start()
    p1.join()
    p2.start()
    p2.join()
    print(f"主{time.time()} - {start_time}")
        
    # 常 is running
    # 常 is gone
    # 辛 is running
    # 辛 is gone
    # 王 is running
    # 王 is gone
    # 主1668601408.4629126 - 1668601398.212321
    
    # 这样就会先执行p进程,再执行p1进程,再执行p2进程
    

    若将p1、p2注释掉:

    p.start()
    p.join()
    p1.start()
    # p1.join()
    p2.start()
    # p2.join()
    print(f"主{time.time()} - {start_time}")
    
    # 常 is running
    # 常 is gone
    # 主1668601776.635927 - 1668601773.5203516
    # 辛 is running
    # 王 is running
    # 辛 is gone
    # 王 is gone
    

多进程缩短运行时间

from multiprocessing import Process
import time


def task(sec):
    print(f"is running")
    time.sleep(sec)
    print(f"is gone")
# 错误写法:
if __name__ == "__main__":
    start_time = time.time()
    for i in range(1, 4):
        p = Process(target=task, args=(i,))  # 创建一个进程对象
        p.start()
        p.join()  # 此处join()会导致同一时间仅有一个进程运行,并不是多任务
    print(f"主{time.time()} - {start_time}")

# is running
# is gone
# is running
# is gone
# is running
# is gone
# 主1668602185.3978484 - 1668602179.1436777
# 正确写法:	
if __name__ == "__main__":
    start_time = time.time()
    l1 = []
    for i in range(1, 4):
        p = Process(target=task, args=(i,))  # 创建一个进程对象
        l1.append(p)
        p.start()
    for i in l1:   # 列表的设立使得每个子进程都要在主进程之前完成,同时保证了多任务
        i.join()
    print(f"主{time.time()} - {start_time}")

# is running
# is running
# is running
# is gone
# is gone
# is gone
# 主1668602245.0995164 - 1668602241.981574

3)子先主后

主进程会等待所有子进程结束之后再结束,虽然子进程之外的代码可能会比子进程提前结束,但这些代码和子进程全部结束才说是主进程真正意义上的结束。但通常把子进程之外的代码称为主进程。

import multiprocessing
import time


def work():
    for i in range(5):
        print('正在工作中====')
        time.sleep(0.2)


if __name__ == '__main__':
    # 创建子进程
    sub = multiprocessing.Process(target=work)
    # 启动子进程
    sub.start()
    time.sleep(1)
    print('主进程中代码执行完毕')

4)进程不共享全局变量

import multiprocessing

# 定义全局变量
my_list = []


def write_data():
    """向my_list里面添加数据"""
    for i in range(3):
        my_list.append(i)
        print('向列表里面添加了:', i)
    print('这是写数据的子进程的my_list列表:', my_list)


def read_data():
    """读my_list里面的数据"""
    print('这是读数据的子进程读到的my_list列表', my_list)


if __name__ == '__main__':
    write_process = multiprocessing.Process(target=write_data)
    read_process = multiprocessing.Process(target=read_data)
    write_process.start()
    read_process.start()
    write_process.join()
    read_process.join()
    if not my_list:
        print("主进程中my_list为空")
    else:
        for j in my_list:
            print(j)

# 向列表里面添加了: 0
# 向列表里面添加了: 1
# 向列表里面添加了: 2
# 这是写数据的子进程的my_list列表: [0, 1, 2]
# 这是读数据的子进程读到的my_list列表 []
# 主进程中my_list为空

总结: 三个进程之间操作的是自己进程中的全局变量my_lsit,不会对其他进程中的全局变量产生影响,所以进程之间不共享全局变量,只不过是进程之间的my_list名字一样而已,但是操作的不是同一对象。

5)有参多任务

进程中执行有参的多任务

  • args() 以元组的方式传参
  • kwargs() 以字典的方式传参
# 导入进程包
import multiprocessing
import time


def coding(num):
    """编写代码的任务"""
    for i in range(num):
        print('正在编写代码====')
        time.sleep(1)


def music(num):
    """听音乐的任务"""
    for i in range(num):
        print('正在听音乐===')
        time.sleep(1)


if __name__ == '__main__':
    # coding()
    # music()
    # 创建进程对象
    coding_process = multiprocessing.Process(target=coding, args=(3,))
    music_process = multiprocessing.Process(target=music, kwargs={"num": 5})
    # 启动子进程
    coding_process.start()
    music_process.start()

# 正在编写代码====
# 正在听音乐===
# 正在编写代码====
# 正在听音乐===
# 正在编写代码====
# 正在听音乐===
# 正在听音乐===
# 正在听音乐===
  • 以元组方式传参:元组方式传参一定要和参数的顺序保持一致
  • 以字典方式传参:字典方式传参字典中的key一定要和目标任务中的形参保持一致,顺序没有要求。

6)进程编号

程序中为区别主进程和子进程方便管理,每个进程都有一个进程编号。获取进程编号的方式:

  • 获取当前进程编号: os.getpid()
  • 获取当前父进程的编号: os.getppid()
# 导入进程包
import multiprocessing
import time
import os


def coding():
    """编写代码的任务"""
    print('编写代码这个子进程的进程编号是:', os.getpid())
    print('编写代码这个子进程的父进程编号是:', os.getppid())
    for i in range(3):
        print('正在编写代码====')
        time.sleep(1)


def music():
    """听音乐的任务"""
    print("听音乐这个子进程的进程编号为:", os.getpid())
    print("听音乐这个子进程的父进程编号为:", os.getppid())
    for i in range(3):
        print('正在听音乐===')
        time.sleep(1)


if __name__ == '__main__':
    print(os.getpid())
    # 创建进程对象
    coding_process = multiprocessing.Process(target=coding)
    music_process = multiprocessing.Process(target=music)
    # 启动子进程
    coding_process.start()
    music_process.start()
    
# 20704
# 编写代码这个子进程的进程编号是: 21720
# 编写代码这个子进程的父进程编号是: 20704
# 正在编写代码====
# 听音乐这个子进程的进程编号为: 15240
# 听音乐这个子进程的父进程编号为: 20704
# 正在听音乐===
# 正在编写代码====
# 正在听音乐===
# 正在编写代码====
# 正在听音乐===

以上子进程都是由主进程创建的,所以主进程是以上子进程的父进程。

3.孤儿进程,僵尸进程

子进程的开启所消耗的资源和时间比较长。所有的子进程在执行完毕之后,并不会立即消失,会保留进程号,进程的执行时间等,这种状态称为僵尸进程。接着父进程结束,子进程还在运行,此时子进程就是孤儿进程,会被init进程(0)接管。(此时init会发送wait给子进程)

孤儿进程:

定义:父进程先于子进程退出,此时子进程就成为孤儿进程
孤儿进程会被操作系统指定的进程收养,系统进程就成为孤儿进程的新的父进程

僵尸进程:

定义:子进程先于父进程退出,但是父进程没有处理子进程的退出状态,此时子进程就会成为僵尸进程
僵尸进程会存留少量PCB信息在内存中,大量的僵尸进程会消耗系统资源,应该避免僵尸进程的产生

1.os.wait()阻塞等待回收子进程
缺点:父进程会阻塞,影响运行效率

pid,status = os.wait()
功能:在父进程中阻塞等待处理子进程退出
返回值:pid 退出的子进程的PID status 子进程退出状态
import os

pid = os.fork()
if pid < 0:
	print('Error')
elif pid == 0:
	print('Child process:',os.getpid())
	os._exit(1)	#子进程退出,1为进程退出状态
else:
	pid,status = os.wait()	#阻塞等待回收子进程
	print('pid:',pid)
	print('status:',status)		#打印出子进程退出状态
	while True:	#让父进程不退出
		pass

1.父进程忽略子进程退出信号,直接让系统来处理子进程退出

signal.signal(signal.SIGCHLD,signal.SIG_IGN)
import os
import signal

#父进程忽略子进程退出信号,让操作系统来处理,父进程不会阻塞
signal.signal(signal.SIGCHLD,signal.SIG_IGN)

pid = os.fork()
if pid < 0:
	print('Create process failed')
elif pid == 0:
	print('Child process:',os.getpid())
else:
	print('Parent process')
	while True:
		pass

4.守护主进程

守护进程:主进程执行完子进程就没必要存在 ,那么这个子进程就应被设置为守护进程。守护进程就是会随主进程的结束而结束的进程,具有以下两个特点:

  • 守护进程会在主进程代码执行结束后就终止
  • 守护进程内无法再开启子进程,否则抛出异常

python中默认进程为非守护。若设置了多个进程而主进程非守护,主进程要等待所有子进程运行结束才能退出:

  • 设置守护主进程,主进程结束子进程就得结束;
    • 子进程对象.daemon = True
    • 子进程对象 = multiprocessing.Process(target=子进程名, daemon=True)
  • 子进程没结束主进程就可以强制将其结束在大多数情景不能符合生产需要,可将子进程设置join()实现子进程执行完主进程才能执行。
  • join一旦执行会阻塞主进程,join的子进程以后不影响其他进程的并行运行:join之前的进程都会并行执行,join之后的进程也会在解阻塞后继续并行运行。
  • 手动销毁子进程
    • 子进程对象.terminate()
import multiprocessing
import time


def work():
    for i in range(10):
        print('正在工作中====')
        time.sleep(0.2)


if __name__ == '__main__':
    # 创建子进程
    sub = multiprocessing.Process(target=work)
    # 1.设置守护主进程
    # 法一:sub = multiprocessing.Process(target=work, daemon=True)
    # 法二:sub.daemon = True
    # 启动子进程
    sub.start()
    time.sleep(1)
    # 2.在主进程结束之前手动结束子进程
    # sub.terminate()
    print('主进程中代码执行完毕')

5.进程池

在程序实际处理问题过程中,忙时会有成千上万的任务需要被执行,闲时可能只有零星任务。那么在成千上万个任务需要被执行的时候,我们就需要去创建成千上万个进程么?首先,创建进程需要消耗时间,销毁进程也需要消耗时间。第二即便开启了成千上万的进程,操作系统也不能让他们同时执行,这样反而会影响程序的效率。因此我们不能无限制的根据任务开启或者结束进程。那么我们要怎么做呢?

在这里,要给大家介绍一个进程池的概念,定义一个池子,在里面放上固定数量的进程,有需求来了,就拿一个池中的进程来处理任务,等到处理完毕,进程并不关闭,而是将进程再放回进程池中继续等待任务。如果有很多任务需要执行,池中的进程数量不够,任务就要等待之前的进程执行任务完毕归来,拿到空闲进程才能继续执行。也就是说,池中进程的数量是固定的,那么同一时间最多有固定数量的进程在运行。这样不会增加操作系统的调度难度,还节省了开闭进程的时间,也一定程度上能够实现并发效果。

语法:Pool([numprocess [,initializer [, initargs]]]):创建进程池

参数:

numprocess:要创建的进程数,默认为cpu_count()的值。
initializer:是每个工作进程启动时要执行的可调用对象,默认为None。
initargs:是要传给initializer的参数组。
Python进程池Pool常用方法
方法 说明
apply(func[, args=()[, kwds={}]]) 该函数用于传递不定参数,主进程会被阻塞直到函数执行结束(不建议使用,并且3.x以后不在出现)。
apply_async(func[, args=()[, kwds={}[, callback=None]]]) 与apply用法一样,但它是非阻塞且支持结果返回进行回调。
map(func, iterable[, chunksize=None]) Pool类中的map方法,与内置的map函数用法行为基本一致,它会使进程阻塞直到返回结果。 注意,虽然第二个参数是一个迭代器,但在实际使用中,必须在整个队列都就绪后,程序才会运行子进程。
close() 关闭进程池(pool),使其不在接受新的任务。
terminate()

结束工作进程,不在处理未处理的任务。
join()

主进程阻塞等待子进程的退出,join方法必须在close或terminate之后使用。

  • 需创建的子进程数量庞大使用multiprocessing模块提供的Pool方法。初始化Pool时可指定最大进程数(进程池),当有新请求到Pool中池没满会创建新进程来执行该请求;若池中进程数达到指定最大值,那么该请求就会等到池中有进程结束才创建新进程来执行。
  • 进程池主要分为阻塞式和非阻塞式。
    • 阻塞式:全部添加到队列中立刻返回,不等待其他的进程完毕(但回调函数等待任务完成后才调用)。阻塞式是产生一个任务立刻让进程1执行,再产生一个让进程2执行下一个。轮流执行直到执行完毕。
    • 非阻塞式:当进程池满时其他进程等待,一旦进程池的某一个进程结束就执行等待的进程。
from multiprocessing import Pool

import time
import os
import random


def task(thing):
    print('开始做任务啦!', thing)
    start = time.time()
    time.sleep(random.random() * 2)
    end = time.time()
    print(f'完成任务:{thing}!  用时:{start} - {end}  进程id:{os.getpid()}')


if __name__ == '__main__':
    pool = Pool(5)  # 进程池最多可以容纳5个进程
    task_name = ['听音乐', '吃饭', '洗衣服', '打游戏', '散步', '看孩子', '做饭']
    for i in task_name:
        pool.apply_async(task, args=(i,))
    pool.close()  # 向主进程添加任务结束
    pool.join()  # 阻塞主进程,也就是说只有子进程结束主进程才能结束
    print('over!')

# 开始做任务啦! 听音乐
# 开始做任务啦! 吃饭
# 开始做任务啦! 洗衣服
# 开始做任务啦! 打游戏
# 开始做任务啦! 散步
# 完成任务:洗衣服!  用时:1668606782.5426586 - 1668606782.580676  进程id:11008
# 开始做任务啦! 看孩子
# 完成任务:看孩子!  用时:1668606782.580676 - 1668606782.6176894  进程id:11008
# 开始做任务啦! 做饭
# 完成任务:吃饭!  用时:1668606782.5311007 - 1668606783.356508  进程id:9716
# 完成任务:听音乐!  用时:1668606782.5280826 - 1668606783.4777398  进程id:24404
# 完成任务:打游戏!  用时:1668606782.5456884 - 1668606783.662577  进程id:9440
# 完成任务:散步!  用时:1668606782.5574005 - 1668606784.2197962  进程id:1848
# 完成任务:做饭!  用时:1668606782.6176894 - 1668606784.3336265  进程id:11008
# over!

下面演示一些实例:

import time
from multiprocessing import Pool

def run(num):
    '''
    计算num的num次方
    :param num: 数字
    :return: num的num次方
    '''
    time.sleep(1)
    return num ** num

if __name__ == '__main__':
    lst = [1, 2, 3, 4, 5, 6]
    print("顺序:")
    t_one = time.time()
    for fn in lst:
        run(fn)
    t_two = time.time()
    print("执行时间:", t_two - t_one)

```
print("多进程:")
pool = Pool(5)
res = pool.map(run, lst)
pool.close()
pool.join()
t_thr = time.time()
print("执行时间:", t_thr - t_two)
print(res)
```

结果:

顺序:
执行时间: 6.0030517578125
多进程:
执行时间: 2.216660976409912
[1, 4, 27, 256, 3125, 46656]

Process finished with exit code 0

上例是一个创建多个进程并发处理与顺序执行处理同一数据,所用时间的差别。从结果可以看出,并发执行的时间明显比顺序执行要快很多,但是进程是要耗资源的,所以平时工作中,进程数也不能开太大。

程序中的res表示全部进程执行结束后全局的返回结果集,run函数有返回值,所以一个进程对应一个返回结果,这个结果存在一个列表中,也就是一个结果堆中,实际上是用了队列的原理,等待所有进程都执行完毕,就返回这个列表(列表的顺序不定)。

对Pool对象调用join()方法会等待所有子进程执行完毕,调用join()之前必须先调用close(),让其不再接受新的Process了。

再来分析一个例子:

import time
from multiprocessing import Pool

def run(num):
    time.sleep(2)
    print(num ** num)

if __name__ == '__main__':
    start_time = time.time()
    lst = [1, 2, 3, 4, 5, 6]
    pool = Pool(10)
    pool.map(run, lst)
    pool.close()
    pool.join()
    end_time = time.time()
    print("时间:", end_time - start_time)

结果:

42561

466563125

27

时间: 2.455129384994507

Process finished with exit code 0

再运行:

14

27
256
3125
46656
时间: 2.4172260761260986

Process finished with exit code 0

问题:结果中为什么还有空行和没有折行的数据呢?

其实这跟进程调度有关,当有多个进程并行执行时,每个进程得到的时间片时间不一样,哪个进程接受哪个请求以及执行完成时间都是不定的,所以会出现输出乱序的情况。那为什么又会有没这行和空行的情况呢?因为有可能在执行第一个进程时,刚要打印换行符时,切换到另一个进程,这样就极有可能两个数字打印到同一行,并且再次切换回第一个进程时会打印一个换行符,所以就会出现空行的情况。

利用Python进行系统管理的时候,特别是同时操作多个文件目录,或者远程控制多台主机,并行操作可以节约大量的时间。当被操作对象数目不大时,可以直接利用multiprocessing中的Process动态成生多个进程,几个还好,但如果是上百个,上千个目标,手动的去限制进程数量却又太过繁琐,这时候进程池Pool发挥作用的时候就到了。

Pool可以提供指定数量的进程,供用户调用,当有新的请求提交到pool中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到规定最大值,那么该请求就会等待,直到池中有进程结束,才会创建新的进程来它。这里有一个简单的例子:

from multiprocessing import Pool
from time import sleep

def func(num):
    for i in range(3):
        print(i, num)
        sleep(2)

def main():
    pool = Pool(3)
    for i in range(3, 6):
        res = pool.apply_async(func, (i,))
    pool.close()
    pool.join()

if __name__ == '__main__':
    main()

结果:(略)

容量为3,每次都只是运行三个进程。

6.锁——Lock

程序的异步让多个任务可同时在几个进程中并发处理,但进程间运行无序且开启不受控制。尽管并发编程能更加充分的利用IO资源,但也带来了新问题:当多个进程使用同一份数据资源的时候,会引发数据安全或顺序混乱问题:

import os
import time
import random
from multiprocessing import Process


def work(num):
    print("%s:%s正在运行" % (num, os.getpid()))
    time.sleep(random.random())
    print("%s:%s执行完毕" % (num, os.getpid()))


if __name__ == '__main__':
    for i in range(5):
        p = Process(target=work, args=(i,))
        p.start()

# 0:2156正在运行
# 1:5716正在运行
# 2:19192正在运行
# 3:13556正在运行
# 4:19376正在运行
# 1:5716执行完毕
# 4:19376执行完毕
# 0:2156执行完毕
# 2:19192执行完毕
# 3:13556执行完毕

使用锁机制后:

import os
import time
import random
from multiprocessing import Process, Lock


def work(num, lock_lock):
    lock_lock.acquire()
    print("%s:%s正在运行" % (num, os.getpid()))
    time.sleep(random.random())
    print("%s:%s执行完毕" % (num, os.getpid()))
    lock_lock.release()


if __name__ == '__main__':
    lock = Lock()  # 实例化对象防止信号量或锁释放过多次
    for i in range(5):
        p = Process(target=work, args=(i, lock))
        p.start()
        
# 0:13480正在运行
# 0:13480执行完毕
# 1:19772正在运行
# 1:19772执行完毕
# 2:17572正在运行
# 2:17572执行完毕
# 3:12340正在运行
# 3:12340执行完毕
# 4:5056正在运行
# 4:5056执行完毕
  • 加锁保证多个进程修改同一块数据时,同一时间仅有一个进程可修改数据,即串行的修改。虽然程序变回串行且延长了执行时间,但加锁的形式实现了进程顺序执行,保证了数据的安全。

三、队列和管道

  • mutiprocessing模块方法能够兼顾效率(多个进程共享一块内存的数据)和锁。
  • mutiprocessing模块提供基于消息的IPC通信机制:队列和管道。
  • 队列和管道都是将数据存放于内存中,队列又是基于(管道+锁)实现的,可以摆脱复杂的锁问题。
  • 队列和管道属于进程之间的通信机制。

1.Queue(队列)

队列(Queue)创建共享的进程队列,Queue是多进程安全的队列,可使用Queue实现多进程之间的数据传递。

1)队列作用

  • 提供了同步的、线程安全的队列类
  • 解耦,使程序实现松耦合(一个模块修改不会影响其他模块)
  • 提高效率

2)常用方法

maxsize

是实例化 Queue 类时的一个参数,默认0。Queue(maxsize=0) 可控制队列中数据容量

put()

put(item [, block [,timeout]])

block为布尔值用于设置是否阻塞,timeout`设置阻塞时等待时长。

向队列添加数据item

阻塞:队列满时阻塞,队列不满时再向其添加数据。
不阻塞:队列排满或到达等待时长会报错:queue.Emptyqueue.Full

get()

get(block, timeout)

获取队列数据

阻塞:队列空后get阻塞,队列非空后再获取数据。
不阻塞:队列空后或到达等待时长会报错:_queue.Empty

put_nowait()

相当于Queue.put(False),非阻塞方法

get_nowait()

相当于Queue.get(False),非阻塞方法

empty()

队列空返回True,反之False。若其他进程或线程正在往队列中添加项目,结果不可靠。

full()

队列满返回True,反之False,Queue.full 与 maxsize 大小对应。由于线程的存在结果也可能不可靠(参考q.empty()方法)。

join()

使程序等到队列为空,再执行其他操作。

qsize()

获取队列中数据量(返回队列的大小)。(多线程下不可靠,获取时其他线程可能重复对队列操作)此方法可能引发NotImplementedError异常

close()

关闭队列,防止队列中加入更多数据。调用此方法时,后台线程将继续写入那些已入队列但尚未写入的数据,但将在此方法完成时马上关闭。如果q被垃圾收集,将自动调用此方法。关闭队列不会在队列使用者中生成任何类型的数据结束信号或异常。例如,如果某个使用者正被阻塞在get()操作上,关闭生产者中的队列不会导致get()方法返回错误。

task_done()

在完成队列的某项任务后向队列发送完成信号。get()来得到任务,task_done()则告诉队列该任务已结束。执行put会未完成任务+1,但执行get不会未完成任务-1。所以需用 task_done让未完成任务 -1,否则 join 无法判断队列为空而报错。

cancel_join_thread()

不会再进程退出时自动连接后台线程。这可以防止join_thread()方法阻塞。

join_thread()

连接队列的后台线程。此方法用于在调用q.close()方法后,等待所有队列项被消耗。默认情况下,此方法由不是q的原始创建者的所有进程调用。调用q.cancel_join_thread()方法可以禁止这种行为。

3)实例

import queue
import threading
import time


def q_put():
    for i in range(10):
        q.put('1')
    while True:
        q.put('2')
        time.sleep(1)


def q_get():
    while True:
        temp = q.get()
        q.task_done()
        print(temp)
        time.sleep(0.3)


q = queue.Queue()
t1 = threading.Thread(target=q_put)
t2 = threading.Thread(target=q_get)
t1.start()
t2.start()
q.join()
print('queue is empty now')

# 主线程执行到 q.join 就开始阻塞,当 t2 线程将队列中的数据全部取出之后,主线程才继续执行。如果将 task_done 注释掉主线程就永远阻塞在 q.join,不再继续向下执行。

实例:

from multiprocessing import Queue

q = Queue(3)  # maxsize = 3

# put, get, put_nowait, get_nowait, full, empty

'''放数据'''
q.put(3)
q.put(3)
q.put(3)

# q.put(3)

# q.put(3)  # 队列满了。阻塞在此处,等待别人取走,如果别人没有取走,程序将永远停留在这里

'''可以使用put_nowait,如果队列满了不会阻塞,但是会因为队列满了而报错。
因此我们用try来捕捉,这样的话程序就不会一直阻塞下去,但是也会丢掉这个消息'''
try:
    q.put_nowait(3)
except:
    print("队列满了")

'''放入数据之前,可以使用full查看队列状态,如果满了就不能继续put了,
返回值是True或False'''
print(q.full())

'''取数据'''
print(q.get())
print(q.get())
print(q.get())

# print(q.get())  # 因为已经取完了,再取就会发生发生阻塞,直到后续有数据放入为止

print(q.full())  # 取完了,所以是False

'''可以使用get_nowait,如果队列满了不会阻塞,但是会因为没取到值而报错。
因此我们可以用一个try语句来处理这个错误。这样程序不会一直阻塞下去。'''
try:
    q.get_nowait(3)
except:
    print("队列空了")

print(q.empty())  # 判断是否为空

结果:

队列满了
True
3
3
3
False
队列空了
True

Process finished with exit code 0

上面的例子没有引入进程,我们下面引入进程来操作队列:

import time
from multiprocessing import Process, Queue

def func(queue):
    queue.put([time.asctime(), 'Hello', 'Python'])

if __name__ == '__main__':
    q = Queue()
    p = Process(target=func, args=(q,))
    p.start()
    print(q.get())
    p.join()
结果:

['Mon Aug 27 15:26:47 2018', 'Hello', 'Python']

Process finished with exit code 0

上面是一个queue的简单应用,使用队列q对象调用get函数来取得队列中最先进入的数据。 接下来看一个稍微复杂一些的例子:

import os
import time
import multiprocessing

def input_queue(q):
    info = str(os.getpid()) + '(put):' + str(time.asctime())
    q.put(info)
    print(info)

def output_queue(q):
    info = q.get()
    print('\033[32m%s\033[0m' % info)

if __name__ == '__main__':
    multiprocessing.freeze_support()
    record_one = []
    record_two = []
    queue = multiprocessing.Queue(3)

```
# 放入数据
for i in range(10):
    p = multiprocessing.Process(target=input_queue, args=(queue,))
    p.start()
    record_one.append(p)
 
# 取出数据
for i in range(10):
    p = multiprocessing.Process(target=output_queue, args=(queue,))
    p.start()
    record_one.append(p)
 
for p in record_one:
    p.join()
 
for p in record_two:
    p.join()
```

结果:

328(put):Mon Aug 27 15:45:34 2018
328(put):Mon Aug 27 15:45:34 2018
27164(put):Mon Aug 27 15:45:34 2018
27164(put):Mon Aug 27 15:45:34 2018
8232(put):Mon Aug 27 15:45:34 2018
8232(put):Mon Aug 27 15:45:34 2018
23804(put):Mon Aug 27 15:45:34 2018
23804(put):Mon Aug 27 15:45:34 2018
23004(put):Mon Aug 27 15:45:34 2018
23004(put):Mon Aug 27 15:45:34 2018
22184(put):Mon Aug 27 15:45:34 2018
22184(put):Mon Aug 27 15:45:34 2018
24948(put):Mon Aug 27 15:45:34 2018
24948(put):Mon Aug 27 15:45:34 2018
19704(put):Mon Aug 27 15:45:34 2018
19704(put):Mon Aug 27 15:45:34 2018
8200(put):Mon Aug 27 15:45:34 2018
8200(put):Mon Aug 27 15:45:34 2018
14380(put):Mon Aug 27 15:45:34 2018
14380(put):Mon Aug 27 15:45:34 2018

Process finished with exit code 0

实现进程通信

multiprocessing模块下的Queue(队列)类,提供了多个进程之间实现通信的诸多方法:

"""
利用多进程模拟下载文件程序:
开启两个进程进行文件下载,然后再开启一个进程保存下载的文件
"""

from multiprocessing import Queue, Process
import time
import random


def download1(q, args):
    for i in args:
        q.put(i)
        print('正在下载文件:{}'.format(i))
        time.sleep(random.random() * 2)


def download2(q, args):
    for i in args:
        q.put(i)
        print('正在下载文件:{}'.format(i))
        time.sleep(random.random() * 2)


def savefile(q):
    
    while True:
        file = q.get()
        print('文件{}保存成功'.format(file))


if __name__ == '__main__':
    a = Queue(5)  # 队列中可以保存的元素上限,只有元素取出后,才会有下一个进入
    args1 = ['url1', 'url3', 'url5', 'url7']
    args2 = ['url2', 'url4', 'url6']
    down1 = Process(target=download1, args=(a, args1,))
    down2 = Process(target=download2, args=(a, args2,))
    save = Process(target=savefile, args=(a,))
    down1.start()
    down2.start()
    save.start()
    down1.join()
    down2.join()
    save.terminate()
    print('over!')

# 正在下载文件:url1
# 正在下载文件:url2
# 文件url1保存成功
# 文件url2保存成功
# 正在下载文件:url4
# 文件url4保存成功
# 正在下载文件:url3
# 文件url3保存成功
# 正在下载文件:url6
# 文件url6保存成功
# 正在下载文件:url5
# 文件url5保存成功
# 正在下载文件:url7
# 文件url7保存成功
# over!

2.Pipe(管道)

Pipe又称“管道”,用于实现两个进程间通信,两个进程分别位于管道的两端。

使用 Pipe 实现进程通信,首先需要调用 multiprocessing.Pipe() 函数来创建一个管道。该函数的语法格式如下:

conn1, conn2 = multiprocessing.Pipe( [duplex=True] )

# conn1, conn2分别接收Pipe函数返回的两个端口;
# duplex参数默认为True,表示该管道双向,即两个端口的进程可相互发送、接受数据。若值为False则表示管道单向,conn1只接收,conn2只发送。

Pipe常用方法

Pipe()

Pipe([duplex])

在线程之间创建一条管道,并返回元祖(con1,con2),其中con1,con2表示管道两端连接的对象。

duplex:默认管道双向,如果将duplex映射为False,con1只能用于接收,con2只能由于发送。

注意:必须在产生Process之前产生管道。

在进程间创建一条管道,并返回元组(conn1,conn2),conn1,conn2表示管道两端的连接对象(必须在产生Process对象之前产生管道)
若duplex=False,conn1只接收,conn2只发送。

con1.recv()

接收conn2.send(obj)发送的对象。如果没有消息可接收,recv方法会一直阻塞。如果连接的另外一端已经关闭,那么recv方法会抛出EOFError。

接收conn2.send(obj)发送的对象。若无消息接收,recv方法会一直阻塞。若连接的另外一端已关闭,recv方法会报错。

con1.send()

con1.send(obj)

通过连接发送对象。obj是与序列化兼容的任意对象。

通过连接发送obj(对象)给管道的另一端,另一端用recv()方法接收(该obj必须可序列化),若该对象序列化之后>32MB可能引发 ValueError 异常。

con1.close()

关闭连接。如果conn1被垃圾回收,将自动调用此方法。

con1.fileno()

返回连接使用的整数文件描述符。

conn1.poll()

conn1.poll([timeout])

若连接上传输的数据可用返回True。若省略此参数方法将立即返回结果。若timeout=None,操作将永久等待数据到达。

conn1.recv_bytes()

conn1.recv_bytes([maxlength])

接收c.send_bytes()方法发送的一条完整的字节消息。若字节数超过最大值会引发IOError异常,且在连接上无法进一步读取数据。若连接的另外一端关闭,不存在任何数据,将引发EOFError异常。接收c.send_bytes()方法发送的一条完整的字节消息。maxlength指定要接收的最大字节数。如果进入的消息,超过了这个最大值,将引发IOError异常,并且在连接上无法进行进一步读取。如果连接的另外一端已经关闭,再也不存在任何数据,将引发EOFError异常。

conn.send_bytes()

conn.send_bytes(buffer [, offset [, size]])

  • uffer:支持缓冲区接口的任意对象
  • offset:缓冲区中的字节偏移量
  • size:要发送字节数

通过连接发送字节数据缓冲区。结果数据以单条消息的形式发出,然后调用c.recv_bytes()函数进行接收

conn1.recv_bytes_into()

conn1.recv_bytes_into(buffer [, offset])

  • offset:指定缓冲区中放置消息处的字节位移。

接收一条完整的字节消息并保存在buffer对象中,该对象支持可写入的缓冲区接口(即bytearray对象或类似的对象)。返回值是收到的字节数。如果消息长度大于可用的缓冲区空间,将引发BufferTooShort异常。

实例:

from multiprocessing import Process, Pipe

def func(conn):
    conn.send("Hello,This is Python!")  # 发送
    conn.close()

if __name__ == '__main__':
    parent_conn, child_conn = Pipe()
    p = Process(target=func, args=(child_conn,))
    p.start()
    print(parent_conn.recv())  # 接收
    p.join()
结果:

Hello,This is Python!

Process finished with exit code 0

应该特别注意管道端点的正确管理问题。如果是生产者或消费者中都没有使用管道的某个端点,就应将它关闭。这也说明了为何在生产者中关闭了管道的输出端,在消费者中关闭管道的输入端。如果忘记执行这些步骤,程序可能在消费者中的recv()操作上挂起。管道是由操作系统进行引用计数的,必须在所有进程中关闭管道后才能生成EOFError异常。因此,在生产者中关闭管道不会有任何效果,除非消费者也关闭了相同的管道端点。

下面的操作将引发EOFError:

from multiprocessing import Process, Pipe

def func(parent_conn, child_conn):
    # parent_conn.close()  # 写了close()将引发OSError
    while 1:
        try:
            print(child_conn.recv())
        except EOFError:
            child_conn.close()

if __name__ == '__main__':
    parent_conn, child_conn = Pipe()
    p = Process(target=func, args=(parent_conn, child_conn))
    p.start()
    child_conn.close()
    parent_conn.send("Hello,This is Python!")
    parent_conn.close()
    p.join()

结果:

Hello,This is Python!

Manager
展望未来,基于消息传递的并发编程是大势所趋。即便是使用线程,推荐做法也是将程序设计为大量独立的线程集合,通过消息队列交换数据。这样极大地减少了对使用锁定和其他同步手段的需求,还可以扩展到分布式系统中。但进程间应该尽量避免通信,即便需要通信,也应该选择进程安全的工具来避免加锁带来的问题。

进程间数据是独立的,可以借助于队列或管道实现通信,二者都是基于消息传递的。虽然进程间数据独立,但可以通过Manager实现数据共享,事实上Manager的功能远不止于此:

from multiprocessing import Manager, Process, Lock

def work(d, lock):
    with lock:  # 不加锁而操作共享的数据,数据会出乱
        d['count'] -= 1

if __name__ == '__main__':
    lock = Lock()
    with Manager() as m:
        dic = m.dict({'count': 100})
        p_l = []
        for i in range(100):
            p = Process(target=work, args=(dic, lock))
            p_l.append(p)
            p.start()
        for p in p_l:
            p.join()
        print(dic)

结果:(略)

示例:

from multiprocessing import Pipe, Process
import time
import random

def download1(p, args):
    for i in args:
        p.send(i)
        print('正在下载文件:{}'.format(i))
        time.sleep(random.random()*2)

def savefile(p):
    while True:
        file = p.recv()
        print('文件{}保存成功'.format(file))

if __name__ == '__main__':
    p = Pipe()
    args1 = ['url1', 'url3', 'url5', 'url7']
    down1 = Process(target=download1, args=(p[0], args1, ))
    save = Process(target=savefile, args=(p[1], ))
    down1.start()
    save.start()
    down1.join()
    save.terminate()
    print('over!')

结果:
E:\python数据结构与算法\day\Scripts\python.exe E:/python数据结构与算法/python_BB/process_pipe.py
正在下载文件:url1
文件url1保存成功
正在下载文件:url3
文件url3保存成功
正在下载文件:url5
文件url5保存成功
正在下载文件:url7
文件url7保存成功
over!

Process finished with exit code 0

四、作业调度与进程调度

  • 作业调度: 将位于外存后备队列中的某个(或某几个)作业调入内存,排在内存的就绪队列上。这时仅是将作业调入内存,并为作业创建进程、分配资源,此时进程处于就绪状态,并没有执行。
  • 进程调度:从内存的就绪队列中选取一个(或几个)进程,并分配处理机,此时为执行。
  • 区别:前者是为作业建立进程的过程,是将作业由外存调入内存的过程;而后者整个过程并没有跑出内存的范围,是将就绪态的进程变为运行态的过程。

作业、进程和程序之间的联系:

​ 一个作业通常包括程序、数据和操作说明书3部分。每一个进程由PCB、程序和数据集合组成。这说明程序是进程的一部分,是进程的实体。因此,一个作业可划分为若干个进程来完成,而每一个进程有其实体————程序和数据集合。

作业与进程的区别:

​ 一个进程是一个程序对某个数据集的执行过程,是分配资源的基本单位。作业是用户需要计算机完成的某项任务,是要求计算机所做工作的集合。一个作业的完成要经过作业提交、作业收容、作业执行和作业完成4个阶段。而进程是对已提交完毕的程序所执行过程的描述,是资源分配的基本单位。其主要区别如下。

(1)作业是用户向计算机提交任务的任务实体。在用户向计算机提交作业后,系统将它放入外存中的作业等待队列中等待执行。而进程则是完成用户任务的执行实体,是向系统申请分配资源的基本单位。任一进程,只要它被创建,总有相应的部分存在于内存中。

(2)一个作业可由多个进程组成,且必须至少由一个进程组成,反过来则不成立。

(3)作业的概念主要用在批处理系统中,像UNIX这样的分时系统中就没有作业的概念。而进程的概念则用在几乎所有的多道程序系统中。

1.调度算法

1)FCFS

先来先服务调度法(First Come First Service):按照先后顺序处理事件的一种算法。该算法既可用于作业调度,也可用于进程调度。FCFS算法比较有利于长作业(进程),而不利于短作业(进程)。由此可知,本算法适合于CPU繁忙型作业,而不利于I/O繁忙型的作业(进程)。

2)SJF/SPN

短作业优先法(Shortest Job First):又称为短进程优先算法(SPN,Shortest Process Next),能有效减少平均周转时间。该算法既可用于作业调度,也可用于进程调度。但其对长作业不利;不能保证紧迫性作业(进程)被及时处理;作业的长短只是被估算出来的。

3)RR

时间片轮转法(Round Robin):让每个进程在就绪队列中的等待时间与享受服务的时间成比例,也就是需要将CPU的处理时间分成固定大小的时间片,如果一个进程在被调度选中之后用完了系统规定的时间片,但又未完成要求的任务,则它自行释放自己所占有的CPU而排到就绪队列的末尾,等待下一次调度。同时,进程调度程序又去调度当前就绪队列中的第一个进程。

显然,轮转法只能用来调度分配一些可以抢占的资源。这些可以抢占的资源可以随时被剥夺,而且可以将它们再分配给别的进程。CPU是可抢占资源的一种。但打印机等资源是不可抢占的。由于作业调度是对除了CPU之外的所有系统硬件资源的分配,其中包含有不可抢占资源,所以作业调度不使用轮转法。

在轮转法中,时间片长度的选取非常重要。首先,时间片长度的选择会直接影响到系统的开销和响应时间。如果时间片长度过短,则调度程序抢占处理机的次数增多。这将使进程上下文切换次数也大大增加,从而加重系统开销。反过来,如果时间片长度选择过长,例如,一个时间片能保证就绪队列中所需执行时间最长的进程能执行完毕,则轮转法变成了先来先服务法。时间片长度的选择是根据系统对响应时间的要求和就绪队列中所允许最大的进程数来确定的。

在轮转法中,加入到就绪队列的进程有3种情况:

分给某进程的时间片用完,但进程还未完成,回到就绪队列的末尾等待下次调度去继续执行。
分给该进程的时间片并未用完,只是因为请求I/O或由于进程的互斥与同步关系而被阻塞。当阻塞解除之后再回到就绪队列。
新创建进程进入就绪队列。
如果对这些进程区别对待,给予不同的优先级和时间片从直观上看,可以进一步改善系统服务质量和效率。例如,我们可把就绪队列按照进程到达就绪队列的类型和进程被阻塞时的阻塞原因分成不同的就绪队列,每个队列按FCFS原则排列,各队列之间的进程享有不同的优先级,但同一队列内优先级相同。这样,当一个进程在执行完它的时间片之后,或从睡眠中被唤醒以及被创建之后,将进入不同的就绪队列。

4)多级反馈队列

多级反馈队列调度算法则不必事先知道各种进程所需的执行时间,而且还可以满足各种类型进程的需要,因而它是目前被公认的一种较好的进程调度算法。在采用多级反馈队列调度算法的系统中,调度算法的实施过程如下所述。

应设置多个就绪队列,并为各个队列赋予不同的优先级。第一个队列的优先级最高,第二个队列次之,其余各队列的优先权逐个降低。该算法赋予各个队列中进程执行时间片的大小也各不相同,在优先权愈高的队列中,为每个进程所规定的执行时间片就愈小。例如,第二个队列的时间片要比第一个队列的时间片长一倍,……,第i+1个队列的时间片要比第i个队列的时间片长一倍。
当一个新进程进入内存后,首先将它放入第一队列的末尾,按FCFS原则排队等待调度。当轮到该进程执行时,如它能在该时间片内完成,便可准备撤离系统;如果它在一个时间片结束时尚未完成,调度程序便将该进程转入第二队列的末尾,再同样地按FCFS原则等待调度执行;如果它在第二队列中运行一个时间片后仍未完成,再依次将它放入第三队列,……,如此下去,当一个长作业(进程)从第一队列依次降到第n队列后,在第n 队列便采取按时间片轮转的方式运行。
仅当第一队列空闲时,调度程序才调度第二队列中的进程运行;仅当第1~(i-1)队列均空时,才会调度第i队列中的进程运行。如果处理机正在第i队列中为某进程服务时,又有新进程进入优先权较高的队列(第1~(i-1)中的任何一个队列),则此时新进程将抢占正在运行进程的处理机,即由调度程序把正在运行的进程放回到第i队列的末尾,把处理机分配给新到的高优先权进程。
进程状态介绍
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

状态描述

就绪(Ready)状态:当进程已分配到除CPU以外的所有必要的资源,只要获得处理机便可立即执行,这时的进程状态称为就绪状态。

执行/运行(Running)状态:当进程已获得处理机,其程序正在处理机上执行,此时的进程状态称为执行状态。

阻塞(Blocked)状态:正在执行的进程,由于等待某个事件发生而无法执行时,便放弃处理机而处于阻塞状态。引起进程阻塞的事件可有多种,例如,等待I/O完成、申请缓冲区不能满足、等待信件(信号)等。

import time
print("程序开始")  # 程序开始,运行状态
name = input("请输入姓名:")  # 用户输入,进入阻塞
print(name)  # 运行状态
time.sleep(1)  # 睡眠1秒,阻塞状态
print("程序结束")  # 运行状态

2.同步/异步

同步(synchronous): 所谓同步就是一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列。

简言之,要么成功都成功,失败都失败,两个任务的状态可以保持一致。

异步(asynchronous):所谓异步是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列。

3.进程创建/结束

创建

但凡是硬件,都需要有操作系统去管理,只要有操作系统,就有进程的概念,就需要有创建进程的方式,一些操作系统只为一个应用程序设计,比如微波炉中的控制器,一旦启动微波炉,所有的进程都已经存在。

而对于通用系统(跑很多应用程序),需要有系统运行过程中创建或撤销进程的能力,主要分为4中形式创建新的进程:

1.系统初始化(查看进程linux中用ps命令,windows中用任务管理器,前台进程负责与用户交互,后台运行的进程与用户无关,运行在后台并且只在需要时才唤醒的进程,称为守护进程,如电子邮件、web页面、新闻、打印)。
2.一个进程在运行过程中开启了子进程(如nginx开启多进程,os.fork,subprocess.Popen等)。
3.用户的交互式请求,而创建一个新进程(如用户双击暴风影音)。
4.一个批处理作业的初始化(只在大型机的批处理系统中应用)。
无论哪一种,新进程的创建都是由一个已经存在的进程执行了一个用于创建进程的系统调用而创建的。

结束

  • 正常退出(自愿,如用户点击交互式页面的叉号,或程序执行完毕调用发起系统调用正常退出,在linux中用exit,在windows中用ExitProcess)。
  • 出错退出(自愿,python a.py中a.py不存在)。
  • 严重错误(非自愿,执行非法指令,如引用不存在的内存,1/0等,可以捕捉异常,try…except…)。
  • 被其他进程杀死(非自愿,如kill -9)。

五、多线程

在Python中实现多任务还可用多线程来完成。线程优点:

  • 进程是资源分配的最小单位,创建进程后会分配系统资源。线程是程序执行的最小单位,利用进程分配的资源执行程序。可以说进程是线程的容器。同属于一个进程中的线程共享此进程中的全部资源。
  • 一个进程至少默认有一个线程负责执行程序,线程本身不拥有系统资源,只需少量程序运行时必不可少的资源,多线程实现多任务的同时也节省了资源。

1.threading属性方法

threading.current_thread() 获取线程信息

2.实现多线程

  • 导入线程模块 import threading
  • 通过线程类创建线程对象 线程对象 = threading.Thread()
  • 启动线程执行目标任务 线程对象.start()
import threading
import time


def coding():
    for i in range(3):
        print('正在写代码===')
        time.sleep(1)


def music():
    for i in range(3):
        print('正在听歌====')
        time.sleep(1)


if __name__ == '__main__':
    # 创建线程对象
    coding_thread = threading.Thread(target=coding)
    music_thread = threading.Thread(target=music)
    # 启动子线程
    coding_thread.start()
    music_thread.start()

import threading
import time


def coding(num):
    for i in range(num):
        print('正在写代码===')
        time.sleep(1)


def music(num):
    for i in range(num):
        print('正在听歌====')
        time.sleep(1)


if __name__ == '__main__':
    # 创建线程对象
    coding_thread = threading.Thread(target=coding, args=(3,))
    music_thread = threading.Thread(target=music, kwargs={'num': 5})
    # 启动子线程
    coding_thread.start()
    music_thread.start()

传参的注意事项跟进程中的传参规则一样。

1)子先主后

对比进程,主线程会等待所有子线程执行结束之后再结束。

import threading
import time


def work():
    for i in range(10):
        print('正在工作中====')
        time.sleep(0.2)


if __name__ == '__main__':
    # 创建子线程
    sub = threading.Thread(target=work)
    # 启动子线程
    sub.start()
    time.sleep(1)
    print('主线程中代码执行完毕')
2)守护主线程

设置守护主线程

import threading
import time


def work():
    for i in range(10):
        print('正在工作中====')
        time.sleep(0.2)


if __name__ == '__main__':
    # 创建子线程
    sub = threading.Thread(target=work)
    # sub = threading.Thread(target=work, daemon=True)
    # 方案二: 子线程对象.setDaemon(True)
    # sub.setDaemon(True)
    # 启动子线程
    sub.start()
    time.sleep(1)
    print('主线程中代码执行完毕')
3)线程间执行顺序

线程之间的执行顺序

threading.current_thread() 获取线程信息

import threading
import time


def get_info():
    """获取线程信息"""
    time.sleep(0.5)
    print(threading.current_thread())


if __name__ == '__main__':
    for i in range(10):
        sub = threading.Thread(target=get_info)
        sub.start()
        

总结:线程之前执行是无序的,到底那个线程先执行那个线程后执行,这是由底层cpu调度决定的。

4)线程共享全局变量

线程之间共享全局变量

多个线程都是在一个进程中,多个线程使用的资源都是同一进程里面的,因此同属于一个进程中的多个线程之间共享全局变量。

import threading
import time

# 定义全局变量
my_list = []


def write_data():
    """向my_list里面添加数据"""
    for i in range(3):
        my_list.append(i)
        print('像列表里面添加了:', i)
    print('这是写数据的子线程的my_list列表:', my_list)


def read_data():
    """读my_list里面的数据"""
    print('这是读数据的子线程读到的my_list列表', my_list)


if __name__ == '__main__':
    # 创建子线程
    write_thread = threading.Thread(target=write_data)
    read_thread = threading.Thread(target=read_data)
    # 启动子线程
    write_thread.start()
    # 阻塞等待
    time.sleep(1)  # 沉睡一秒
    read_thread.start()
    print(my_list)

线程之间共享全局变量导致数据出现错误:

需求: 定义两个函数,实现循环100万次,每循环一次给全局变量进行加一操作,创建两个子线程去执行对应的两个函数,查看计算后的结果。

import threading


# 定义一个全局变量
g_num = 0


def sum_num1():
    """对全局变量进行加一操作"""
    for i in range(1000000):
        global g_num
        g_num += 1
    print('g_num1:', g_num)


def sum_num2():
    """对全局变量进行加一操作"""
    for i in range(1000000):
        global g_num
        g_num += 1
    print('g_num2:', g_num)


if __name__ == '__main__':
    # 创建线程对象执行不同的函数
    sum1 = threading.Thread(target=sum_num1)
    sum2 = threading.Thread(target=sum_num2)
    # 启动子线程
    sum1.start()
    sum2.start()
    # sum_num1()
    # sum_num2()


总结: 两个线程同时操作全局变量,可能出现全局变量的值还没加上,而另一个线程已经获取值了,这时获取的值还是上一次没加上的值,最后就会出现数据少加的情况,所以结果就会出错。

解决办法:

  • 同步:协同步调,按照规定的先后顺序进行运行,好比现实生活中你讲完我再讲…………不能一起讲。
  • 使用线程同步,保证同一时刻只有一个线程去操作全局变量。
  • 方式:互斥锁

3.互斥锁

互斥锁可以对共享数据进行锁定,保证同一时刻只有一个线程去操作锁定数据,对于加锁的部分只有单线程的效果,但是对于整体目标任务而言,还是多线程。

注意:互斥锁是多个线程一起去抢,抢到锁的线程就先执行,没有抢到锁的就继续等待,等待上一个抢到锁的线程释放锁之后,其他等待的线程再去抢这个锁。

# 创建锁
mutex = threading.Lock()

# 上锁
mutex.acquire()

# 释放锁
mutex.release()

import threading


# 定义一个全局变量
g_num = 0


def sum_num1():
    """对全局变量进行加一操作"""
    # 上锁
    mutex.acquire()
    for i in range(1000000):
        global g_num
        g_num += 1
    # 释放锁
    mutex.release()
    print('g_num1:', g_num)


def sum_num2():
    """对全局变量进行加一操作"""
    # 上锁
    mutex.acquire()
    for i in range(1000000):
        global g_num
        g_num += 1
    # 释放锁
    mutex.release()
    print('g_num2:', g_num)


if __name__ == '__main__':
    # 创建锁
    mutex = threading.Lock()
    # 创建线程对象执行不同的函数
    sum1 = threading.Thread(target=sum_num1)
    sum2 = threading.Thread(target=sum_num2)
    # 启动子线程
    sum1.start()
    sum2.start()

注意:使用互斥锁需要注意避免死锁的状态,死锁产生的原因就是没有及时或者没有在正确的位置释放锁.

4.queue

Python的queue模块中提供了同步的、线程安全的队列类,包括FIFO(先入先出)队列Queue,LIFO(后入先出)队列LifoQueue,和优先级队列PriorityQueue。这些队列都实现了锁原语,能够在多线程中直接使用。可以使用队列来实现线程间的同步。队列与列表的关系:队列中数据只有一份,取出就没有了,区别于列表,列表数据取出只是复制了一份

1)FIFO(先入先出)

queue.Queue(maxsize=0)

示例:

import queue

q = queue.Queue()
q.put(1)
q.put(2)
q.put(3)

print(q.get())
print(q.get())
print(q.get())

# 1
# 2
# 3
2)LIFO (后入先出)

queue.LifoQueue
示例:

import queue

q = queue.LifoQueue()
q.put(1)
q.put(2)
q.put(3)
print(q.get())
print(q.get())
print(q.get())

# 3
# 2
# 1
3)PriorityQueue

queue.PriorityQueue(数据可设置优先级)
同优先级的按照 ASCII 排序
示例:

import queue
q = queue.PriorityQueue()
q.put((2, '2'))
q.put((1, '1'))
q.put((3, '3'))
q.put((1, 'a'))
print(q.get())
print(q.get())
print(q.get())
print(q.get())

# (1, '1')
# (1, 'a')
# (2, '2')
# (3, '3')

示例代码如下:

from Queue import Queue,LifoQueue,PriorityQueue
#先进先出队列
q=Queue(maxsize=5)
#后进先出队列
lq=LifoQueue(maxsize=6)
#优先级队列
pq=PriorityQueue(maxsize=5)

for i in range(5):
    q.put(i)
    lq.put(i)
    pq.put(i)
    
print "先进先出队列:%s;是否为空:%s;多大,%s;是否满,%s" %(q.queue,q.empty(),q.qsize(),q.full())
print "后进先出队列:%s;是否为空:%s;多大,%s;是否满,%s" %(lq.queue,lq.empty(),lq.qsize(),lq.full())
print "优先级队列:%s;是否为空:%s,多大,%s;是否满,%s" %(pq.queue,pq.empty(),pq.qsize(),pq.full())

print q.get(),lq.get(),pq.get()

print "先进先出队列:%s;是否为空:%s;多大,%s;是否满,%s" %(q.queue,q.empty(),q.qsize(),q.full())
print "后进先出队列:%s;是否为空:%s;多大,%s;是否满,%s" %(lq.queue,lq.empty(),lq.qsize(),lq.full())
print "优先级队列:%s;是否为空:%s,多大,%s;是否满,%s" %(pq.queue,pq.empty(),pq.qsize(),pq.full())

先进先出队列:deque([0, 1, 2, 3, 4]);是否为空:False;多大,5;是否满,True
后进先出队列:[0, 1, 2, 3, 4];是否为空:False;多大,5;是否满,False
优先级队列:[0, 1, 2, 3, 4];是否为空:False,多大,5;是否满,True
0 4 0
先进先出队列:deque([1, 2, 3, 4]);是否为空:False;多大,4;是否满,False
后进先出队列:[0, 1, 2, 3];是否为空:False;多大,4;是否满,False
优先级队列:[1, 3, 2, 4];是否为空:False,多大,4;是否满,False

还有一种队列是双边队列,示例代码如下:

from Queue import deque
dq=deque(['a','b'])
dq.append('c')
print dq
print dq.pop()
print dq
print dq.popleft()
print dq
dq.appendleft('d')
print dq
print len(dq)
deque(['a', 'b', 'c'])
c
deque(['a', 'b'])
a
deque(['b'])
deque(['d', 'b'])
2

Queue多线程代码示例如下:

from Queue import Queue
import time,threading
q=Queue(maxsize=0)

def product(name):
count=1
while True:
q.put(‘气球兵{}’.format(count))
print (‘{}训练气球兵{}只’.format(name,count))
count+=1
time.sleep(5)
def consume(name):
while True:
print (‘{}使用了{}’.format(name,q.get()))
time.sleep(1)
q.task_done()
t1=threading.Thread(target=product,args=(‘wpp’,))
t2=threading.Thread(target=consume,args=(‘ypp’,))
t3=threading.Thread(target=consume,args=(‘others’,))

t1.start()
t2.start()
t3.start()

网上还有很多非常好的生产者消费者模式的Queue代码例子,开发同学需要根据具体的实际需求去设计实际模式

六、进程和线程的对比

  • 关系对比
    • 线程依附在进程里面的,没有进程就没有线程
    • 一个进程中默认至少有一个线程,进程中可以创建多个线程
  • 区别对比
    • 进程之间不共享全局变量
    • 线程共享全局变量,但是要注意资源竞争问题,解决办法就是互斥锁,但是使用互斥锁时候需要注意避免出现死锁状态,应该及时在正确的位置释放锁。
    • 进程创建资源开销比线程大
    • 进程是资源分配的最小单位,线程是cpu调度的最小单位。
    • 线程不能够独立执行,必须在进程里面。
  • 优缺点对比
    • 进程优点就是可以使用多核,但是缺点就是资源开销大
    • 线程优点是资源开销小,缺点就是不能使用多核。

生产者消费者模型(主要用于解耦)

在多线程开发当中,如果生产线程处理速度很快,而消费线程处理速度很慢,那么生产线程就必须等待消费线程处理完,才能继续生产数据。同样的道理,如果消费线程的处理能力大于生产线程,那么消费线程就必须等待生产线程。为了解决这个问题于是引入了生产者和消费者模式
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
示例:

import threading
import time
import queue
def producer():
    count = 1
    while 1:
        q.put('No.%i' % count)
        print('Producer put No.%i' % count)
        time.sleep(1)
        count += 1
def customer(name):
    while 1:
        print('%s get %s' % (name, q.get()))
        time.sleep(1.5)
q = queue.Queue(maxsize=5)
p = threading.Thread(target=producer, )
c = threading.Thread(target=customer, args=('jack', ))
p.start()
c.start()

生产者消费者模式并不是GOF提出的众多模式之一,但它依然是开发同学编程过程中最常用的一种模式

生产者模块儿负责产生数据,放入缓冲区,这些数据由另一个消费者模块儿来从缓冲区取出并进行消费者相应的处理。该模式的优点在于:

解耦:缓冲区的存在可以让生产者和消费者降低互相之间的依赖性,一个模块儿代码变化,不会直接影响另一个模块儿
并发:由于缓冲区,生产者和消费者不是直接调用,而是两个独立的并发主体,生产者产生数据之后把它放入缓冲区,就继续生产数据,不依赖消费者的处理速度
三、采用生产者消费者模式开发的Python多线程
在Python中,队列是最常用的线程间的通信方法,因为它是线程安全的,自带锁。而Condition等需要额外加锁的代码操作,在编程对死锁现象要很小心,Queue就不用担心这个问题。

‘.format(count))
print (’{}训练气球兵{}只’.format(name,count))
count+=1
time.sleep(5)
def consume(name):
while True:
print (‘{}使用了{}’.format(name,q.get()))
time.sleep(1)
q.task_done()
t1=threading.Thread(target=product,args=(‘wpp’,))
t2=threading.Thread(target=consume,args=(‘ypp’,))
t3=threading.Thread(target=consume,args=(‘others’,))

t1.start()
t2.start()
t3.start()

网上还有很多非常好的生产者消费者模式的Queue代码例子,开发同学需要根据具体的实际需求去设计实际模式

六、进程和线程的对比

  • 关系对比
    • 线程依附在进程里面的,没有进程就没有线程
    • 一个进程中默认至少有一个线程,进程中可以创建多个线程
  • 区别对比
    • 进程之间不共享全局变量
    • 线程共享全局变量,但是要注意资源竞争问题,解决办法就是互斥锁,但是使用互斥锁时候需要注意避免出现死锁状态,应该及时在正确的位置释放锁。
    • 进程创建资源开销比线程大
    • 进程是资源分配的最小单位,线程是cpu调度的最小单位。
    • 线程不能够独立执行,必须在进程里面。
  • 优缺点对比
    • 进程优点就是可以使用多核,但是缺点就是资源开销大
    • 线程优点是资源开销小,缺点就是不能使用多核。

生产者消费者模型(主要用于解耦)

在多线程开发当中,如果生产线程处理速度很快,而消费线程处理速度很慢,那么生产线程就必须等待消费线程处理完,才能继续生产数据。同样的道理,如果消费线程的处理能力大于生产线程,那么消费线程就必须等待生产线程。为了解决这个问题于是引入了生产者和消费者模式
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
示例:

import threading
import time
import queue
def producer():
    count = 1
    while 1:
        q.put('No.%i' % count)
        print('Producer put No.%i' % count)
        time.sleep(1)
        count += 1
def customer(name):
    while 1:
        print('%s get %s' % (name, q.get()))
        time.sleep(1.5)
q = queue.Queue(maxsize=5)
p = threading.Thread(target=producer, )
c = threading.Thread(target=customer, args=('jack', ))
p.start()
c.start()

生产者消费者模式并不是GOF提出的众多模式之一,但它依然是开发同学编程过程中最常用的一种模式

生产者模块儿负责产生数据,放入缓冲区,这些数据由另一个消费者模块儿来从缓冲区取出并进行消费者相应的处理。该模式的优点在于:

解耦:缓冲区的存在可以让生产者和消费者降低互相之间的依赖性,一个模块儿代码变化,不会直接影响另一个模块儿
并发:由于缓冲区,生产者和消费者不是直接调用,而是两个独立的并发主体,生产者产生数据之后把它放入缓冲区,就继续生产数据,不依赖消费者的处理速度
三、采用生产者消费者模式开发的Python多线程
在Python中,队列是最常用的线程间的通信方法,因为它是线程安全的,自带锁。而Condition等需要额外加锁的代码操作,在编程对死锁现象要很小心,Queue就不用担心这个问题。

  • 31
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

木颤简叶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值