python进阶篇-day07-进程与线程

day06进程与线程

一. 进程

每个软件都可以看作是一个进程(数据隔离)

软件内的多个任务可以看作是多个线程(数据共享)

单核CPU: 宏观并行, 微观并发

真正的并行必须有多核CPU

多任务介绍

概述

多任务指的是, 多个任务"同时"执行

目的

节约资源, 充分利用CPU资源, 提高效率

表现形式

并发

针对于单核CPU来讲, 如果有多个任务同时请求执行, 但是同一瞬间CPU只能执行1个(任务), 于是就安排他们交替执行.

因为时间间隔非常短, 所以宏观上看是并行, 但是微观上还是并发的.

并行

针对多核CPU来讲, 多个任务可以同时执行

进程介绍

概述

进程: 指的是可执行程序(*.exe), 也是CPU分配资源的最小单位.

线程: 进程的执行路径, 执行单元, 也是CPU调度资源的最小单位

解释:

进程: 车

线程: 车道

多进程实现

步骤

  1. 导包(multiprocessing)

  2. 创建进程对象, 关联 该进程要执行的任务(函数)

  3. 启动进程执行任务

代码
import multiprocessing, time
​
​
# 定义代码函数
def coding():
    for i in range(1, 21):
        time.sleep(1)
        print(f'正在敲代码----  {i}')
​
        
# 定义音乐函数
def music():
    for i in range(1, 21):
        time.sleep(1)
        print(f'正在听音乐****  {i}')
​
​
if __name__ == '__main__':
    # 创建进程对象
    p1 = multiprocessing.Process(target=coding)
    p2 = multiprocessing.Process(target=music)
    # 执行进程
    p1.start()
    p2.start()

进程参数

参数

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

代码演示
import multiprocessing, time
​
​
def coding(name, num):
    for i in range(1, num):
        time.sleep(0.01)
        print(f'{name}正在敲第{i}行代码-')
​
​
def music(name, num):
    for i in range(1, num):
        time.sleep(0.01)
        print(f'{name}正在听第{i}首音乐********')
​
​
if __name__ == '__main__':
    p1 = multiprocessing.Process(target=coding, args=('小明', 21))
    p2 = multiprocessing.Process(target=music, kwargs={'num': 21, 'name': '小明'})
    p11 = multiprocessing.Process(target=coding, args=('小明', 21), name='QQ')
    p22 = multiprocessing.Process(target=music, kwargs={'num': 21, 'name': '小明'}, name='WX')
    print(f'p1:{p1.name}')
    print(f'p2:{p2.name}')
    print(f'p11:{p11.name}')
    print(f'p22:{p22.name}')
    p1.start()
    p2.start()
​

main进程

解释

main程序入口也相当于一个进程, 在程序执行时遇到自定义进程会发生资源抢占, 上述代码中在输出进程对象后, 进程才启动, 所以上述的自定义进程不会和main进程强制资源, 并且自定义进程的启动需要一定时间, 此时的main进程可能已经完成自己的任务, 执行自定义进程.

图解

代码
import multiprocessing, time
​
​
def coding(name, num):
    for i in range(1, num):
        time.sleep(0.01)
        print(f'{name}正在敲第{i}行代码-')
​
​
def music(name, num):
    for i in range(1, num):
        time.sleep(0.01)
        print(f'{name}正在听第{i}首音乐********')
​
​
if __name__ == '__main__':
    p1 = multiprocessing.Process(target=coding, args=('小明', 11))
    p2 = multiprocessing.Process(target=music, kwargs={'num': 11, 'name': '小明'})
    # p11 = multiprocessing.Process(target=coding, args=('小明', 11), name='QQ')
    # p22 = multiprocessing.Process(target=music, kwargs={'num': 11, 'name': '小明'}, name='WX')
    p1.start()
    p2.start()
    # print(f'p1:{p1.name}')
    # time.sleep(0.1)
    # print(f'p2:{p2.name}')
    # time.sleep(0.1)
    # print(f'p11:{p11.name}')
    # time.sleep(0.1)
    # print(f'p22:{p22.name}')
