进程与线程

一. 进程

创建进程的本质 >>> 在内存中申请一块内存空间用于运行相应的程序代码

1.1创建进程

★★★强调:

不同的操作系统创建进程的要求不一样

在 windows 中创建进程是以导入模块的方式进行 所以创建进程的代码必须写在__main__子代码中

否则会直接报错 因为在无限制创建进程

在linux和mac中创建进程是直接拷贝一份源代码然后执行 不需要写在__main__子代码中

方式一

from multiprocessing import Process
import time


def task(name):
    print(f'{name} is runing')
    time.sleep(2)
    print(f'{name} is over')


if __name__ == '__main__':

    # 创建一个进程
    p = Process(target=task, args=('进程1',))
    # 启动进程
    p.start()

    print("主进程")

方式二

from multiprocessing import Process
import time


class MyProcess(Process):
    def __init__(self, username):
        self.username = username
        super().__init__()

    def run(self):
        print('你好啊 小姐姐', self.username)
        time.sleep(2)
        print('get out!!!', self.username)


if __name__ == '__main__':

    # 创建一个进程
    p = MyProcess('tony')
    # 启动进程
    p.start()

    print('主进程')

1.2 join方法

让主进程代码等待子进程代码运行完毕再执行

from multiprocessing import Process
import time


def task(name, n):
    print(f'{name} is running')
    time.sleep(n)
    print(f'{name} is over')


if __name__ == '__main__':
    p1 = Process(target=task, args=('jason', 1))
    p2 = Process(target=task, args=('tony', 2))
    p3 = Process(target=task, args=('kevin', 3))
    start_time = time.time()
    p1.start()
    p2.start()
    p3.start()
    p1.join()
    p2.join()
    p3.join()
    end_time = time.time() - start_time
    print('主进程', f'总耗时:{end_time}')

# 加 join 运行结果
"""
  jason is running
  tony is running
  kevin is running
  jason is over
  tony is over
  kevin is over
  主进程 总耗时:3.163069725036621
"""

# 不加 join 运行结果
"""
  主进程 总耗时:0.03191542625427246
  jason is running
  tony is running
  kevin is running
  jason is over
  tony is over
  kevin is over
"""

1.3 进程间数据默认隔离

"""默认隔离  但是可以通过一些技术打破"""

# 内存可以看成是有很多个小隔间组成的 彼此不干扰
from multiprocessing import Process

money = 999

def task():
    global money  # 局部修改全局不可变类型
    money = 666


if __name__ == '__main__':
    p = Process(target=task)
    p.start()
    p.join()  # 确保子进程代码运行结束再打印money
    print(money)

# 输出结果:
"""
  999
"""

1.4 进程对象属性和方法

"""
进程号如何查看
    windows:   tasklist结果集中PID
    mac:       ps -ef
"""

1.查看进程号的方法
    1.1.current_process函数
        from multiprocessing import Process, current_process
        current_process().pid
    1.2.os模块
        os.getpid()      # 获取当前进程的进程号
        os.getppid()     # 获取当前进程的父进程号

2.杀死子进程
    terminate()
3.判断子进程是否存活
    is_alive()

1.5 僵尸进程与孤儿进程

1. 僵尸进程
"""
  为什么主进程默认需要等待子进程结束才会结束
  所有的子进程在运行结束之后都会变成僵尸进程(死了没死透)
  还保留着pid和一些运行过程的中的记录便于主进程查看(短时间保存)
  这些信息会被主进程回收(僵尸彻底死了)
"""
    1.主进程正常结束
    2.调用join方法

2. 孤儿进程
      子进程存活着 父进程意外死亡
      子进程会被操作系统自动接管(儿童福利院)

1.6 守护进程

守护进程(daemon)是一类在后台运行的特殊进程,是一个在后台运行并且不受任何终端控制的进程。用于执行特定的系统任务。很多守护进程在系统引导的时候启动,并且一直运行直到系统关闭。另一些只在需要的时候才启动,完成任务后就自动结束。守护进程最重要的特性是后台运行。

""" 守护进程会随着主进程代码的执行完毕而结束。 """


from multiprocessing import Process
import time


def task(name):
    print(f'大内总管:{name}正常活着')
    time.sleep(3)
    print(f'大内总管:{name}正常死了')


if __name__ == '__main__':
    p = Process(target=task, args=('赵公公',))

    # 必须写在start前面
    p.daemon = True         # 将子进程设置为守护进程:主进程结束 子进程立刻结束

    p.start()
    time.sleep(1)
    print('皇帝Jason寿终正寝')

# 运行结果

"""
  大内总管:赵公公正常活着
  皇帝Jason寿终正寝
"""

1.7 互斥锁(重要)

"""
锁相关知识 (其他类型的锁)
    行锁:针对行数据加锁 同一时间只能一个人操作
    表锁:针对表数据加锁 同一时间只能一个人操作
锁的应用范围很广 但是核心都是为了保证数据的安全!!!
"""

