Python进阶————进程和线程



前言

  • 之前所写的程序都是单任务的,也就是说一个函数或者方法执行完成 , 另外一个函数或者方法才能执行 . 要想实现多个任务同时执行就需要使用多任务

一、多任务

1.1 多任务概述

"""
1、概述:
     多任务指的是 多个任务同时执行.
2、目的:
     节约资源, 充分利用CPU资源, 提高效率.
"""

1.2 多任务的两种表现形式

1.2.1 并发

  • 针对于单核CPU来讲的, 如果有多个任务同时请求执行, 但是同一瞬间CPU只能执行1个(任务), 于是就安排它们交替执行.
  • 因为时间间隔非常短(CPU执行速度太快了), 我们看起来好像是同时执行的, 其实不是.

1.2.2 并行

  • 针对于多核CPU来讲的. 多个任务可以同时执行.

1.3 多任务的优点

  • 多任务执行能够充分利用cpu资源提高程序执行效率

二、进程

2.1 进程介绍

  • 进程(Process)是CPU资源分配最小单位,它是操作系统进行资源分配和调度运行的基本单位
  • 通俗理解:一个正在运行的程序就是一个进程 。 例如:正在运行的qq ,微信等他们都是一个进程

2.2 多进程完成多任务

多进程的实现步骤:
1、导包。
2、创建进程对象, 关联: 要执行的函数。
3、启动进程。

2.2.1 多进程代码演示

代码如下:

import multiprocessing, time

# 需求: 模拟一遍写代码, 一遍听音乐.

# 1. 定义函数, 表示: 写代码.
def coding():
    for i in range(1, 21):
        # 为了让效果更明显, 加入: 休眠线程.
        time.sleep(0.01)
        print(f"正在写代码... {i}", end='\n')


# 2. 定义函数, 表示: 听音乐.
def music():
    for i in range(1, 21):
        # 为了让效果更明显, 加入: 休眠线程.
        time.sleep(0.01)
        print(f"正在听音乐------ {i}")


# 3. 在main函数中测试.
if __name__ == '__main__':
    # 4. 创建两个进程对象, 分别关联上述的两个函数.
    p1 = multiprocessing.Process(target=coding)
    p2 = multiprocessing.Process(target=music)

    # 5. 启动进程.
    p1.start()
    p2.start()

2.2.2 带参数的多进程代码演示

代码如下:

"""
1、进程的参数解释, 即: multiprocessing模块的 Process类的 的参数:
 	   target: 用于关联 进程要执行的任务的.
    	name:   进程名, 默认是: Process-1, Process-2,......Process-n, 可以手动修改, 一般不改.
    	args:   可以通过 元组 的形式传递参数, 实参的个数 及 对应的数据类型 要和 形参的个数及类型 一致.
    	kwargs: 可以通过 字典 的形式传递参数, 实参的个数 要和 形参的个数 一致.

2、进程传参的两种方式是什么?
		a.元组方式传参 :元组方式传参一定要和任务函数的参数顺序保持一致。
		
		b.字典方式传参:字典方式传参字典中的key一定要和任务函数的参数保持一致

"""
import multiprocessing, time


# 需求: 使用多进程来模拟小明一边写num行代码, 一边听count首音乐.
# 1. 小明写num行代码
def code(name, num):
    for i in range(1, num):
        # 为了让效果更明显, 加入: 休眠线程.
        time.sleep(0.01)
        print(f"{name} 正在写第 {i} 行代码... ")


# 2. 小明听count首音乐
def music(name, count):
    for i in range(1, count):
        # 为了让效果更明显, 加入: 休眠线程.
        time.sleep(0.01)
        print(f"{name} 正在听第 {i} 首音乐------ ")


# 3. 在main函数中编写测试代码.
if __name__ == '__main__':
    # 6. 看看我在不同的地方, 有什么结果.
    for i in range(200):
        time.sleep(0.01)
        print(f'看看我在什么地方---{i}')

    # 4. 创建进程对象.
    p1 = multiprocessing.Process(target=code, args=("小明", 20), name='QQ进程')
    # print(f'p1进程的名字: {p1.name}')
    p2 = multiprocessing.Process(target=music, kwargs={'count': 20, 'name': '小明'}, name='微信进程')
    # print(f'p2进程的名字: {p2.name}')

    # 5. 启动进程.
    p1.start()
    p2.start()

    # # 6. 看看我在不同的地方, 有什么结果.
    # for i in range(200):
    #     time.sleep(0.01)
    #     print(f'看看我在什么地方---{i}')

