并发编程总结

本文围绕操作系统中的进程与线程展开。介绍了操作系统的作用、并发并行与同步异步概念。详细阐述进程的状态、创建方式、僵尸与孤儿进程等,还提及线程的概念、开启方式。对比了进程与线程的区别,分析了GIL、死锁等问题,并介绍信号量、Event事件、定时器等相关内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

操作系统的作用

1.隐藏丑陋复杂的硬件接口,提供良好的抽象接口;
2.管理、调度进程,并且将多个进程对硬件的竞争变得有序。

并发并行与同步异步的概念

  • 并发:是系统具有处理多个任务的能力;单核CPU快速切换可以实现并发;

  • 并行:是系统具有同时处理多个任务的能力;单核CPU无法并行;

    并行是并发的一个子集;

  • 同步:当进程执行中到一个IO(等待外部数据)的时候,---------等:同步 例如打电话

  • 异步: 当进程执行中到一个IO(等待外部数据)的时候,--------不等:一直等到数据接收成功后,再回来处理

一、进程

进程的三种状态

运行态
阻塞态
就绪态

进程的概念

  1. 任务管理器里面,我们能够看到进程;
  2. 程序本质是一段代码,运行起来才是一个进程;
  3. 进程本质上是一段程序的运行过程;专业的说进程是一个程序在数据集上的一次动态执行过程;数据集就是程序执行需要调动的资源;
  4. 进程一般由程序,数据集,进程控制块三部分组成;例如一个厨师要做一个蛋糕,食谱就是程序,食材就是数据集,
  5. 之所以有进程,是我们想让多个程序同时跑起来;
  6. 一个单核CPU只会在不同程序间切换,绝对不会并行,并行就是两个程序同时执行,只是切换速度很快,我们感觉到两个程序是同时执行;双核才会并行程序;

创建进程的两种方式

1、创建进程对象
2、创建类继承

from multiprocessing import Process
import time


def f(name):
    time.sleep(1)
    print('hello', name, time.ctime())


if __name__ == '__main__':
    p_list = []
    for i in range(3):
        p = Process(target=f, args=('shi',))
        p_list.append(p)
        p.start()
    for i in p_list:
        i.join()
    print('end')
from multiprocessing import Process
import time

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

    def run(self):
        time.sleep(1)
        print('hello', self.name, time.ctime())


if __name__ == '__main__':
    p_list = []
    for i in range(3):
        p = MyProcess()
        p_list.append(p)
        p.start()
    for i in p_list:
        i.join()
    print('end')

僵尸进程与孤儿进程

僵尸进程:子进程已经执行完,但是父进程还没有执行完,子进程不会消失,此状态为僵尸进程。
所有的进程都会经历僵尸进程的阶段。
孤儿进程:子进程还没有执行完,父进程就死掉了,此时子进程的父进程就是操作系统的一个公共父进程。
类似于操作系统有一个孤儿院,来作为父进程死掉的子进程的父进程。

join方法在进程中的应用

子进程调用join()方法,在程序这一行,主进程暂停,等待这个子进程执行完,再继续执行主进程。

from multiprocessing import Process
import time

def f(name):
    time.sleep(1)
    print('hello', name, time.ctime())


if __name__ == '__main__':
    p = Process(target=f, args=('shi',))
    p.start()

    p.join()
    print('end')

Process对象的其它方法和属性

构造方法:
1、target 要执行的方法
2、name 进程名
3、args/kwargs 要传入方法的参数

实例方法:
is_alive() 返回进程是否正在运行
join([timeout]) 阻塞当前上下文环境的进程,知道调用此方法的进程终止或达到指定的timeout(可选参数)。
start() 向操作系统发送一个开启进程的信号;进程准备就绪,等待CPU调度
run() start调用run方法
terminate() 不管任务是否完成,立即停止工作进程

属性:
daemon 守护进程, 和线程setDaemon功能一样
name 进程名字
pid 进程号
ppid 父进程号

