Python进阶学习笔记(一)—— 多任务编程

1 多任务介绍


利用现学知识能够让两个函数或者方法同时执行吗?

不能,因为之前所写的程序都是单任务的,也就是说一个函数或者方法执行完成另外一个函数或者方法才能执行,要想实现这种操作就需要使用多任务。

多任务 是指在同一时间内执行多个任务,例如: 现在电脑安装的操作系统都是多任务操作系统,可以同时运行着多个软件。

多任务的最大好处是充分利用CPU资源提高程序的执行效率

多任务的执行方式

  • 并发
    • 在一段时间内交替去执行任务。
  • 并行
    • 对于多核 cpu 处理多任务,操作系统会给 cpu 的每个内核安排一个执行的软件,多个内核是真正的一起执行软件。这里需要注意多核 cpu 是并行的执行多任务,始终有多个软件一起执行。

2 进程

2.1 进程的概念


在Python程序中,想要实现多任务可以使用进程来完成,进程是实现多任务的一种方式。

一个正在运行的程序或者软件就是一个进程,它是操作系统进行资源分配的基本单位,也就是说每启动一个进程,操作系统都会给其分配一定的运行资源(内存资源)保证进程的运行。

注意:一个程序运行后至少有一个进程,一个进程默认有一个线程,进程里面可以创建多个线程,线程是依附在进程里面的,没有进程就没有线程。

2.2 进程的作用


(1)单进程

在这里插入图片描述
(2)多进程

在这里插入图片描述

2.3 多进程的使用


(1)导入进程包

import multiprocessing

(2)Process进程类的说明

Process([group [, target [, name [, args [, kwargs]]]]])

参数说明:

  • group:指定进程组,目前只能使用None,一般不需要设置
  • target:执行的目标任务名
  • name:进程名字(常用属性,默认为Process-N,N为从1开始递增的整数)
  • args:以元组方式给执行任务传参
  • kwargs:以字典方式给执行任务传参

常用方法:

  • start():启动子进程实例(创建子进程)
  • join():等待子进程执行结束
  • terminate():不管任务是否完成,立即终止子进程

(3)多进程完成多任务的代码

import multiprocessing
import time

# 跳舞任务
def dance():
    for i in range(3):
        print("跳舞中...")
        time.sleep(0.2)

# 唱歌任务
def sing():
    for i in range(3):
        print("唱歌中...")
        time.sleep(0.2)

if __name__ == '__main__':
    # 创建跳舞的子进程(自己手动创建的进程是子进程)
    dance_process = multiprocessing.Process(target=dance)
    # 创建唱歌的子进程
    sing_process = multiprocessing.Process(target=sing)

    # 启动子进程执行对应的任务
    dance_process.start()
    sing_process.start()
    
    # 主进程执行唱歌任务(默认为主进程)
    # sing()

结果输出:

跳舞中...
唱歌中...
跳舞中...
唱歌中...
跳舞中...
唱歌中...

注意:

  • 如果既有主进程也有子进程任务,默认先执行主进程的任务
  • 本例中一个主进程创建了两个子进程,一个负责唱歌,一个负责跳舞,子进程执行是无序的,具体哪个子进程先执行是由操作系统决定的

2.4 获取进程编号


获取进程编号的目的是验证主进程和子进程的关系,可以得知子进程是由那个主进程创建出来的。

获取进程编号的两种操作

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

# 跳舞任务
def dance():
    # 获取当前进程的编号
    print("dance进程的编号:", os.getpid())
    # 获取当前进程
    print("dance进程对象:", multiprocessing.current_process())
    # 获取父进程的编号
    print("dance进程的父进程编号:", os.getppid())
    for i in range(3):
        print("跳舞中...")
        time.sleep(0.2)
        # 扩展:根据【进程编号:9】强制杀死指定进程
        os.kill(os.getpid(), 9)

