python多线程讲解

目录

什么是线程?

并发与并行

什么是GIL?

为什么要使用多线程?

多线程实例

守护线程

多线程共享全局变量

互斥锁Lock

RLock递归锁

信号量


python多线程是一个非常重要的知识点,接下来对该部分内容进行梳理。

什么是线程?

线程也叫轻量级进程,是操作系统能够进行运算调度的最小单位,它被包涵在进程之中,是进程中的实际运作单位。线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其他线程共享进程所拥有的全部资源。一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行。

并发与并行

并发是快速交替轮换执行(线程看起来是同时在运行,实际上是一个线程遇到耗时的IO等操作时将此线程挂起释放GIL全局解释器锁转而执行其他线程的过程,这个过程轮换的非常快,所以宏观上看起来就像是多个线程同时运行),并行是同一时间同时运行(进程可以做到并行运行,充分利用多核cpu的优势)

什么是GIL?

GIL全称是全局解释器锁,为了解决多线程之间数据完整性和状态同步而产生的一把锁。它保证了在同一时刻只有一个线程执行,当其他线程需要运行时必须先拿到这把锁才能执行,这就保证了数据的完整性和状态的同步。关于GIL的更多知识可以参考这篇博客

为什么要使用多线程?

线程在程序中是独立的、并发的执行流。与分隔的进程相比,进程中线程之间的隔离程度要小,它们共享内存、文件句柄 和其他进程应有的状态。
因为线程的划分尺度小于进程,使得多线程程序的并发性高。进程在执行过程之中拥有独立的内存单元,而多个线程共享内存,从而极大的提升了程序的运行效率。线程比进程具有更高的性能,这是由于同一个进程中的线程都有共性,多个线程共享一个进程的虚拟空间。线程的共享环境包括进程代码段、进程的共有数据等,利用这些共享的数据,线程之间很容易实现通信。操作系统在创建进程时,必须为改进程分配独立的内存空间,并分配大量的相关资源,但创建线程则简单得多。因此,使用多线程来实现并发比使用多进程的性能高得要多。

操作系统在创建进程时,需要为该进程重新分配系统资源,但创建线程的代价则小得多。因此使用多线程来实现多任务并发执行比使用多进程的效率。python语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了python的多线程编程。

多线程实例

普通创建方式:

 打印结果如下:

通过打印的顺序可以发现,多线程是异步非阻塞的(首先打印task2,再打印task3,最后等待task1执行结束后再结束主线程),主线程的结束并没有强制结束子线程,是等待子线程结束后才结束主线程。 当我们需要某个线程执行完成再往下面执行,可以手动添加join()方法阻塞主线程,这样主线程会等待join的线程执行完成再往下执行,下面守护线程会讲到join()方法。

自定义线程------继承threading.Thread类实现自定义线程,其本质是重构Thread类的run方法:

打印结果:

t.start()方法在执行的时候会自动调用run()方法,所以在自定义线程的时候只需要重写__init__方法和run()方法即可,__init__方法负责接收参数,这些参数最后会传入run()方法中,根据需要实现自定义功能。

守护线程

这里使用setDaemon(True)把所有的子线程都变成了主线程的守护线程,因此当主线程结束后,子线程也会随之结束,所以当主线程结束后,整个程序就退出了。

 把线程t1设置成守护线程,意味着不管t1线程有没有运行完成,只要主线程运行结束了,t1线程也会被迫结束,看一下打印结果:

t2、t3线程运行结束后主线程也随之运行到结尾,此时t1线程还处于sleep状态,但是由于设置成守护线程了,此时主线程关闭,t1线程也被迫结束,整个程序退出。

 但是在这种情况下,可以使用join()方法手动阻塞当前线程,看如下代码:

 使用t1.join()阻塞当前进程,当主线程运行到t1.join()的时候会被阻塞在此处,直到t1线程运行结束,所以就算t1线程设置成了守护线程,主线程也得等到t1线程运行完成后才能往下执行,然后结束主线程,退出程序。打印结果如下:

多线程共享全局变量

一个进程中的多个线程资源是共享的,看下面这个例子:

打印结果如下:

从打印结果可以看出来,全局的变量在work1中做了加法,在work2中打印num的值时显示的是加法运行之后的值,说明两个work操作的是同一个变量。从而印证了线程之间的资源是共享的。

虽然有了全局解释器锁GIL,但是它不能保证线程数据的安全性,当多个线程操作一个变量的时候,举个例子有个变量a初始值是1,要对其进行a+=1这个操作可以细分成两步,a+1 和a=a+1,比如两个线程对a进行操作,当线程1拿到调度权的时候,对a进行a+1操作,此时线程1交出了调度权,线程2获取到的执行的机会,同样对a进行操作,此时的a仍然是1,因为线程1只是做了加法运算还没有最终把a+1的值赋值给变量a的时候线程2便拿到了调度权,所以线程2做了一次完整的a+=1操作,a的值变成了2,这个时候线程2交出调度权,线程1拿到调度权继续运行最后的赋值操作,a的值为2。此时发现a做了两次加法操作,但是最终的值是2,这便产生了脏数据。为了解决这个问题引入线程锁。

互斥锁Lock

为了解决线程数据不安全性,引入互斥锁概念,看如下例子:

在修改变量n的时候,先lock.acquire()给线程上锁,这样其他的线程便拿不到这个资源,当完成对数据的操作后,lock.release()解锁,其他线程便可以操作变量n,这样便解决了原子性问题。

打印结果:使用10个线程对数字10000000进行减法操作,如期得到最后的结果1。使用互斥锁解决了线程数据不安全问题,避免了脏数据的产生。

RLock递归锁

还有一种锁RLock递归锁,用法与lock锁一样,不同的是它支持嵌套,在多个锁没有释放的时候,一般会使用RLock类。

信号量

用一句话说,信号量就是为了控制线程的并发数。

例如餐桌有三个位置,但是有10个人同时就餐,这时候就需要信号量参与保证餐桌上只有三个人,也就是线程的并发数始终是3。

观察打印结果发现,只有三个线程在并发运行,其他线程需要等待线程释放之后才参与进来。线程池也是为了控制线程的并发数。

如果你是新手,这篇博客可能会对你有所帮助。博客的作者是B站黑马程序员的《python多线程编程》的视频的笔记摘录。博客中的代码是作者手动敲的,可以直接运行,如果有错误,作者欢迎评论指正。在博客中,有两个示例代码可以帮助你理解多线程的概念和使用方法。第一个示例是一个简单的多线程实现,其中一个线程用来唱歌,另一个线程用来跳舞。在示例代码中,使用了`threading.Thread`来创建两个线程,并分别指定了目标函数和参数。然后通过`start`方法启动线程。这样,两个线程就可以并发地执行唱歌和跳舞的任务。第二个示例代码展示了线程之间执行的顺序。在这个示例中,使用了`threading.current_thread`来获取当前线程的线程对象,并打印出来。然后通过循环创建了5个子线程,并启动它们。由于多线程的执行是并发的,所以无法确定线程的执行顺序。希望这些示例代码对你理解和学习python多线程编程有所帮助。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [Python多线程编程(详细:适合小白入门)](https://blog.csdn.net/weixin_44917390/article/details/119610760)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值