def info(title):
    print('title:', title)
    print('parent process id:', os.getppid())
    print('process id:', os.getpid())


if __name__ == '__main__':
    info('shi')
    p = Process(target=info, args=('qian',))
    p.start()
    p.join()
    print('end')

守护进程

如果一个子进程要随着子进程的结束而结束,那么就设置这个子进程为守护进程;
也就是说当主进程结束之后,子进程没有存在的意义了,那我们就设置这个子进程为守护进程。

from multiprocessing import Process
import time


def f(name):
    time.sleep(1)
    print('hello', name, time.ctime())


if __name__ == '__main__':
    p = Process(target=f, args=('shi',))
    p.daemon = True  # 设置守护进程必须在start之前
    p.start()
      
  	print('end')

互斥锁(进程同步)

进程里的互斥锁,也叫 进程同步,也叫 同步锁

from multiprocessing import Process, Lock
import time


def f(i, lock):
    lock.acquire()
    print(i)
    time.sleep(1)
    print(i, '==')
    lock.release()


if __name__ == '__main__':
    lock = Lock()   # 创建一把锁
    for i in range(10):
        p = Process(target=f, args=(i,lock))   # 保证所有的子进程用的是同一把锁,就把锁作为参数传进去
        p.start()

多进程模拟抢票

from multiprocessing import Process, Lock
import json

# with open('db', 'w', encoding='utf-8') as f:
#     json.dump({'count': 1}, f)

def search(name):
    dic = json.load(open('db', 'r', encoding='utf-8'))
    print('<%s>剩余票数:%s' % (name, dic['count']))


def get(name):
    dic = json.load(open('db', 'r', encoding='utf-8'))
    if dic['count'] > 0:
        dic['count'] -= 1
        json.dump(dic, open('db', 'w', encoding='utf-8'))
        print('<%s>买票成功' % name)


def task(name, lock):
    search(name)
    lock.acquire()
    get(name)
    lock.release()


if __name__ == '__main__':
    lock = Lock()
    for i in range(10):
        p = Process(target=task, args=(i, lock,))
        p.start()

join和互斥锁的区别

join和互斥锁都是将并发变成串行,保证数据的安全性
join():(在start之后紧跟join) 将子进程所有代码变成串行,跟不开启进程一样。
互斥锁:将共享数据的一部分代码变成串行。

进程中的队列

每一个子进程的内存空间是独立的,但是可以共享硬盘上的数据。
上面模拟抢票中,每一个子进程都要去硬盘读取数据,效率低,而且需要自己枷锁处理;
那么multiprocessing模块提供了队列,在内存中共享数据,而且自动枷锁。

先来看看什么是队列

from multiprocessing import Queue

q = Queue(3)

q.put('s')
q.put(1)
q.put([1, 2])
print(q.full())

q.get()
q.get()
q.get()
print(q.empty())

多进程的生产者消费者模型

from multiprocessing import Queue, Process
import time


def produce(name, q):
    for i in range(10):
        print('厨师<%s>生产了%s个包子' % (name, name+str(i)))
        q.put(name+str(i))
        time.sleep(0.5)


def consumers(name, q):
    while True:
        time.sleep(1)
        res = q.get()
        if res == None:break
        print('顾客<%s>吃了%s包子' % (name, res))


if __name__ == '__main__':
    q = Queue()
    p1 = Process(target=produce, args=('厨师A', q,))
    p2 = Process(target=produce, args=('厨师B', q,))
    p3 = Process(target=produce, args=('厨师C', q,))

    c1 = Process(target=consumers, args=('顾客A', q,))
    c2 = Process(target=consumers, args=('顾客B', q,))

    p1.start()
    p2.start()
    p3.start()
    c1.start()
    c2.start()

    p1.join()
    p2.join()
    p3.join()
    q.put(None)  # 向消费者发一个信号,说包子做完了,有几个消费者就发几个信号
    q.put(None)
    print('主')