2.3 获取进程编号

"""
1、概述:
      在操作系统中, 每个进程都有自己唯一的ID, 且当前进程被终止时, 该ID会被回收, 即: ID是可以重复使用的.
2、目的:
      1. 查找子进程是由那个父进程创建的, 即: 找子进程和父进程的ID.
      2. 方便我们维护进程, 例如: kill -9 pid值 可以强制杀死进程.
3、获取进程id的方式:
      获取当前进程的ID:
           方式1: os模块的 getpid()方法
           方式2: multiprocessing模块的 pid属性
      获取当前进程的父进程的pid:
           os模块的 getppid()方法   parent process: 父进程
"""

代码如下(示例):

# 案例: 实现一边敲着第n行代码, 一边听着第n首音乐.
# 1. 定义函数, 表示: 敲代码.
def code(name, num):
    for i in range(1, num + 1):
        print(f'{name} 正在敲第 {i} 行代码...')
        time.sleep(0.1)
        # multiprocessing模块的current_process()函数: 获取当前进程对象
        print(f'当前进程(p1)的id: {os.getpid()}, {multiprocessing.current_process().pid}, 父进程的id为: {os.getppid()}')


# 2. 定义函数, 表示: 听音乐
def music(name, count):
    for i in range(1, count + 1):
        print(f'{name} 正在听第 {i} 首音乐.........')
        time.sleep(0.1)
        print(f'当前进程(p2)的id: {os.getpid()}, {multiprocessing.current_process().pid}, 父进程的id为: {os.getppid()}')


# 在main中测试.
if __name__ == '__main__':
    # 3. 创建进程对象.
    # Process类的参数: target: 关联的函数名, name: 当前进程的名字, args:元组的形式传参. kwargs: 字典的形式传参.
    p1 = multiprocessing.Process(name='Process_QQ', target=code, args=('张三', 10))
    p2 = multiprocessing.Process(name='Process_Wechat', target=music, kwargs={'count': 10, 'name': '李四'})

    # print(f"p1进程的名字: {p1.name}")
    # print(f"p2进程的名字: {p2.name}")

    # 4. 启动进程.
    p1.start()
    p2.start()

    print(f'当前进程(main)的id: {os.getpid()}, {multiprocessing.current_process().pid}, 父进程的id为: {os.getppid()}')

2.4 进程需要注意的点

2.4.1 进程间的数据是相互隔离的

解释

"""
大白话解释:
    1. 微信 和 QQ都是进程, 它们之间的数据都是 相互隔离的. 即: 微信不能直接访问QQ的数据, QQ也不能直接访问微信的数据.
    2. 下述代码你会发现, 多个子进程相当于把 父进程的资源全部拷贝了一份(注意: main外资源), 即: 子进程相当于父进程的副本.

需求: 定义全局变量 my_list = [], 搞两个进程, 分别实现往里边添加数据, 从里边读取数据, 并观察结果.
"""

代码如下(示例):

# 案例: 演示进程之间 数据是相互隔离的.

# 导包
import multiprocessing, time

# 1. 定义全局变量.
my_list = []

# 2. 定义函数 write_data(), 往: 列表中 添加数据.
def write_data():
    # 为了让效果更明显, 我们添加多个值.
    for i in range(1, 6):
        my_list.append(i)       # 具体添加元素到列表的动作.
        print(f'add: {i}')      # 打印添加细节(过程)
    print(f'write_data函数: {my_list}')       # 添加完毕后, 打印列表结果


# 3. 定义函数 read_data(), 从: 列表中 读取数据.
def read_data():
    # 休眠一会儿, 确保 write_data()函数执行完毕.
    time.sleep(3)
    print(f'read_data函数: {my_list}')       # 打印列表结果