"""
   每逢节假日抢票
       手机上明明显示还有余票 但是点击购买的时候却提示已经没有票了
       之后回到查询页面发现确实显示没有票了

   上午10:00打开买票软件查看票数 系统给你发过来的是10:00对应的数据
   只要你页面不刷新不点击下一步 那么页面数据永远展示的是10:00的
"""

** 模拟抢票 **

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


# 查票
def search(name):
    with open(r'./ticket_data/json', 'r', encoding='utf8') as f:
        data = json.load(f)
    print(f'{name}查询当前余票:%s' % data.get('ticket_num'))


# 买票
def buy(name):
    '''
    点击买票是需要再次查票的 因为期间其他人可能已经把票买走了
    '''
    # 1.查票
    with open(r'./ticket_data/json', 'r', encoding='utf8') as f:
        data = json.load(f)
    time.sleep(random.randint(1, 2))
    # 2.判断是否还有余票

    if data.get('ticket_num') > 0:
        data['ticket_num'] -= 1
        with open(r'./ticket_data/json', 'w', encoding='utf8') as f:
            json.dump(data, f)
        print(f'{name}抢票成功')
    else:
        print(f'{name}抢票失败 没有余票了')


def run(name, mutex):                                                            # 3. 接受创建的锁
    search(name)

    mutex.acquire()                 # 4. 抢锁
    buy(name)
    mutex.release()                 # 5. 释放锁


# 模拟多人同时抢票
if __name__ == '__main__':

    mutex = Lock()                                                               # 1. 创建锁

    for i in range(1, 10):
        p = Process(target=run, args=('用户:%s' % i, mutex))                     # 2. 将创建的锁通过参数传递给进程的方法
        p.start()

1.8 消息队列

"""
  队列:先进先出(使用频率很高)
  堆栈:先进后出(特定常见下用)
"""
# 导入模块
from multiprocessing import Queue

# 定义队列的长度
q = Queue(3)

# 向队列中添加数据
q.put(1)
q.put(2)

# 查看队列是否满了
print(q.full())
q.put(3)
print(q.full())

# 获取队列的值
print(q.get())
print(q.get())

# 查看队列是否为空
print(q.empty())
print(q.get())
print(q.empty())

# 队列中如果没有值 直接报错
print(q.get_nowait())

"""
    full()
    empty()
    get_nowait()
上述方法能否在并发的场景下精准使用???
    不能用!!!

之所以介绍队列是因为它可以支持进程间数据通信
"""

1.9 IPC机制(进程间通信)

"""
1.主进程与子进程数据交互
2.两个子进程数据交互
本质:不同内存空间中的进程数据交互
"""
from multiprocessing import Process, Queue


def producer(q):
    # print('子进程producer从队列中取值>>>:', q.get())
    q.put('子进程producer往队列中添加值')

def consumer(q):
    print('子进程consumer从队列中取值>>>:', q.get())


if __name__ == '__main__':
    q = Queue()
    p = Process(target=producer, args=(q, ))
    p1 = Process(target=consumer, args=(q,))
    p.start()
    p1.start()
    # q.put(123)  # 主进程往队列中存放数据123
    print('主进程')

1.10 进程状态

image

二. 线程

2.1 线程理论

1. 什么是线程

 '''一个进程中至少有一个线程'''

1. 进程:
    资源单位, 相当于车间(一个个空间)
2. 线程:
    执行单位, 相当于车间里面的流水线(真正干活的)


"""
  进程仅仅是在内存中开辟一块空间(提供线程工作所需的资源)
  线程真正被CPU执行,线程需要的资源跟所在的进程要
"""

2. 为什么要有线程

开设线程的消耗远远小于进程

1. 开进程
    1.申请内存空间
    2.拷贝代码
2. 开线程
    一个进程内可以开设多个线程 无需申请内存空间、拷贝代码
    一个进程内的多个线程数据是共享的

"""
开发一个文本编辑器
    获取用户输入并实时展示到屏幕上
    并实时保存到硬盘中
多种功能应该开设多线程而不是多进程
"""

2.2 开设线程的两种方式

方式一

from threading import Thread
import time


def task(name):
    print(f'{name} is running')
    time.sleep(3)
    print(f'{name} is over')


# 创建线程无需在__main__下面编写 但是为了统一 还是习惯在子代码中写
t = Thread(target=task, args=('子线程1',))

# 创建线程的开销极小 几乎是一瞬间就可以创建
t.start()
print('主线程')

方式二

from threading import Thread
import time


class MyThread(Thread):
    def __init__(self, username):
        super().__init__()
        self.username = username

    def run(self):
        print(f'{self.username} is running')
        time.sleep(3)
        print(f'{self.username} is over')


t = MyThread('子线程1')
t.start()
print('主线程')

2.3 线程join方法

"""
主线程为什么要等着子线程结束才会结束整个进程
    因为主线程结束也就标志着整个进程的结束 要确保子线程运行过程中所需的各项资源
"""


from threading import Thread
import time


def task(name):
    print(f'{name} is running')
    time.sleep(3)
    print(f'{name} is over')


t = Thread(target=task, args=('jason',))
t.start()
t.join()  # 主线程代码等待子线程代码运行完毕之后再往下执行
print('主线程')