# 唱歌任务
def sing():
    # 获取当前进程的编号
    print("sing进程的编号:", os.getpid())
    # 获取当前进程
    print("sing进程对象:", multiprocessing.current_process())
    # 获取父进程的编号
    print("sing进程的父进程编号:", os.getppid())
    for i in range(3):
        print("唱歌中...")
        time.sleep(0.2)

if __name__ == '__main__':

    # 获取当前进程(主进程)的编号
    print("主进程的编号:", os.getpid())
    # 获取当前进程对象,查看当前代码是由哪个进程执行的
    print("主进程对象:", multiprocessing.current_process())
    # 创建跳舞的子进程
    dance_process = multiprocessing.Process(target=dance)
    # 创建唱歌的子进程
    sing_process = multiprocessing.Process(target=sing)

    # 启动子进程执行对应的任务
    dance_process.start()
    sing_process.start()

结果输出:

主进程的编号: 1936
主进程对象: <_MainProcess name='MainProcess' parent=None started>
dance进程的编号: 1892
dance进程对象: <Process name='Process-1' parent=1936 started>
dance进程的父进程编号: 1936
跳舞中...
sing进程的编号: 18268
sing进程对象: <Process name='Process-2' parent=1936 started>
sing进程的父进程编号: 1936
唱歌中...
唱歌中...
唱歌中...

2.5 进程执行带有参数的任务


前面我们使用进程执行的任务是没有参数的,假如我们使用进程执行的任务带有参数,如何给函数传参呢?

Process 类执行任务并给任务传参数有两种方式:

  • args 表示以元组的方式给执行任务传参:元组方式传参一定要和参数的顺序保持一致。
  • kwargs 表示以字典方式给执行任务传参:字典方式传参字典中的 key 一定要和参数名保持一致
import multiprocessing


# 显示信息的任务
def show_info(name, age):
    print(name, age)


if __name__ == '__main__':
    # 创建子进程
    # 以元组方式传参,元组里面的元素顺序要和函数的参数顺序保持一致
    sub_process = multiprocessing.Process(target=show_info, args=("李四", 20))
    # 启动进程
    sub_process.start()

    # 以字典方式传参,字典里面的key要和函数里面的参数名保持一致,没有顺序要求
    sub_process = multiprocessing.Process(target=show_info, kwargs={"age":20, "name": '王五'})
    # 启动进程
    sub_process.start()

    # 第一个参数用元组传,第二个参数用字典传
    sub_process = multiprocessing.Process(target=show_info, args=("冯七",), kwargs={"age": 20})
    # 启动进程
    sub_process.start()

结果输出:

李四 20
王五 20
冯七 20

2.6 进程小结


进程

  • 进程之间执行是无序的
  • 进程之间不共享全局变量
  • 主进程会等待所有的子进程执行结束再结束

(1)进程之间执行是无序的

结论: 进程之间执行是无序的,是由 操作系统调度进程 来决定的,操作系统调度哪个进程,哪个进程就先执行,没有调度的进程不能执行。

(2) 进程之间不共享全局变量

import multiprocessing
import time

# 定义全局变量列表
g_list = list()  #=> []

# 添加数据的任务
def add_data():
    for i in range(3):
        # 因为列表是可变类型,可以在原有内存的基础上修改数据,并且修改后内存地址不变
        # 所以不需要加上global关键字
        # 加上global 表示声明要修改全局变量的内存地址
        g_list.append(i)
        print("add:", i)
        time.sleep(0.2)
    print("添加完成:", g_list)

# 读取数据的任务
def read_data():
    print("read:", g_list)

# 提示: 对应linux和mac主进程执行的代码不会进程拷贝,但是对应window系统来说主进程执行的代码也会进行拷贝执行
# 对应window来说创建子进程的代码如果进程拷贝执行相当于递归无限制进行创建子进程,会报错

# 如何解决windows递归创建子进程?通过判断是否是主模块来解决。