# 4. 在main中测试
if __name__ == '__main__':
    # 4.1 创建两个进程对象.
    p1 = multiprocessing.Process(target=write_data)     # 进程1, 例如: QQ
    p2 = multiprocessing.Process(target=read_data)      # 进程2, 例如: 微信

    # 4.2 开启进程.
    p1.start()
    p2.start()

    # print('看看我打印了几遍? ')   # main函数内的内容, 只执行一次.

	print('看看我打印了几遍? ')   # main外资源, 自身要执行一次,  p1进程拷贝1遍,  p2进程拷贝1遍 = 3次

2.4.2 主进程会等待子进程执行结束(默认的)

解释

"""
案例:
    演示 默认情况下, 主进程会等待子进程执行结束在结束.

需求:
    创建1个子进程, 子进程执行完需要3秒钟, 让主进程1秒就结束, 观察: 整个程序是立即结束, 还是会等待子进程结束再结束.

结论:
    1. 默认情况下, 主进程会等待所有子进程执行结束再结束.
    2. 问: 如何实现, 当主进程结束的时候, 子进程也立即结束呢?
        方式1: 设置子进程为守护进程.   类似于: 公主 和 守护骑士.
        方式2: 手动关闭子进程.
"""

代码演示主进程关闭 ,但是在会等待子进程执行完毕

# 导包
import multiprocessing, time


# 1. 定义函数work(), 要被: 子进程关联.
def work():
    for i in range(10):
        print(f'工作中... {i}')
        time.sleep(0.3)     # 总计休眠时间: 0.3 * 10 = 3秒


# 2. 在main函数中测试.
if __name__ == '__main__':
    # 3. 创建子进程.
    p1 = multiprocessing.Process(target=work)
    # 4. 开启子进程.
    p1.start()

    # 5. 让主进程(main进程)休眠1秒.
    time.sleep(1)
    # 6. 主进程结束, 打印提示语句.
    print('主进程执行结束了!')

2.4.2 修改 主进程关闭 子进程也跟着立刻关闭

解释

"""
案例:
    演示 默认情况下, 主进程会等待子进程执行结束在结束.

需求:
    创建1个子进程, 子进程执行完需要3秒钟, 让主进程1秒就结束, 观察: 整个程序是立即结束, 还是会等待子进程结束再结束.

结论:
    1. 默认情况下, 主进程会等待所有子进程执行结束再结束.
    2. 问: 如何实现, 当主进程结束的时候, 子进程也立即结束呢?
        方式1: 设置子进程为守护进程.   类似于: 公主 和 守护骑士.
        方式2: 手动关闭子进程.

细节:
    1. 当非守护进程结束的时候, 它的所有守护进程都会立即终止, 且释放资源.
    2. 如果是 terminate()方式, 子进程主动结束自己, 则会变成僵尸进程, 不会立即释放资源.

"""

代码演示 主进程结束 子进程也立刻结束


# 导包
import multiprocessing, time


# 1. 定义函数work(), 要被: 子进程关联.
def work():
    for i in range(10):
        print(f'工作中... {i}')
        time.sleep(0.3)     # 总计休眠时间: 0.3 * 10 = 3秒


# 2. 在main函数中测试.
if __name__ == '__main__':
    # 3. 创建子进程.
    p1 = multiprocessing.Process(target=work)

    # 核心细节: 设置p1为守护线程, 因为p1的父进程是main, 所以p1是main的守护进程.
    # p1.daemon = True

    # 4. 开启子进程.
    p1.start()

    # 5. 让主进程(main进程)休眠1秒.
    time.sleep(1)

    # 方式2: 在主进程执行结束前, 子进程主动自己关闭自己.
    p1.terminate()  # 不推荐用, 会变成僵尸进程, 不会主动释放资源, 过段时间会交由(init进程, 充当父进程),来释放资源.

    # 6. 主进程结束, 打印提示语句.
    print('主进程执行结束了!')

三、线程

3.1 线程介绍

"""
1、线程:
       CPU调度资源的最小单位, 即: 进程的执行单元, 执行路径.
       可以理解为: 车道
2、多进程:
       多个*.exe, 例如: 微信.exe, QQ.exe, 即: 不同的软件.
       
3、多线程:
       同一个进程, 有多条执行路径, 例如: 微信.exe -> 查看钱包, 和张三聊天, 和李四聊天, 查看照片...
"""