​
    for i in range(1, 21):
        print(f'main: -------  {i}')
        time.sleep(0.1)

注: 上述代码中, 若将main中的循环放到自定义进程前, 则自定义进程不会和main抢占资源, 因为执行完main中的循环才执行到自定义进程, 可以把main函数看作栈, 从上到下执行

代码执行结果(每次可能都不一样)

进程编号

前面创建的进程为main进程的子进程, main为前面进程的父进程,

main的父进程为pycharm

程序执行默认有main函数

在操作系统中, 每个进程都有自己唯一的ID, 且当前进程被终止时, 该ID会被回收, 即: ID是可以重复使用的.

目的

  1. 查找子进程是由那个父进程创建的, 即: 找子进程和父进程的ID.

  2. 方便我们维护进程, 例如: kill -9 pid值 可以强制杀死进程.

当前编号

方式1: os模块的 getpid()方法方式2: multiprocessing模块的current_process()方法的 pid属性

当前的父进程编号

os模块的 getppid()方法 parent process: 父进程

代码演示

# 导包
import multiprocessing, time, os
​
​
# 案例: 小明一边敲着第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()}')

执行效果

进程注意事项

关于进程, 你要记忆的内容: 1. 进程之间数据是相互隔离的. 例如: 微信(进程) 和 QQ(进程)之间, 数据就是相互隔离的.

  1. 默认情况下, 主进程会等待它所有的子进程 执行结束再结束.

细节: 多进程之间, 针对于 main进程的(外部资源), 每个子进程都会拷贝一份, 进行执行.

数据隔离

代码
import multiprocessing, time
​
# 需求: 定义1个列表, 然后定义两个函数, 分别往列表中添加数据, 获取数据. 
# 之后用两个进程关联这个两个函数, 启动进程并观察结果.
# 1. 定义全局变量 my_list
my_list = []
print('我是main外资源, 看看我执行了几遍!')   # 执行三次
​
# 2. 定义函数, 实现往列表中添加数据.
def write_data():
    for i in range(1, 6):
        # 具体的添加元素的动作
        my_list.append(i)
        # 打印添加动作.
        print(f'添加 {i} 成功!')
    # 细节: 添加数据完毕后, 打印结果.
    print(f'write_data函数: {my_list}')
​
# 3. 定义函数, 实现从列表中获取数据.
def read_data():
    # 休眠 3 秒, 确保 write_data函数执行完毕.
    time.sleep(3)
    # 打印结果.
    print(f'read_data函数: {my_list}')
​
​
# 在main中测试
if __name__ == '__main__':
    # 4. 创建进程对象.
    p1 = multiprocessing.Process(target=write_data)
    p2 = multiprocessing.Process(target=read_data)
​
    # 5. 启动进程
    p1.start()
    p2.start()
​
    print('我是main内资源, 看看我执行了几遍!')   # 执行一次
执行结果

(默认)主进程等待子进程结束

演示
import multiprocessing, time
​
​
# 需求: 设置子进程执行3秒, 主进程执行1秒, 观察效果.
# 1. 定义方法, 用于关联子进程的.
def my_method():
    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=my_method)
    p1.start()  # 3秒后结束.
​
    # 4. 主进程执行1秒后结束.
    time.sleep(1)
​
    # 5. 打印主进程的结束提示.
    print('主进程 main 执行结束!')
解决
  1. 设置子进程为守护进程 p1.daemon = True 推荐

    类似于: 骑士(守护) 和 公主(非守护)

  2. 手动关闭子进程 p1.terminate() 不推荐(僵尸进程)

import multiprocessing, time
​
​
# 需求: 设置子进程执行3秒, 主进程执行1秒, 观察效果.
# 1. 定义方法, 用于关联子进程的.
def my_method():
    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=my_method)
    # 方式1: 设置子进程p1为守护进程, 
    # 非守护进程是: main进程, 所以: 当main进程关闭的时候, 它的守护进程也会关闭.
    # p1.daemon = True
    p1.start()  # 3秒后结束.