# 理解说明: 直接执行的模块就是主模块,那么直接执行的模块里面就应该添加判断是否是主模块的代码
# if __name__ == '__main__':的作用
# 1. 防止别人导入文件的时候执行main里面的代码
# 2. 防止windows系统递归创建子进程
if __name__ == '__main__':
	# 添加数据的子进程
	add_process = multiprocessing.Process(target=add_data)
	# 读取数据的子进程
	read_process = multiprocessing.Process(target=read_data)
	
	# 启动进程执行对应的任务
	add_process.start()
	# 当前进程(主进程)等待添加数据的进程执行完成以后代码再继续往下执行
	add_process.join()
	print("main:", g_list)
	read_process.start()

结果输出:

add: 0
add: 1
add: 2
添加完成: [0, 1, 2]
main: []
read: []

结论: 进程之间不共享全局变量

  • 创建子进程会对主进程资源进行拷贝,也就是说子进程是主进程的一个副本,好比是一对双胞胎,之所以进程之间不共享全局变量,是因为操作的不是同一个进程里面的全局变量,只不过不同进程里面的全局变量名字相同而已。

在这里插入图片描述

(3)主进程会等待所有的子进程执行结束再结束

import multiprocessing
import time


def task():
    for i in range(5):
        print("任务执行中...")
        time.sleep(0.2)


# 判断是否是直接执行的模块, 程序入口模块

# 标准python写法,直接执行的模块,需要加上判断是否是主模块的代码
if __name__ == '__main__':

    # 创建子进程
    sub_process = multiprocessing.Process(target=task)
    sub_process.start()

    # 主进程延时0.5秒钟
    time.sleep(0.5)
    print("over")

结果输出:

任务执行中...
over
任务执行中...
任务执行中...
任务执行中...
任务执行中...

结论: 一般情况下,主进程会等待子进程执行完成以后程序再退出。

问题:当子进程是死循环时,主进程就始终无法退出了。假如我们就让主进程执行0.5 秒钟,子进程就销毁不再执行,那怎么办呢?

解决办法:

  • 让子进程设置成为守护主进程,主进程退出子进程销毁,子进程会依赖主进程
  • 让主进程退出之前先让子进程销毁
import multiprocessing
import time


def task():
    while True:
        print("任务执行中...")
        time.sleep(0.2)


if __name__ == '__main__':
    # 创建子进程
    sub_process = multiprocessing.Process(target=task)

    # 1. 把子进程设置成为守护主进程,以后主进程退出子进程直接销毁
    sub_process.daemon = True

    sub_process.start()

    # 主进程延时0.5秒钟
    time.sleep(0.5)

    # 2. 退出主进程之前,先让子进程进行销毁
    # sub_process.terminate()

    print("over")  # 这句打印语句就是主进程执行的

结果输出:

任务执行中...
over

3 线程

3.1 线程的概念


在Python中,想要实现多任务除了使用进程,还可以使用线程来完成,线程是实现多任务的另外一种方式。

线程是进程中执行代码的一个分支,每个执行分支(线程)要想工作执行代码需要cpu进行调度 ,也就是说线程是cpu调度的基本单位,每个进程至少都有一个线程,而这个线程就是我们通常说的主线程。

3.2 线程的作用


多线程可以完成多任务

在这里插入图片描述

3.3 多线程的使用


(1)导入线程包

import threading

(2)Thread线程类的说明

Thread([group [, target [, name [, args [, kwargs]]]]])

参数说明:

  • group: 线程组,目前只能使用None
  • target: 执行的目标任务名
  • args: 以元组的方式给执行任务传参
  • kwargs: 以字典方式给执行任务传参
  • name: 线程名,一般不用设置

常用方法:

  • start():启动子线程实例

(3)多线程完成多任务的代码

# 1. 导入线程模块
import threading
import time


