并发编程小结

Posted setcreed

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了并发编程小结相关的知识,希望对你有一定的参考价值。

多道技术

单道:一台哦到

多道:

  • 时间上复用, 遇到IO操作就会切换,程序占用CPU时间过长就会切换
  • 空间上复用, 支持多个程序

并发与并行

并发:看起来像是同时运行

并行:真正意义上的同时运行

并行与并发的区别:

并行是从微观上,也就是一个精确的时间片刻,有不同的程序在执行

并发是从宏观上,在一个时间段上可以看出是同时执行的

进程

进程是资源单位,每创建一个进程都会生成一个名称空间,占用内存资源

程序与进程

程序就是一堆代码

进程:狭义上来讲一堆代码运行的过程;广义上讲,进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动,是操作系统动态执行的基本单元

进程调度

  1. 先来先服务调度算法
  2. 短作业优先调度算法
  3. 时间片轮转法
  4. 多级反馈队列

进程的状态

  • 就绪
  • 运行
  • 阻塞

同步异步阻塞非阻塞

同步:执行一个操作之后,等待通知,然后才执行接下来的操作

异步:执行一个操作之后,可以去执行其他的操作,然后等待通知 回来执行未完成的操作。

阻塞:进程给cpu传达一个任务之后,一直等待cpu处理完成,然后才执行接下来的操作

非阻塞:进程给cpu传达任务后,继续处理后续的操作,隔段时间再来询问之前的操作是否完成。

阻塞与非阻塞是指进程访问的数据,如果尚未就绪,进程是否需要等待,指的是一种状态。

同步与异步是指访问数据的机制,同步一般指主动请求并等待I/O操作完毕的方式,当数据就绪后,在读写的时候必须阻塞;异步则指主动请求数据后,便可以继续处理其它任务,随后等待I/O操作完毕的通知,这可以使进程在数据读写时也不阻塞。

创建进程的两种方式

使用multiprocessing模块

# 方法一

from multiprocessing import Process
import time

def task():
    print('子进程开始。。。')
    time.sleep(2)
    print('子进程结束。。。')


if __name__ == '__main__':
    p = Process(target=task)
    p.start()
    p.join()   # 等待所有子进程结束后,主进程才结束
    print('主进程') 
    
    
# 方法二:
class MyProcess(Process):
    def run(self) -> None:
        print('子进程开始。。。')
        time.sleep(2)
        print('子进程结束。。。')


if __name__ == '__main__':
    p = MyProcess()
    p.start()
    p.join()
    print('主进程')

回收进程资源的两种方式

  • 调用join,等待子进程结束后主进程结束
  • 主进程正常结束

僵尸进程、孤儿进程、守护进程

僵尸进程:子进程结束,PID号还在,主进程没有回收子进程资源

孤儿进程:子进程没有结束,但是主进程意外死亡,操作系统优化机制会将子进程结束之后回收资源。

守护进程:

  • 守护进程会在主进程代码结束之后就终止。
  • 守护进程内无法再开启子进程

进程互斥锁

from multiprocessing import Lock

进程间数据不共享,但共享同一套文件系统,多个进程访问同一个文件会造成混乱、数据不安全,必须加锁。

总结:加锁可以保证多个进程修改同一块数据时,同一时间只能有一个任务可以进行修改,即串行的修改,这样虽然牺牲了效率,但保证了数据的安全

from multiprocessing import Lock
mutex = Lock()

mutex.acquire()# 加锁
# 修改数据的操作
mutex.release()# 释放锁

进程间通信

进程间数据是隔离的,需要通过队列的方式进行通信

队列

FIFO队列:先进先出

from multiprocessing import Queue
q = Queue(5)

q.put()  #添加数据,若队列满了,则等待
q.put_nowait() # 添加数据,若队列添加数据满了,就会直接报错

q.get() # 若队列中没有数据,就会卡住等待
q.put_nowait() # 若队列中没有数据,会直接报错