二、线程

线程的概念

1、线程是最小的执行单元(实例),可以理解为进程内再开进程,即微进程;
2、一个程序肯定至少有一个进程,一个进程至少有一个线程,即主线程;
3、进程在执行过程中拥有独立的内存单元,多个线程共享这片内存资源,线程不可能在进程外;
4、一个线程可以创建和撤销另一个线程,同一个进程中的线程可以并发执行;

比如开发一个文本编辑器,至少有三个功能,接收、显示和保存,那么这三个功能很明显要共享用户输入的数据,而且要并发执行,
所以就必须用到多线程。

开启线程的两种方式

1、创建对象
2、重写类

import threading,time

def music():
    print('begin to listen %s'%time.ctime())
    time.sleep(3)
    print('stop to listen %s'%time.ctime())

def game():
    print('begin to game %s'%time.ctime())
    time.sleep(5)
    print('stop to game %s'%time.ctime())

if __name__ == '__main__':

    t1= threading.Thread(target=music)   #创建一个线程对象
    t1.start()

    t2= threading.Thread(target=game)   #创建一个线程对象
    t2.start()
    print('ending-----------')
from threading import Thread
import time


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

    def run(self):
        time.sleep(1)
        print('hello', self.name, time.ctime())


if __name__ == '__main__':
    t_list = []
    for i in range(3):
        t = MyThread()
        t_list.append(t)
        t.start()
    print('end')

进程与线程的区别

1、开进程的开销远大于开线程
2、统一进程内的线程共享该进程的地址空间
3、瞅一眼pid

from threading import Thread
import os

def music():
    print('begin to listen %s'%time.ctime())
    time.sleep(3)
    print('stop to listen %s'%time.ctime())
    print(os.getpid())


def game():
    print('begin to game %s'%time.ctime())
    time.sleep(5)
    print('stop to game %s'%time.ctime())
    print(os.getpid())


if __name__ == '__main__':

    t1= Thread(target=music)   #创建一个线程对象
    t1.start()

    t2= Thread(target=game)   #创建一个线程对象
    t2.start()
    print('ending-----------', os.getpid())

join在线程中的应用

join的线程不执行完,主线程暂停住,等join线程执行完,主线程在执行。不影响其他子线程的执行

import threading,time

def music():
    print('begin to listen %s'%time.ctime())
    time.sleep(3)
    print('stop to listen %s'%time.ctime())

def game():
    print('begin to game %s'%time.ctime())
    time.sleep(5)
    print('stop to game %s'%time.ctime())

if __name__ == '__main__':

    t1= threading.Thread(target=music)   #创建一个线程对象
    t2= threading.Thread(target=game)   #创建一个线程对象

    t1.start()
    t2.start()

    t1.join()     #t1和t2不执行完,主线程不执行
    t2.join()

    print('ending----------')

此段代码执行后,等t1,t2执行完,主线程才执行。

Thread对象的其他属性或方法

Thread实例对象的方法
  isAlive(): 返回线程是否活动的。
  getName(): 返回线程名。
  setName(): 设置线程名。

threading模块提供的一些方法:
  threading.currentThread(): 返回当前的线程变量。
  threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
  threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。

守护线程

在start之前设置
子线程是否要守护主线程(主线程结束后,子线程立马结束,不在继续执行)
t1.setDaemon(True)
t1.daemon = True

from threading import Thread
import time


def music():
    print('begin to listen %s'%time.ctime())
    time.sleep(3)
    print('stop to listen %s'%time.ctime())


def game():
    print('begin to game %s'%time.ctime())
    time.sleep(5)
    print('stop to game %s'%time.ctime())


if __name__ == '__main__':

    t1 = Thread(target=music)   # 创建一个线程对象
    t1.daemon = True
    t1.start()

    t2 = Thread(target=game)   # 创建一个线程对象
    # t2.daemon = True
    t2.start()
    print('ending-----------')

