Python——线程

Python——线程

参考博文:https://www.cnblogs.com/yuanchenqi/articles/6248025.html

一、线程的调用

调用线程需要导入 threading 模块

在以往,运行程序是从上到下顺序运行。现在如果想要两个函数同时运行,那么这里就要需要用到线程模块。
在这里插入图片描述
看时间能够得知,是同时出来的。

二、线程相关知识

1、join方法——子线程结束之后父线程才能结束

在这里插入图片描述
上图中,加了个print(),我们通过多次运行可以发现发生如上图所示情况,print 是主线程,t1是线程1,t2是线程二。虽然三个线程是同时出来,但是有时出来的顺序不一。

这时我们来看一下 join 方法:
在这里插入图片描述
加入了 join 之后,其意思是:t2 线程完毕之后,其他线程才能继续执行。所以此时无论怎么运行,主线程都需要等待 t2 运行完毕之后才能结束。由于 t1 早于 t2 ,所以 t1 不受影响。

2、setDaemon()——守护线程

在这里插入图片描述
通常情况下,主线程需要等待子线程运行完成后才能结束。但是在设置了守护线程后,主线程不必在等子线程,主线程完成后,不管子线程是否完成,都结束。

注意:必须在 start() 方法前设置

在这里插入图片描述

3.继承式调用

4、其他方法

在这里插入图片描述

5、GIL

5(1)、初识GIL

GIL也被称为:全局解释锁。其作用是:让Python没有多线程效用。

那么问题来了,为什么上面似乎实现了多线程的效果呢?

5(2)、并发并行与异步同步

这里先要学习并发与并行两种概念:

并发:是指系统具有处理多个任务(动作)的能力
并行:是指系统具有同时处理多个任务(动作)的能力

并行是并发的子集。

同步:当进程执行到一个IO(等待外部数据)的时候,如果进程等待,就是同步。
异步:当进程执行到一个IO(等待外部数据)的时候,如果进程不等,去干其他事情,直到数据接收成功,再回来处理,这种就是异步。

举个例子:做家务的时候,等到洗衣机洗好衣服,晾完衣服,再洗碗,就是同步。如果洗衣机在洗衣服的时候,去洗碗;直到洗衣机洗好衣服,才去晾衣服,这种就是异步。

5(3)、GIL的解释

在这里插入图片描述

5(4)、GIL的问题

那么问题来了,既然Python不支持多线程并发效果,为何上面的的程序又能有线程的效果呢?

其实上面的程序是通过了异步的效果才有多线程的感觉。当一个线程处于等待的时候,另外一个线程才有机会去工作。

如果是另外一种情况:
在这里插入图片描述
如果是使用上面这种情况,就会发现,此时的多线程失效了。甚至不使用多线程这个模块,效率可能还低点。

其原因在于:
任务与任务之间是不同的,分两种情况:
IO密集型-----------Pyhton的多线程是有意义的。(特别提醒:sleep() 这种,也属于IO密集型)。效率更高的可以采用多进程+协程(可以勉强用于计算密集型,但更多的用于IO密集型)

计算密集型--------Pyhton的的多线程就不推荐,此时Python就不适用。建议更换适合的语言处理这种情况。(比如 GO 语言)

就是因为IO密集型的任务,在一个任务停止运行的时候,会切换到另外一个任务继续工作,所以才有线程的效果。
到了计算密集型,由于计算需要消耗几乎全部的资源,所以任务跑完了,cpu才会空闲,才能处理另外一个任务。此时多线程模块就没用了

6、同步锁

先来设置一个程序,100,每次递减1。通过调用一百条线程来执行
相关知识自己百度去
结果理应是0,但是可以看到结果明显是错误的。多执行几次还能发现结果还会变动。
问题产生的原因就是没有控制多个线程对同一资源的访问,对数据造成破坏,使得线程运行的结果不可预期。这种现象称为“线程不安全”。

那么如何解决这个问题呢? 可以使用一个叫“同步锁”的方法。
在这里插入图片描述
那么上锁到解锁之间的这段代码,就只是单一的串行,不会并行。同步锁能使某个区域的代码只使用一个线程来执行;但是这段代码之外的其他地方,还是能够并发的。

那么这把锁上锁的时候要十分谨慎,哪里上锁哪里解锁都要仔细思考后才能使用。

7、递归锁

import threading
import time

class MyThread(threading.Thread):
    def A(self):
        a.acquire()
        print(self.name,'取得A锁')
        time.sleep(2)

        b.acquire()
        print(self.name, '取得B锁')
        time.sleep(1)

        b.release()
        a.release()

    def B(self):
        b.acquire()
        print(self.name, '取得B锁')
        time.sleep(2)

        a.acquire()
        print(self.name, '取得A锁')
        time.sleep(1)

        a.release()
        b.release()

    def run(self):
        self.A()
        self.B()