代码演示 用多线程思路, 实现: 一边写代码, 一边听音乐.

# 导包
import threading, time

# 1. 定义函数coding(), 表示: 敲代码
def coding():
    for i in range(10):
        print(f'正在敲第 {i} 遍代码! -------')
        time.sleep(0.1)

# 2. 定义函数music(), 表示: 听音乐.
def music():
    for i in range(10):
        print(f'正在听第 {i} 首音乐!')
        time.sleep(0.1)

# 3. 在main中测试.
if __name__ == '__main__':
    # 4. 创建两个线程对象.
    t1 = threading.Thread(target=coding)
    t2 = threading.Thread(target=music)

    # 5. 开启线程.
    t1.start()
    t2.start()

3.2 多线程带参数

代码演示 用多线程思路, 实现: 一边写代码, 一边听音乐.(带参数版)

# 导包
import threading, time

# 1. 定义函数coding(), 表示: 敲代码
def coding(name, num):
    for i in range(num):
        print(f'{name} 正在敲第 {i} 遍代码! -------')
        time.sleep(0.1)

# 2. 定义函数music(), 表示: 听音乐.
def music(name, count):
    for i in range(count):
        print(f'{name} 正在听第 {i} 首音乐!', end='\n')
        time.sleep(0.1)

# 3. 在main中测试.
if __name__ == '__main__':
    # 4. 创建两个线程对象.
    # Thread()类中的参数如下:
    #   target      关联要执行的目标函数对象.
    #   name        设置线程名
    #   args        以元组形式传参, 个数, 顺序都要保持一致.
    #  kwargs       以字典形式传参, 个数, 参数名(键名和形参名)保持一致.
    t1 = threading.Thread(target=coding , args = ('张三', 9))
    t2 = threading.Thread(target=music, kwargs={'name': '李四', 'count': 10})

    # 5. 开启线程.
    t1.start()
    t2.start()

3.3 多线程注意的点

3.3.1 多线程执行顺序

解释

"""
结论:
    1. 多线程的执行具有 随机性, 因为: 线程是由CPU调度的.
    2. 调度资源常用的两种方式:
        抢占式调度: 谁抢到资源, 谁执行.
            Python, Java用的都是这种思路.
        均分时间片: 每个任务获取CPU的时间都是固定的.
"""

代码演示多线程代码 的执行顺序.

# 导包
import threading
import time


# 1. 定义函数get_info(), 用来打印: 线程信息, 看看当前是哪个线程在执行.
def get_info():
    # 休眠一会儿, 让效果更明显.
    time.sleep(0.1)

    # 1.1 获取当前的线程对象.
    ct = threading.current_thread()

    # 1.2 打印当前线程对象即可.
    print(f'当前的线程对象是: {ct}')


# 2. 在main中测试.
if __name__ == '__main__':
    # 3. 为了让效果更明显, 创建多个线程.
    # t1 = threading.Thread(target=get_info)
    # t2 = threading.Thread(target=get_info)
    #
    # t1.start()
    # t2.start()

    for i in range(10):
        t1 = threading.Thread(target=get_info)
        t1.start()

3.3.2 多线程-等待子线程结束再结束

解释

"""
结论:
    1. 默认情况下, 主线程会等待所有子线程执行结束再结束.
    2. 问: 如何实现, 当主线程结束的时候, 子线程也立即结束呢?
        设置子线程为守护线程.   类似于: 公主 和 守护骑士.

"""

代码 演示 默认情况下, 主线程会等待子线程执行结束在结束.

"""
需求:
    创建1个子线程, 子线程执行完需要3秒钟, 让主线程1秒就结束, 观察: 整个程序是立即结束, 还是会等待子线程结束再结束.
"""
# 导包
import threading, time


# 1. 定义函数work(), 要被: 子线程关联.
def work():
    for i in range(10):
        print(f'工作中... {i}')
        time.sleep(0.3)     # 总计休眠时间: 0.3 * 10 = 3秒