互斥锁(线程同步)

from threading import Thread, Lock
import time

num = 100
def sub():
    global num

    lock.acquire()
    num1 = num
    time.sleep(0.01)
    num = num1-1
    lock.release()

if __name__ == '__main__':

	list1=list()
	lock=Lock()      #创建锁对象
	
	for i in range(100):
	    t=Thread(target=sub)
	    t.start()
	    list1.append(t)
	 
	for t in list1:
	    t.join()
	
	print(num)

GIL基本概念

全局解释锁
在一个进程内,无论你启用多少个线程,你有多少CPU,python在执行的时候会淡定的在同一时刻只允许一个线程执行

因为有GIL,所以同一时刻只能有单核CPU执行

解决方式: 多进程+协成

任务:
IO密集型 遇到IO,线程切换,会充分利用CPU (sleep等同于IO操作)
计算密集型 一直在使用CPU

对于IO密集型的任务:python的多线程是有意义的,,,可以采用多进程+协成
对于计算密集型的任务:python的多线程就不推荐了,可以用多进程,或者用别的语言

IO密集型任务多进程与多线程比较

from multiprocessing import Process
from threading import Thread
import threading
import os,time


def work():
    time.sleep(2)


if __name__ == '__main__':
    l = []
    # print(os.cpu_count())  # 本机为4核
    start = time.time()
    for i in range(400):
        p = Process(target=work)   # 耗时2.94s多,大部分时间耗费在创建进程上
        # p = Thread(target=work)  # 耗时2.032s多
        l.append(p)
        p.start()
    for p in l:
        p.join()
    stop = time.time()
    print('run time is %s' % (stop-start))

计算密集型任务多进程与多线程比较

from multiprocessing import Process
from threading import Thread
import os,time


def work():
    res = 0
    for i in range(100000000):
        res *= i


if __name__ == '__main__':
    l = []
    # print(os.cpu_count())   # 本机为4核
    start = time.time()
    for i in range(4):
        p = Process(target=work)   # 耗时4.35s多
        # p = Thread(target=work)   #  耗时15.45s多
        l.append(p)
        p.start()
    for p in l:
        p.join()
    stop = time.time()
    print('run time is %s' % (stop-start))

死锁与递归锁

所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程,如下就是死锁

from threading import Thread,Lock
import time
mutexA=Lock()
mutexB=Lock()

class MyThread(Thread):
    def run(self):
        self.func1()
        self.func2()
    def func1(self):
        mutexA.acquire()
        print('\033[41m%s 拿到A锁\033[0m' %self.name)

        mutexB.acquire()
        print('\033[42m%s 拿到B锁\033[0m' %self.name)
        mutexB.release()

        mutexA.release()

    def func2(self):
        mutexB.acquire()
        print('\033[43m%s 拿到B锁\033[0m' %self.name)
        time.sleep(2)

        mutexA.acquire()
        print('\033[44m%s 拿到A锁\033[0m' %self.name)
        mutexA.release()

        mutexB.release()

if __name__ == '__main__':
    for i in range(10):
        t=MyThread()
        t.start()

解决方法,递归锁,在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。

这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁,二者的区别是:递归锁可以连续acquire多次,而互斥锁只能acquire一次

from threading import Thread,RLock
import time

mutexA=mutexB=RLock() #一个线程拿到锁,counter加1,该线程内又碰到加锁的情况,则counter继续加1,这期间所有其他线程都只能等待,等待该线程释放所有锁,即counter递减到0为止

class MyThread(Thread):
    def run(self):
        self.func1()
        self.func2()
    def func1(self):
        mutexA.acquire()
        print('\033[41m%s 拿到A锁\033[0m' %self.name)

        mutexB.acquire()
        print('\033[42m%s 拿到B锁\033[0m' %self.name)
        mutexB.release()

        mutexA.release()

    def func2(self):
        mutexB.acquire()
        print('\033[43m%s 拿到B锁\033[0m' %self.name)
        time.sleep(2)

        mutexA.acquire()
        print('\033[44m%s 拿到A锁\033[0m' %self.name)
        mutexA.release()

        mutexB.release()