堆栈

LIFO:后进先出

生产者与消费者模型

生产者:生产数据

消费者:使用数据

通过队列实现,生产者将数据扔进队列中,消费者从队列中获取数据,从而平衡了生产者与消费者的处理能力

线程

进程是资源单位,而线程是执行单位。

创建进程会自带一个线程,一个进程下可以创建多个线程

使用线程可以节省资源的开销

进程与线程的优缺点

进程的优点:

  • 多核下可以并行
  • 计算密集型下提高效率

进程的缺点:

  • 开销资源远高于线程

线程的优点:

  • 占用资源远比进程小
  • IO密集型下能提高效率

线程的缺点:

  • 无法利用多核优势

线程间数据是共享的

GIL全局解释器锁

  • 只有cpython才自带有一个GIL全局解释器锁
  • GIL本质上是一个互斥锁
  • GIL是为了阻止同一个进程内多个线程同时运行。单个进程下的多线程只能实现并发
  • GIL锁主要是因为cpython的内存管理不是“线程安全”的,多个线程执行,遇到IO操作,会立马释放GIL锁,交给下一个先进来的线程

总结:GIL锁主要是为了保证线程安全的,保证数据安全

死锁与递归锁

死锁

指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种相互等待的现象,若无外力作用,它们都无法推进下去,这就是死锁现象。

from threading import Lock
mutexA=Lock()
mutexA.acquire()
mutexA.acquire()
print(123)
mutexA.release()
mutexA.release()

递归锁

解决死锁现象

mutex = Lock()
mutex1, mutex2 = Rlock()  # 可以引用多次
只要这把锁计数为0,就释放该锁,让下一个人使用

信号量

from threading import Semaphore

信号量也是一把锁,可以让多个任务一起使用

Event事件

可以控制线程的执行,让一些线程控制另一些线程的执行

e = Event()

- 线程1
e.set()  # 给线程2发送信号,让他执行

- 线程2
e.wait()  # 等待线程1的信号

线程队列

线程间数据不安全的情况下使用线程队列,保证线程间数据安全

import queue
FIFO: 先进先出队列   queue.Queue()

LIFO: 后进先出队列   queue.LifoQueue()

优先级队列:根据数据大小判断出队列优先级;进队列数据是无序的
queue.PriorityQueue()

进程池与线程池

为了控制进程/线程创建的数量,保证了硬件能正常运行

from concurrent.futures import ProcessPoolExecutor
from concurrent.futures import ThreadPoolExecutor

poo1 = ProcessPoolExecutor()  # 括号里不加数字表示默认
poo2 = ThreadPoolExecutor(100)

# 将函数地址的执行结果,给回调函数
pool2.submit(任务函数地址,参数).add_done_callback(回调函数地址)

回调函数(必须接收一个参数res)
res2 = res.result()  # 获取值

协程

单线程下实现并发,不是任何单位

单线程下实现并发,好处是节省资源

  • 在IO密集型的情况下,协程有优势
  • 在计算密集型的情况下,进程you优势

手动创建协程:

  • 手动实现切换 + 保存状态
    • yield 可以保存状态
    • 通过调用next,可以不停的切换
  • yield不能监听IO操作

gevent

第三方模块,用gevent可以实现监听IO操作

from gevent import monkey
monkey.path_all()  # 监听所有IO
from gevent import spawn, joinall  # spawn实现切换 + 保存状态

s1 = spawn(任务1)
s2 = spawn(任务2)
joinall([s1, s2])

IO模型

  • 阻塞IO
  • 非阻塞IO
  • 多路复用IO
  • 异步IO

以上是关于并发编程小结的主要内容,如果未能解决你的问题,请参考以下文章

并发编程学习小结

并发编程学习小结

并发编程学习小结

java并发编程小结

《java并发编程的艺术》学习小结

《java并发编程的艺术》学习小结