并发编程之多线程二

1>GIL全局解释器锁

    任何Python线程执行前,必须先获得GIL(全局解释器锁),然后,每执行100条字节码,解释器就
    自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,

    所以,多线程在Python中只能交替执行,计算10个线程跑在10核CPU上,也只能用到1个核。

    为何会这样呢?

    在一个python的进程内,不仅有test.py的主线程或者由该主线程开启的其他线程,还有解释器开启
    的垃圾回收等解释器级别的线程,总之,所有线程都运行在这一个进程内,所以,所有数据都是共享的,
    解释器的代码是所有线程共享的,所以垃圾回收线程也可能访问到解释器的代码而去执行,这就导致了
    一个问题:对于同一个数据或代码,可能会出现多个线程同时修改或者执行的情况,要解决这种问题,就

    只能加锁处理,保证python解释器同一时间只能执行一个任务的代码,所以就有了GIL锁。

    GIL本质就是一把互斥锁,既然是互斥锁,所有互斥锁的本质都一样,都是将并发运行变成串行,
    以此来控制同一时间内共享数据只能被一个任务所修改,进而保证数据安全。

2>线程互斥锁

    线程的锁,首先确定一点,保护不同数据需要使用不同的锁,而GIL锁是保护线程任务的安全性,
    即所有的线程要想运行自己的任务,首先需获取到能够访问到解释器的代码的锁,
    但是对线程自己数据(程序中内存的,文件的等)的修改,就只能由线程自己加互斥锁解决了,
    GIL只是基于解释器保护线程安全的,其他自定义互斥锁是保护数据安全的。如下例子:

    

    加入互斥锁,如下

    

3>多进程和多线程的应用场景

    3.1>如果并发任务是计算密集型,则多进程效率高

    

    3.2>如果并发任务是I/O密集型,则多线程效率高

    

4>死锁现象与递归锁

    4.1>死锁现象

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

    这些永远在互相等待的进程称为死锁进程,如下就是死锁。

    

    分析:并发了十个线程,因线程开启速度极快,几乎是在发送start()指令之后就开了
    所以线程1先拿到了A锁,紧接又‘轻松的’拿到了B锁(此时就算有其他线程起来了,
    也要先去抢A锁,因为先执行func1,但是A锁被线程1拿走了还没释放,所以,其他
    就算已经起来了的线程也只能处于等待A锁释放的状态,所以线程1轻松拿B锁)
    紧接着两步释放,现在A,B锁都释放了,但是其他进程也已经起来并且在等待A锁,
    所以进程2抢到了A锁,紧接着准备抢B锁,但是,在进程2抢A锁的时候,进程1继续
    执行func2,又再一起‘轻松’拿到B锁(原理同上,其他进程都在抢A锁),紧接着
    准备抢A锁,所以出现了----进程2拿A锁,准备抢B锁,而进程1拿B锁,准备抢A锁

    的这么一个状态---------死锁现象。(相互不释放,相互都在等待)

    解决之法,就是用递归锁。

    4.2>递归锁 RLock()

    这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源
    可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源,也就是
    说:只有counter为0,才能被其他线程获取加锁。
    二者的区别是:递归锁可以连续acquire多次,而互斥锁只能acquire一次

    

    分析:并发是个线程,线程1先拿到了A锁,此时counter+1,不为0了,则其他线程就算起来了也不能抢锁。
    线程A继续拿到了B锁,紧接着释放两锁(counter相应-1),counter为0,那么所有其他已起来的
    线程均开始抢锁,只要有一个线程抢到了A锁,或者线程1继续抢到了B锁,counter又再+1,未抢到的
    就又只能等到counter重新为0的时候再抢,执行结果是线程3最后抢到锁,func2函数的第一次休眠(1s)
    其他这时就算有100个线程也已经全部起来了,都‘蓄势待发’的准备等到counter=0时抢锁,那真是
    “各凭本事"了,所以并不一定最后抢到锁的就是线程10,乱序是完全正常的。

5>信号量、Event、定时器

    5.1>信号量也是一把锁,但可以指定信号量;

    对比互斥锁同一时间只能有一个任务抢到锁去执行;指定信号量为5,则同一时间可以有5个任务拿到锁去执行。

    

    备注:Semaphore内部也有一个计数器,每当调用acquire()时内置计数器-1;调用release() 时内置计数器+1;
    计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。
    什么时候为0,你设置的允许线程数减完的时候。

    5.2>Event

    线程的一个关键特性是每个线程都是独立运行且状态不可预测。如果程序中的其他线程需要通过判断某个线程
    的状态来确定自己下一步的操作,就需要引入threading库中的Event对象来解决。 
    对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在初始情况下,Event对象中的
    信号标志被设置为False。如果线程的信号标志为False,那么这个线程将会被一直阻塞直至该标志为True。

    如果一个Event对象的信号标志设置为True,它将唤醒所有等待这个Event对象的线程,继续执行。

    event.isSet():返回event的状态值;
    event.wait():如果 event.isSet()==False将阻塞线程;
    event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;

    event.clear():恢复event的状态值为False。

        5.2.1>模拟老师发送下课指令,学生由上课切换到下课的例子

    

    5.2.2>模拟客户端要连接server,先检测server端是否起来,并尝试多次连接的例子

    

    5.3>定时器Timer:指定多少时间之后再执行任务

        5.3.1>Timer基本用法

    

        5.3.2>Timer实际场景,验证码失效场景(规定时间内输入正确验证码,否则失效刷新验证码)

    

    思路:首先创建generate_code函数,用于生成验证码,
        再创建make_cache函数,函数首先调一次generate_code函数,把结果放入cache,
        引入定时器功能,设定规定时间后再运行一次,
        把make_cache函数放入初始化函数下,实例化就自动执行,
        编写input_code函数,若输入正确,则取消定时任务。

6>线程Queue

    线程Queue的三种用法,队列的put和get方法都默认带了 block=True参数,当发送存取阻塞时便阻塞程序,

    若设置block=False,则在queue已经full的情况下put 和在queue已经empty的情况下get 时,程序都会直接报错

    

    

    6.1>队列,Queue:先进先出

    

    6.2>堆栈,LifoQueue:后进先出

    

    6.3>优先级队列,PriorityQueue:优先级高的先出队列

    put一个元组,元组的第一个元素是优先级(通常是数字,也可以是非数字之间的比较),数字越小优先级越高

    

    

猜你喜欢

转载自blog.csdn.net/huangql517/article/details/80103669