if __name__ == '__main__':
    a = threading.Lock()
    b = threading.Lock()
    l = []

    for i in range(5):
        t = MyThread()
        t.start()
        l.append(t)

        for i in l:
            i.join()

        print("结束")

代码讲解:上述代码中,关键在于A跟B部分;A中:先是锁a,锁b,再释放b,然后释放a。B中:先锁b,然后锁a,再释放b,然后释放a。

那么此时有5个线程来跑,线程1先完整的走完了A,接着开始走B的时候,会取得b锁。接着会有线程2来走A(几乎与线程1是同时取得锁,睡眠一两秒就是保证都拿到锁),取得了a锁。这时关键来了,你取得了b锁,我拿到a锁,大家都在等对面执行完然后拿锁,于是大家就这么死等着,程序也因此一直卡在这里。这就是死锁(也叫递归锁)

那么怎么解决呢?

threading.RLock()
在这里插入图片描述
使用 RLock() 方法。那么就不会产生死锁了。

可以看到,一开始锁还分a、b,现在直接是一把锁。此方法内置了一个计数器,当遇到上锁的时候,这把锁的计数器就加1,遇到第二把锁的时候,还是用同一把锁来锁,计数器变为2。在计数器变为0之前,其他线程都不能触碰这把锁,于是此问题就解决了。

8、event——同步对象

线程的一个关键特性是每个线程都是独立运行且状态不可预测。如果有个线程,需要判断其他线程再来确定自己下一步的操作,这时可以用 threading 模块下的 event 方法。

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

在这里插入图片描述执行顺序:先是老板打印加班,接着event设置为true,然后来到打工人函数,因为一开始被wait阻塞了,直到event设置为true之后,才开始继续往下走,发出感叹命苦,接着event值被清空,回归为false,继续等待,老板函数说可以下班后,event设置为true,最后打工人函数才能通过,发出oh year,程序运行完毕。

9、semaphore——信号量

每当调用acquire()时,内置计数器 -1
每当调用release()时,内置计数器 +1
计数器不能小于0,当计数器为0时,acquire()将阻塞线程直到其他线程调用release()(类似于停车位的概念)。
在这里插入图片描述

10、queue——队列(多线程利器)

先看下面这种情况:
在这里插入图片描述列表里有5个数,用两个线程从后往前取,每取出一个就删除一个。但是可以看到,一开始两个线程都取了5,因此就报错了;当然,避免报错可以采用锁的方法。但是如果不想用锁的方法,那么我们可以使用 队列。

10(a)、队列——先进先出模式(FIFO)

队列里可以放很多数据类型
先来看下下图的结果:
在这里插入图片描述
上面结果中,可以看到似乎执行完了,但是程序却还没结束。是因为队列的使用是面向多线程的,此时程序没有结束,是为了留给其他线程取出的机会。
注意,此时 q 队列里面没有填写数字,意为没有限制容量

此时我们看下如果限制容量,并且放置了超出容量的数目,结果会是怎样?
在这里插入图片描述
此时程序连打印都没有打印出来。重点在于卡在了 put 的这一步,这时容量已经满了,不让你塞了,队列此时在等待其他线程取出里面的东西,让多出来的 put 可以塞进去;所以此时程序是卡在了这个地方。

注意:线程是可以与线程共享一个进程里的所有数据。所以此时所有线程都可以操作这个队列。

如果队列想要给多出来的数据报错,那么可以如下操作:
在这里插入图片描述在 put 里面加一个 False,该数据属于多出来的数据时,就会报错。

同样的,get方法里面也有:
在这里插入图片描述
如果已经为空,还取数据,就会报错。

10(b)、队列——后进先出(也是先进后出,LIFO,其实就是堆栈)

此时的调用方式就变了:
在这里插入图片描述

10(c)、队列——优先级(可以通过使用数字定优先级)

在这里插入图片描述
数字越小的优先级越高。

data 里也可以填写输出的位置
在这里插入图片描述

10(d)、队列的其他方法

q.qsize() 返回队列的大小
q.empty() 如果队列为空,返回True,反之False
q.full() 如果队列满了,返回True,反之False
q.full 与 maxsize 大小对应

q.get([block[, timeout]]) 获取队列,timeout等待时间
q.get_nowait() 相当q.get(False)
非阻塞 q.put(item) 写入队列,timeout等待时间
q.put_nowait(item) 相当q.put(item, False)

q.task_done() 在完成一项工作之后,该方法向任务已经完成的队列发送一个信号(通过join()来接收)
q.join() 实际上意味着等到队列为空,再执行别的操作(接收task_done() 的信号)

11、生产者消费者模型(用得少,考的多)

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值