def sing():
    # 获取当前线程
    current_thread = threading.current_thread()
    print("sing:", current_thread)

    for i in range(3):
        print("唱歌中...")
        time.sleep(0.2)


def dance():
    # 获取当前线程
    current_thread = threading.current_thread()
    print("dance:", current_thread)

    for i in range(3):
        print("跳舞中...")
        time.sleep(0.2)


if __name__ == '__main__':

    # 获取当前线程
    current_thread = threading.current_thread()
    print("main_thread:", current_thread)

    # 2. 创建子线程
    sing_thread = threading.Thread(target=sing, name="sing_thread")
    dance_thread = threading.Thread(target=dance, name="dance_thread")
    # 3. 启动子线程执行对应的任务
    sing_thread.start()
    dance_thread.start()

结果为

main_thread: <_MainThread(MainThread, started 8840)>
sing: <Thread(sing_thread, started 22236)>
唱歌中...
dance: <Thread(dance_thread, started 21712)>
跳舞中...
唱歌中...跳舞中...

跳舞中...唱歌中...

这里由于多线程环境下输出的竞争条件导致的。在多线程编程中,多个线程同时访问共享资源(例如标准输出 stdout)时,可能会导致输出的顺序和格式不确定。

3.4 线程执行带有参数的任务


Thread类执行任务并给任务传参数有两种方式:

  • args 表示以元组的方式给执行任务传参
  • kwargs 表示以字典方式给执行任务传参
import threading


def show_info(name, age):
    print("name: %s age: %d" % (name, age))

if __name__ == '__main__':
    # 创建子线程
    # 以元组方式传参,要保证元组里面元素的顺序和函数的参数顺序一致
    # sub_thread = threading.Thread(target=show_info, args=("李四", 20))
    # # 启动线程执行对应的任务
    # sub_thread.start()

    # 以字典的方式传参,要保证字典里面的key和函数的参数名保持一致
    sub_thread = threading.Thread(target=show_info, kwargs={"name": "王五", "age": 30})
    # 启动线程执行对应的任务
    sub_thread.start()

3.5 线程小结


线程

  • 线程之间执行是无序的
  • 主线程会等待所有的子线程执行结束再结束
  • 线程之间共享全局变量
  • 线程之间共享全局变量数据出现错误问题

(1)线程之间执行是无序的

结论:线程之间的执行是无序的,具体哪个线程执行是由CPU调度决定的,CPU调度哪个线程,哪个线程就先执行,没有调度的线程不能执行。

(2)主线程会等待所有的子线程执行结束再结束

import threading
import time

def task():
    while True:
        print("任务执行中...")
        time.sleep(0.3)

if __name__ == '__main__':
    # 创建子线程
    sub_thread = threading.Thread(target=task)
    sub_thread.start()

    # 主线程延时执行1秒
    time.sleep(1)
    print("over")

结果输出:

任务执行中...
任务执行中...
任务执行中...
任务执行中...
over
任务执行中...
……
……

结论: 主线程会等待子线程执行结束再结束

问题:假如我们就让主线程执行1秒钟,子线程就销毁不再执行,那怎么办呢?
解决办法:我们可以设置守护主线程(守护主线程就是主线程退出子线程销毁不再执行)

  • threading.Thread(target=show_info, daemon=True)
  • 线程对象.setDaemon(True)
import threading
import time


def task():
    while True:
        print("任务执行中...")
        time.sleep(0.3)

if __name__ == '__main__':
    # 创建子线程
    # daemon=True 表示创建的子线程守护主线程,主线程退出子线程直接销毁
    # sub_thread = threading.Thread(target=task, daemon=True)
    sub_thread = threading.Thread(target=task)
    # 把子线程设置成为守护主线程
    sub_thread.setDaemon(True)
    sub_thread.start()

    # 主线程延时执行1秒
    time.sleep(1)

    print("over")

结果输出:

任务执行中...
任务执行中...
任务执行中...
任务执行中...
over

(3)线程之间共享全局变量