​
    # 4. 主进程执行1秒后结束.
    time.sleep(1)
​
    # 方式2: 手动关闭子进程.
    # 会导致子进程变成僵尸进程, 即: 不会立即释放资源.
    # 而是交由init进程接管(充当新的父进程), 在合适的时机释放资源.
    p1.terminate()      # 不推荐使用.
​
    # 5. 打印主进程的结束提示.
    print('主进程 main 执行结束!')

二. 线程

介绍

线程是CPU调度资源的最基本单位, 进程是CPU分配资源的基本单位.进程 = 可执行程序, 文件. 即: *.exe = 进程, 微信, QQ都是进程.线程 = 进程的执行路径, 执行单元. 微信这个进程, 可以实现: 和张三聊聊天, 和李四聊天, 查看朋友圈, 微信支付... 车在车道上跑, 有: 单行道, 双车道, 四车道, 八车道...

多线程实现

无论是进程, 还是线程, 都是实现 多任务的一种方式, 目的都是: 充分利用CPU资源, 提高效率.线程的操作步骤: 1. 导包. 2. 创建线程对象. 3. 启动线程.

代码
# 导包
import threading, time
​
# 1.定义函数, 表示: 敲代码.
def coding():
    for i in range(10):
        print(f"正在敲代码... {i}")
        time.sleep(0.1)
​
​
# 2.定义函数, 表示: 听音乐
def music():
    for i in range(10):
        print(f"正在听音乐... {i}")
        time.sleep(0.1)
​
# 在main中测试.
if __name__ == '__main__':
    # 3. 创建线程对象, 分别关联上述的两个函数.
    t1 = threading.Thread(target=coding)
    t2 = threading.Thread(target=music)
​
    # 4. 启动线程.
    t1.start()
    t2.start()

带有参数

Thread类(线程类)中的参数 和 Process类(进程类)的参数几乎一致: target: 关联目标函数的. name: 线程名(Thread-1, Thread-2...) 或者 进程名(Process-1, Process-2...). args: 以 元组的 形式传参, 个数及对应的类型都要一致. kwargs: 以 字典的 形式传参, 个数要一致.

# 导包
# from threading import Thread
import threading, time
​
​
# 1. 定义函数, 实现: 小明正在敲第n行代码.
def coding(name, num):
    for i in range(1, num + 1):
        print(f'{name} 正在敲第 {i} 行代码...')
        time.sleep(0.1)
​
​
# 2. 定义函数, 实现: 小明正在听第n首音乐
def music(name, count):
    for i in range(1, count + 1):
        print(f'{name} 正在听第 {i} 首音乐......')
        time.sleep(0.1)
​
​
# 在main中测试
if __name__ == '__main__':
    # 3. 创建线程对象.
    t1 = threading.Thread(name='杨过', target=coding, args=('乔峰', 10))
    t2 = threading.Thread(name='大雕', target=music, kwargs={'count': 10, 'name': '慕容复'})
    # print(f't1线程的名字: {t1.name}')
    # print(f't2线程的名字: {t2.name}')
​
    # 4. 启动线程.
    t1.start()
    t2.start()

线程注意事项

记忆:

  1. 多线程的执行具有 随机性(无序性), 其实就是在抢CPU的过程, 谁抢到, 谁执行.

  2. 默认情况下: 主线程会等待子线程执行结束再结束.

  3. 线程之间 会共享当前进程的 资源.

  4. 多线程环境 并发 操作共享资源, 有可能引发安全问题, 需要通过 线程同步(加锁) 的思想来解决.

关于CPU的资源分配, 调度, 思路主要有两种: 1. 均分时间片, 即: 每个进程(线程)占用CPU的时间都是 相等的. 2. 抢占式调度, 谁抢到, 谁执行. Python用的是这种.

无序

# 导包
import threading, time
​
​
# 需求: 创建多个线程, 多次运行, 观察歌词线程的执行顺序.
# 1. 定义函数, 获取线程, 并打印.
def get_info():
    # 休眠.
    time.sleep(0.5)