# 2. 在main函数中测试.
if __name__ == '__main__':
    # 3. 创建子线程.
    t1 = threading.Thread(target=work)
    # 4. 开启子线程.
    t1.start()

    # 5. 让主线程(main线程)休眠1秒.
    time.sleep(1)
    # 6. 主线程结束, 打印提示语句.
    print('主线程执行结束了!')

3.3.3 多线程-守护线程

代码演示 当非守护线程结束的时候, 和它关联的守护线程, 也会立即结束, 释放资源.

# 导包
import threading, time


# 1. 定义函数work(), 要被: 子线程关联.
def work():
    for i in range(10):
        print(f'工作中... {i}')
        time.sleep(0.3)  # 总计休眠时间: 0.3 * 10 = 3秒


# 2. 在main函数中测试.
if __name__ == '__main__':
    # 3. 创建子线程.
    # 守护线程, 格式1: 创建线程对象的时候, 直接指定.
    # t1 = threading.Thread(target=work, daemon=True)

    t1 = threading.Thread(target=work)
    # 守护线程, 格式2: 创建线程对象后, 通过 daemon属性设置.
    # t1.daemon = True

    # 守护线程, 格式3: 创建线程对象后, 通过 setDaemon()函数设置.
    t1.setDaemon(True)      # 类似于以前我们学的: get_name(), set_name()

    # 4. 开启子线程.
    t1.start()

    # 5. 让主线程(main线程)休眠1秒.
    time.sleep(1)

    # 6. 主线程结束, 打印提示语句.
    print('主线程执行结束了!')

3.3.4 多线程共享全局变量

解释

"""
结论:
    1. 进程之间, 数据相互隔离.        进程 = 软件.
    2. 线程之间, 数据共享.
"""

代码演示 同一进程的多个线程, 可以共享 该进程的资源.

# 导包
import threading, time

# 1. 定义全局变量.
my_list = []


# 2. 定义函数write_data(), 实现: 往列表中添加数据.
def write_data():
    # 为了效果更明显, 我们添加多个元素.
    for i in range(1, 6):
        # time.sleep(0.1)
        # 往列表中添加元素.
        my_list.append(i)
        # 打印添加的细节
        print(f'add: {i}')

    # 添加之后, 打印结果.
    print(f'write_data函数: {my_list}')


# 3. 定义函数read_data(), 实现: 从列表中读取数据.
def read_data():
    time.sleep(2)     # 为了效果更好看, 加入休眠线程.
    print(f'read_data函数: {my_list}')


# 4. main函数中进行测试.
if __name__ == '__main__':
    # 4.1 创建两个线程对象, 分别关联: write_data(), read_data()函数.
    t1 = threading.Thread(target=write_data)
    t2 = threading.Thread(target=read_data)

    # 4.2 启动线程.
    t1.start()
    t2.start()

3.3.5 数据安全

解释

"""
案例:  线程可以共享全局变量, 同时操作, 可能会出现: 安全问题.

1、遇到的问题:
      两个线程同时操作共享变量, 进行累加操作, 出现: 累计次数"不够"的情况, 我们要的效果是:  get_sum1: 100W,  get_sum2:200W, 但是结果不是.
2、产生原因:
      正常情况:
         假设g_num = 0
            第1次循环:  t1线程抢到资源, 对其累加1, 操作后 g_num = 1, t1线程本次操作完毕.
            第2次循环: 假设t2线程抢到了资源, 此时 g_num = 1, 操作之后(累加1), g_num = 2, t2线程本次操作完毕.
        这个是正常情况.

      非正常情况:
          1. 假设 g_num = 0
          2. 假设 t1 线程抢到了资源, 在还没有来得及对 g_num(全局变量)做 累加操作的时候, 被t2线程抢走了资源.
          3. t2线程也会去读取 g_num(全局变量)的值, 此时 g_num = 0
          4. 之后t1 和 t2线程分别开始了 累加操作:
              t1线程, 累加之后,   g_num = 0     ->    g_num = 1
              t2线程, 累加之后,   g_num = 0     ->    g_num = 1
          5. 上述一共累加了 2次, 但是 g_num的值最终只 累加了 1.
"""