if __name__ == '__main__':
    for i in range(10):
        t=MyThread()
        t.start()

信号量

信号量也是一把锁,可以指定信号量为3,对比互斥锁同一时间只能有一个任务抢到锁去执行,信号量同一时间可以有3个任务拿到锁去执行,如果说互斥锁是合租房屋的人去抢一个厕所,那么信号量就相当于一群路人争抢公共厕所,公共厕所有多个坑位,这意味着同一时间可以有多个人上公共厕所,但公共厕所容纳的人数是一定的,这便是信号量的大小

from threading import Thread, Semaphore, current_thread
import time, random

sm = Semaphore(3)  # 厕所有三个坑


def task():
    with sm:
        print('%s in 上厕所' % current_thread().getName())
        time.sleep(random.randint(1, 3))


if __name__ == '__main__':
    for i in range(10):
        t = Thread(target=task)
        t.start()

Event事件

线程的一个关键特性是每个线程都是独立运行且状态不可预测。如果程序中的其 他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就会变得非常棘手。为了解决这些问题,我们需要使用threading库中的Event对象。 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在 初始情况下,Event对象中的信号标志被设置为假。如果有线程等待一个Event对象, 而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行

1、事件被创建的时候,为FALSE状态 wait() 阻塞状态
wait(1) #状态为FALSE的时候,只等待1s
2、set() # 设置状态为True
3、clear() # 设置状态为FALSE

应用:链接数据库,检测数据库的可链接情况

from threading import Thread, Event, current_thread
import time


def conn():
    count = 0   # 尝试三次链接
    while not event.is_set():   # 数据库服务端没有开启
        if count == 3:
            print('%s您已经尝试太多次数了' % current_thread().getName())
            return     # 三次之后结束
        print('%s正在尝试第%s次链接' % (current_thread().getName(), count))
        event.wait(0.5)     # 等0.5秒就往下执行
        count += 1
    print('%s链接上了' % current_thread().getName())


def check():
    time.sleep(3)   # 检测数据库服务端是否开启
    event.set()    # 开启之后发信号


if __name__ == '__main__':
    event = Event()
    co1 = Thread(target=conn)
    co2 = Thread(target=conn)
    ch = Thread(target=check)
    co1.start()
    co2.start()
    ch.start()

定时器

定时器,指定n秒后执行某操作

from threading import Timer

def hello():
    print("hello, world")

t = Timer(1, hello)
t.start()  # after 1 seconds, "hello, world" will be printed

定时器是异步非阻塞的

from threading import Thread, Condition, Timer
import time


def func():
    print('时间同步')


while True:
    Timer(2, func).start()   # 异步非阻塞
    time.sleep(2)

应用:验证码定时刷新

from threading import Timer
import random


class Mycode:

    def __init__(self):
        self.timer(10)     # 直接执行
        self.cache = None     # 验证码缓存
        self.t = None     # 定时生成新的验证码

    @staticmethod
    def auth_code(n=4):    # 生成一个4为的验证码
        res = ''
        for i in range(n):
            s1 = str(random.randint(1, 9))
            s2 = chr(random.randint(65, 90))
            s = random.choice([s1, s2])
            res += s
        return res

    def timer(self, interval=6):    # 间隔6秒换一个新的
        self.cache = self.auth_code()
        print(self.cache)
        self.t = Timer(interval, self.timer)     # 6秒后,自动再次执行这个函数
        self.t.start()

    def main(self):
        while True:
            code = input('请输入验证码:')
            if code.upper() == self.cache:
                print('验证码正确')
                self.t.cancel()
                break


if __name__ == '__main__':

    obj = Mycode()
    obj.main()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值