​
    # 获取当前的线程对象, 并打印.
    cur_thread = threading.current_thread()
    print(f'当前线程是: {cur_thread}')
​
​
​
# 在main中测试
if __name__ == '__main__':
    # 2. 创建多个线程对象.
    for i in range(10):
        th = threading.Thread(target=get_info)
        # 3. 启动线程即可.
        th.start()

(默认)主线程等待子线程结束

演示
import threading, time
​
​
# 1. 定义函数, 执行: 3秒.
def coding():
    for i in range(10):
        print(f'coding... {i}')
        time.sleep(0.3)     # 总休眠时间 = 0.3 * 10 = 3秒
​
​
# 在main中测试
if __name__ == '__main__':
    # 2. 创建线程对象.
    th = threading.Thread(target=coding)
​
    # 3. 启动线程.
    th.start()
​
    # 4. 设置主线程(main线程), 执行1秒就关闭.
    time.sleep(1)
​
    # 5. 提示即可.
    print('主线程(main)执行结束了!')
解决

设置子线程为守护线程

import threading, time
​
​
# 1. 定义函数, 执行: 3秒.
def coding():
    for i in range(10):
        print(f'coding... {i}')
        time.sleep(0.3)     # 总休眠时间 = 0.3 * 10 = 3秒
​
​
# 在main中测试
if __name__ == '__main__':
    # 2. 创建线程对象.
    # 方式1: 设置th线程为: 守护线程.
    # th = threading.Thread(target=coding, daemon=True) # 推荐使用.
​
    # 方式2: setDaemon()函数实现.
    th = threading.Thread(target=coding)
    th.setDaemon(True)  # 函数已过时, 推荐使用方式1.
​
    # 3. 启动线程.
    th.start()
​
    # 4. 设置主线程(main线程), 执行1秒就关闭.
    time.sleep(1)
​
    # 5. 提示即可.
    print('主线程(main)执行结束了!')

共享全局变量

代码
import threading
import time
​
my_list = []
print('main外输出')
​
​
def write_list():
    for i in range(10):
        my_list.append(i)
        print(f'{i} 已经添加到列表中')
    print('write_list:', my_list)
​
​
def read_list():
    time.sleep(1)
    print('read_list:', my_list)
​
​
if __name__ == '__main__':
    th1 = threading.Thread(target=write_list)
    th2 = threading.Thread(target=read_list)
    th1.start()
    th2.start()
执行结果

数据安全

多线程环境 并发 操作共享资源, 有可能引发安全问题, 需要通过 线程同步(加锁) 的思想来解决.参照4

演示:
# 导包
# 导包
import threading
import time
​
# 定义全局变量
num = 0
​
​
# 定义函数add1()累加
def add1():
    global num
    for i in range(1000000):
        num += 1
    print(f'add1: {num}')
​
​
# 定义函数add2()累加
def add2():
    global num
    for i in range(1000000):
        num += 1
    print(f'add2: {num}')
​
​
if __name__ == '__main__':
    # 创建线程对象
    th1 = threading.Thread(target=add1)
    th2 = threading.Thread(target=add2)
​
    # 启动线程
    th1.start()     # 19328862
    th2.start()     # 20000000

问题描述

两个线程分别对全局变量累加100W次, 正常应为100W 和 200W的结果,但是结果和想象不一样

原因

多线程环境 并发 操作共享资源, 引发安全问题

  • 正常情况:

  1. 假设 全局变量 global_num = 0

  2. 此时 线程t1抢到了资源, 执行累加(一次)动作, 累加后: global_num = 1

  3. 假设 线程t2抢到了资源, 执行累加(一次)动作, 累加后: global_num = 2

  • 非正常情况:

  1. 假设 全局变量 global_num = 0

  2. 此时 线程t1抢到了资源, 但是还没有来得及执行累加动作时, 被t2线程抢走了资源.

  3. 此时 线程t2读取到的 global_num = 0

  4. 此时就会出现 线程t1累加1次, global_num = 1, 线程t2累加1次, global_num = 1

  5. 综上所述, t1和t2线程一共累加了2次, 但是 global_num的值 只加了1

  6. 之所以会有这样的情况, 原因是: 1个线程在执行某1个完整动作期间, 可以被别的前程抢走资源, 就有可能引发安全问题.