2.4 同一个进程内的多个线程数据共享 >>>> 与进程相反

from threading import Thread

money = 10000000000
def task():
    global money
    money = 1

t = Thread(target=task)
t.start()
t.join()
print(money)           # 1

2.5 线程对象属性和方法

1.验证一个进程下的多个线程是否真的处于一个进程
    验证确实如此
2.统计进程下活跃的线程数
    active_count()                          # 注意主线程也算!!!
3.获取线程的名字
    1.current_thread().name
        MainThread                          # 主线程
        Thread-1、Thread-2                  # 子线程
    2.self.name

2.6 守护线程

from threading import Thread
import time


def task(name):
    print(f'{name} is running')
    time.sleep(3)
    print(f'{name} is over')

t1 = Thread(target=task, args=('jason',))
t2 = Thread(target=task, args=('kevin',))

t1.daemon = True                           # 设置 t1 为守护线程

t1.start()
t2.start()
print('主线程')

三. 线程池-进程池

从Python3.2+之后,就成了内置模块。

3.1 概念

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

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

3.2 模块导入

两种池的用法相同, 下面会以进程池为例。

''' 线程池 '''
  from concurrent.futures import ThreadPoolExecutor

''' 进程池 '''
  from concurrent.fututres import ProcessPoolExecutor

3.3 方法及属性

1 submit 方法 >>> 异步提交任务

不难发现submit 方法是异步提交任务

from concurrent.futures import ProcessPoolExecutor
import time

pool1 = ProcessPoolExecutor(5)


def task(n):
    print(n, end=" ")
    time.sleep(1)


if __name__ == '__main__':
    for i in range(20):
        pool1.submit(task, i)

    print('main process')

image

2 result 方法

result 方法 会返回 提交的任务最终返回的结果

from concurrent.futures import ProcessPoolExecutor
import time

pool1 = ProcessPoolExecutor(5)


def task(n):
    print(n,end=' ')
    time.sleep(1)


if __name__ == '__main__':
    for i in range(5):
        res=pool1.submit(task, i)
        print(res.result())
    print('main process')

image

修改提交任务的返回值–验证

from concurrent.futures import ProcessPoolExecutor
import time

pool1 = ProcessPoolExecutor(5)


def task(n):
    print(n,end=' ')
    time.sleep(1)
    return n**2

if __name__ == '__main__':
    for i in range(5):
        res=pool1.submit(task, i)
        print(res.result())
    print('main process')



''' 运行结果 '''

0 0
1 1
2 4
3 9
4 16
main process

进程已结束,退出代码0

3 shutdown 方法

关闭线程池,等待线程池中所有的任务全部运行结束

from concurrent.futures import ProcessPoolExecutor

pool1 = ProcessPoolExecutor()


def task(i):
    print(i)


if __name__ == '__main__':
    l1 = []
    for i in range(10):
        res = pool1.submit(task, i)
        l1.append(res)
    pool1.shutdown()
    for res in l1:
        print('返回值:', res.result())

print('main process')

四. GIL全局解释器锁

In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)

在CPython中,全局解释器锁(GIL)是一个互斥锁,它防止多个原生线程一次执行Python字节码。这个锁是必要的因为CPython的内存管理不是线程安全的。(由于GIL 存在,其他功能已经发展到依赖于它强制执行的保证。)

"""纯理论 不影响编程 只不过面试的时候可能会被问到"""
"""
1.回顾
python解释器的类别有很多
    Cpython Jpython Ppython

GIL只存在于CPython解释器中,不是python的特征
GIL是一把互斥锁用于阻止同一个进程下的多个线程同时执行
原因是因为CPython解释器中的垃圾回收机制不是线程安全的

反向验证GIL的存在 如果不存在会产生垃圾回收机制与正常线程之间数据错乱
GIL是加在CPython解释器上面的互斥锁
同一个进程下的多个线程要想执行必须先抢GIL锁 所以同一个进程下多个线程肯定不能同时运行 即无法利用多核优势


1. 强调:同一个进程下的多个线程不能同时执行即不能利用多核优势
    很多不懂python的程序员会喷python是垃圾 速度太慢 有多核都不能用

反怼:虽然用一个进程下的多个线程不能利用多核优势 但是还可以开设多进程!!!

2. 再次强调:python的多线程就是垃圾!!!

反怼:要结合实际情况
    如果多个任务都是IO密集型的 那么多线程更有优势(消耗的资源更少)
        多道技术:切换+保存状态
    如果多个任务都是计算密集型 那么多线程确实没有优势 但是可以用多进程
        CPU越多越好

以后用python就可以多进程下面开设多线程从而达到效率最大化
"""
1.所有的解释型语言都无法做到同一个进程下多个线程利用多核优势
2.GIL在实际编程中其实不用考虑

ps:明天代码验证多进程和多线程都有用!!!

参考链接:https://blog.csdn.net/weixin_43988680/article/details/124284555

posted @ 2023-05-10 13:48  codegjj  阅读(6)  评论(0编辑  收藏  举报