import threading
import time


# 定义全局变量
g_list = []


# 添加数据的任务
def add_data():
    for i in range(3):
        # 每循环一次把数据添加到全局变量
        g_list.append(i)
        print("add:", i)
        time.sleep(0.3)

    # 代码执行到此,说明添加数据完成
    print("添加数据完成:", g_list)


# 读取数据的任务
def read_data():
    print("read:", g_list)


if __name__ == '__main__':
    # 创建添加数据的子线程
    add_thread = threading.Thread(target=add_data)
    # 创建读取数据的子线程
    read_thread = threading.Thread(target=read_data)

    # 启动线程执行对应的任务
    add_thread.start()
    # time.sleep(1)
    # 让当前线程(主线程)等待添加数据的子线程执行完成以后代码在继续执行
    add_thread.join()
    read_thread.start()

结果输出:

add: 0
add: 1
add: 2
添加数据完成: [0, 1, 2]
read: [0, 1, 2]

结论:多线程在同一个进程里面时,可以共享全局变量。

(4)线程之间共享全局变量数据出现错误问题

需求:

  1. 定义两个函数,实现循环100万次,每循环一次给全局变量加1
  2. 创建两个子线程执行对应的两个函数,查看计算后的结果
import threading


# 全局变量
g_num = 0


# 循环100万次执行的任务
def task1():
    for i in range(1000000):
        # 每循环一次给全局变量加1
        global g_num  # 表示要声明修改全局变量的内存地址
        g_num = g_num + 1  # g_num += 1

    # 代码执行到此,说明数据计算完成
    print("task1:", g_num)


# 循环100万次执行的任务
def task2():
    for i in range(1000000):
        # 每循环一次给全局变量加1
        global g_num  # 表示要声明修改全局变量的内存地址
        g_num = g_num + 1  # g_num += 1

    # 代码执行到此,说明数据计算完成
    print("task2:", g_num)


if __name__ == '__main__':
    # 创建两个子线程
    first_thread = threading.Thread(target=task1)
    second_thread = threading.Thread(target=task2)

    # 启动线程执行任务
    first_thread.start()
    second_thread.start()

结果输出:

task1: 976064
task2: 1418265

注意:多线程同时对全局变量操作数据发生了错误。

问题:为什么会出现这样的结果?

原因如下:

在这里插入图片描述

全局变量数据错误的 解决办法 :

线程同步:保证同一时刻只能有一个线程去操作全局变量。同步: 就是协同步调,按预定的先后次序进行运行。如:你说完,我再说,好比现实生活中的对讲机。

  • 线程等待(join)
  • 互斥锁

① 线程等待

只需要在启动两个线程的代码之间加上first_thread.join() 即主线程等待第一个子线程执行完成以后代码再继续往下执行

    # 启动线程执行任务
    first_thread.start()
    # 线程等待,让第一个线程先执行,然后在让第二个线程再执行,保证数据不会有问题
    first_thread.join() # 主线程等待第一个子线程执行完成以后代码再继续往下执行
    second_thread.start()

② 互斥锁

互斥锁可以保证同一时刻只有一个线程去执行代码,能够保证全局变量的数据没有问题。

threading 模块中定义了 Lock 变量,这个变量本质上是一个函数,通过调用这个函数可以获取一把互斥锁。

# 创建锁
mutex = threading.Lock()

# 上锁
mutex.acquire()

...这里编写代码能保证同一时刻只能有一个线程去操作, 对共享数据进行锁定...

# 释放锁
mutex.release()

注意:

  • acquire 和 release 方法之间的代码同一时刻只能有一个线程去操作
  • 如果在调用 acquire 方法的时候其他线程已经使用了这个互斥锁,那么此时 acquire方法会堵塞,直到这个互斥锁释放后才能再次上锁。

需求:使用互斥锁完成2个线程对同一个全局变量各加100万次的操作

import threading