解决方案

采用 线程同步 的思想, 即: 加锁

# 导包
import threading
import time
​
# 定义全局变量
num = 0
mutex = threading.Lock()
​
​
# 定义函数add1()累加
def add1():
    # 加锁
    mutex.acquire()
    global num
    for i in range(1000000):
        num += 1
    print(f'add1: {num}')
    # 释放锁
    mutex.release()
​
​
# 定义函数add2()累加
def add2():
    # 加锁
    mutex.acquire()
    global num
    for i in range(1000000):
        num += 1
    print(f'add2: {num}')
    # 释放锁
    mutex.release()
​
​
if __name__ == '__main__':
    # 创建线程对象
    th1 = threading.Thread(target=add1)
    th2 = threading.Thread(target=add2)
​
    # 启动线程
    th1.start()  # 19328862
    th2.start()  # 20000000

线程同步

用于解决多线程 并发 操作共享变量的安全问题的, 保证同一时刻只有1个线程操作共享变量.

锁的概述

互斥锁:

对共享数据进行锁定,保证同一时刻只有一个线程去操作。

注:

互斥锁是多个线程一起去抢,抢到锁的线程先执行,没有抢到锁的线程进行等待,等锁使用完释放后,其它等待的线程再去抢这个锁。

使用步骤

  1. 创建锁

  2. 在合适的地方加锁

  3. 在合适的地方释放锁

代码
# 导包
import threading, time
​
# 1. 定义全局变量.
global_num = 0
​
# 创建锁.
mutex = threading.Lock()        # 互斥锁.
# mutex2 = threading.Lock()        # 互斥锁.
​
# 2. 定义函数1, 对全局变量 global_num累加100W次.
def get_sum1():
    # 加锁.
    mutex.acquire()
    # 声明变量为 全局变量.
    global global_num
    # 具体的累加动作.
    for i in range(1000000):
        global_num += 1
    # 解锁
    mutex.release()
    # 累加完毕后, 打印结果.
    print(f'get_sum1函数执行完毕, global_num = {global_num}')
​
​
# 3. 定义函数2, 对全局变量 global_num累加100W次.
def get_sum2():
    # 加锁.
    # mutex2.acquire()
    mutex.acquire()
    # 声明变量为 全局变量.
    global global_num
    # 具体的累加动作.
    for i in range(1000000):
        global_num += 1
    # 解锁
    # mutex2.release()
    mutex.release()
    # 累加完毕后, 打印结果.
    print(f'get_sum2函数执行完毕, global_num = {global_num}')
​
# main函数, 测试
if __name__ == '__main__':
    # 4. 创建线程对象.
    t1 = threading.Thread(target=get_sum1)
    t2 = threading.Thread(target=get_sum2)
​
    # 5. 启动线程.
    t1.start()
    t2.start()

注意事项

  1. 必须使用同一把锁, 否则可能锁不住(同一块cpu有两个锁两个门)

  2. 在合适的地方释放锁, 否则可能死锁

进程与线程的区别

关系

线程依附于进程, 没有进程就没有线程

一个进程默认提供一个线程, 可以创建多个进程

区别

  1. 进程间数据隔离

  2. 线程数据共享,但是要注意资源竞争 ,可以加互斥锁

  3. 进程比线程的资源开销大

  4. 线程是CPU调度资源的最基本单位, 进程是CPU分配资源的基本单位

  5. 线程不能独立执行, 必须存在于进程中

  6. python中 多进程单进程多线程 稳定

优缺点

  • 进程:

优点: 可以使用多核

缺点: 资源开销大

  • 线程:

优点: 资源开销小

缺点: 不可以使用多核

总结

线程依赖于进程

进程数据隔离, 线程数据共享

进程资源开销比线程资源开销大, 所以相对更稳定

无论多线程还是多进程都可以实现多任务, 目的都是: 充分利用cpu资源, 提高程序的执行效率

  • 24
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值