代码如下(示例):

import threading

# 需求: 定义两个函数, 分别对全局变量累加100W次, 并打印观察程序的最终运行结果.

# 1. 定义全局变量.
g_num = 0

# 2. 定义函数 get_sum1(), 实现对全局变量累加 100W次
def get_sum1():
    for i in range(1000000):
        global g_num
        g_num += 1      # 全局变量 + 1
    # 程序计算完毕后, 打印结果.
    print(f'get_sum1函数计算结果: {g_num}')       # 100W

# 3. 定义函数 get_sum2(), 实现对全局变量累加 100W次
def get_sum2():
    for i in range(1000000):
        global g_num
        g_num += 1      # 全局变量 + 1
    # 程序计算完毕后, 打印结果.
    print(f'get_sum2函数计算结果: {g_num}')       # 200W

# 4. 测试
if __name__ == '__main__':
    # 4.1 创建两个线程对象, 分别关联: 两个函数.
    t1 = threading.Thread(target=get_sum1)
    t2 = threading.Thread(target=get_sum2)

    # 4.2 启动线程.
    t1.start()
    t2.start()

3.4 互斥锁的使用

解释

"""
为了解决上述 数据安全出现的问题,我们采用的解决方案:

    		采用 线程同步 解决.

线程同步解释:
      1、概述:
           线程同步, 也叫线程安全, 它是用来解决 多线程操作共享变量, 可能出现非法值的问题的.
      2、解决思路:
            加锁.
      3、步骤:
          1). 创建锁.
          2). 加锁.
          3). 释放锁.
细节:
      1. 必须使用同一把锁, 否则可能出现锁不住的情况.
      2. 选择合适的时机要释放锁, 否则可能出现死锁的情况.


"""

代码如下(示例):

import threading

# 需求: 定义两个函数, 分别对全局变量累加100W次, 并打印观察程序的最终运行结果.

# 1. 定义全局变量.
g_num = 0

# 核心细节, 创建: 锁(互斥锁)
mutex = threading.Lock()        # mutex是变量名, 符合规范即可. mutex单词 是 互斥的意思.
# mutex2 = threading.Lock()        # mutex是变量名, 符合规范即可. mutex单词 是 互斥的意思.

# 2. 定义函数 get_sum1(), 实现对全局变量累加 100W次
def get_sum1():

    # 加锁
    mutex.acquire()
    for i in range(1000000):
        global g_num
        g_num += 1      # 全局变量 + 1
    # 程序计算完毕后, 打印结果.
    # 解锁
    mutex.release()
    print(f'get_sum1函数计算结果: {g_num}')       # 100W

# 3. 定义函数 get_sum2(), 实现对全局变量累加 100W次
def get_sum2():
    # 加锁
    mutex.acquire()
    for i in range(1000000):
        global g_num
        g_num += 1      # 全局变量 + 1
    # 程序计算完毕后, 打印结果.
    # 解锁
    mutex.release()
    print(f'get_sum2函数计算结果: {g_num}')       # 200W

# 4. 测试
if __name__ == '__main__':
    # 4.1 创建两个线程对象, 分别关联: 两个函数.
    t1 = threading.Thread(target=get_sum1)
    t2 = threading.Thread(target=get_sum2)

    # 4.2 启动线程.
    t1.start()
    t2.start()

四、进程和线程的对比

4.1 关系对比

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

4.2 区别对比

  • 进程之间不共享全局变量
  • 线程之间共享全局变量,但是要注意资源竞争的问题,解决办法: 互斥锁
  • 创建进程的资源开销要比创建线程的资源开销要大
  • 进程是操作系统资源分配的基本单位,线程CPU调度的基本单位
  • 线程不能够独立执行,必须依存在进程中
  • Python中多进程开发比单进程多线程开发稳定性要强

4.3 优缺点对比

进程优缺点:
——优点:可以用多核
——缺点:资源开销大
线程优缺点:
——优点:资源开销小
——缺点:不能使用多核

总结

  • 我们将多进程跟多线程放在一起对比学习,能够更清楚的发现进程跟线程的相似点与不同点。
  • 35
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值