# 全局变量
g_num = 0


# 创建互斥锁, Lock本质上是一个函数,通过调用函数可以创建一个互斥锁
lock = threading.Lock()


# 循环100万次执行的任务
def task1():
    # 上锁
    lock.acquire()
    for i in range(1000000):
        # 每循环一次给全局变量加1
        global g_num  # 表示要声明修改全局变量的内存地址
        g_num = g_num + 1  # g_num += 1

    # 代码执行到此,说明数据计算完成
    print("task1:", g_num)
    # 释放锁
    lock.release()


# 循环100万次执行的任务
def task2():
    # 上锁
    lock.acquire()
    for i in range(1000000):
        # 每循环一次给全局变量加1
        global g_num  # 表示要声明修改全局变量的内存地址
        g_num = g_num + 1  # g_num += 1

    # 代码执行到此,说明数据计算完成
    print("task2:", g_num)
    # 释放锁
    lock.release()


if __name__ == '__main__':
    # 创建两个子线程
    first_thread = threading.Thread(target=task1)
    second_thread = threading.Thread(target=task2)

    # 启动线程执行任务
    first_thread.start()
    second_thread.start()

结果输出:

task1: 1000000
task2: 2000000

注意:

  • 线程等待和互斥锁都是把多任务改成单任务去执行,保证了数据的准确性,但是执行性能会下降

另外,需要注意的是,互斥锁如果没有使用好容易出现死锁的情况

死锁:一直等待对方释放锁的情景就是死锁。
结果:会造成应用程序的停止响应,不能再处理其它任务了

需求: 多线程同时根据下标在列表中取值,要保证同一时刻只能有一个线程去取值。

# 死锁: 一直等待对方释放锁的情景叫做死锁
import threading

# 创建互斥锁
lock = threading.Lock()

def get_value(index):
    # 上锁
    lock.acquire()
    my_list = [1, 4, 6]
    # 判断下标是否越界
    if index >= len(my_list):
        print("下标越界:", index)
        # 取值不成功,也需要释放互斥锁,不要影响后面的线程去取值
        # 锁需要在合适的地方进行释放,防止死锁
        lock.release()
        # return 代表当前函数执行结束,后面的语句不再执行
        return

    # 根据下标取值
    value = my_list[index]
    print(value)
    # 释放锁
    lock.release()


if __name__ == '__main__':
    # 创建大量线程,同时执行根据下标取值的任务
    for i in range(10):
        # 每循环一次创建一个子线程
        sub_thread = threading.Thread(target=get_value, args=(i,))
        # 启动线程执行任务
        sub_thread.start()

结果输出:

1
4
6
下标越界: 3
下标越界: 4
下标越界: 5
下标越界: 6
下标越界: 7
下标越界: 8
下标越界: 9

4 进程与线程对比


(1)关系对比

  • 进程和线程都是完成多任务的一种方式
  • 线程不能单独执行必须依附在进程里面,没有进程就没有线程。
  • 一个进程默认提供一条线程,一个进程可以创建多个线程。

(2)区别对比

  • 全局变量

    • 进程之间不共享全局变量
    • 线程之间共享全局变量,但是要注意资源竞争的问题,解决办法: 互斥锁或者线程同步
  • 基本单位

    • 进程是操作系统资源分配的基本单位
    • 线程是CPU调度的基本单位
  • 资源开销

    • 创建进程的资源开销要比创建线程的资源开销要大
  • 稳定性

    • 多进程开发比单进程多线程开发稳定性要强,某个进程挂掉不会影响其它进程。
  1. 优缺点对比
  • 进程优缺点:
    • 优点:可以用多核
    • 缺点:资源开销大
  • 线程优缺点:
    • 优点:资源开销小
    • 缺点:不能使用多核
  1. 使用场景对比
  • 多进程:和密集计算机相关的操作多使用多进程
  • 多线程:文件的写入、下载,i/o操作等
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值