【笔记】python自学笔记(爬虫篇)——多线程
一.前言今天说一下多线程,可能前面的概念理解起来有点费劲,但是还是要说一下的,我们也不能对多线程一点点了解都没有,当然了,我相信到后面的实操,大家就很容易理解了
https://static.52pojie.cn/static/image/hrline/1.gif
二.什么是线程
线程(thread)也叫轻量级进程,是操作系统能够进行运算调度的最小单位,它被包涵在进程之中,是进程中的实际运作单位,线程自己不拥有线程资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其他线程共享进程所拥有的全部资源,一个线程可以创建和撤销另一个线程,同一进程中的多个多线程之间可以并发执行。
https://static.52pojie.cn/static/image/hrline/1.gif
三.为什么要使用多线程
线程在程序中是独立的,并发的执行流,与相互分隔的进程相比,进程中线程之间的隔离程度更小,它们共享内存,文件句柄和其他进程中应有的状态,线程共享的环境包括进程代码段,进程的共有数据等,利用这些共享的数据,线程之间很容易实现通信
总结起来,使用多线程编程具有如下几个优点:
1.进程之间不能共享内存,但是线程之间共享内存非常容易
2.操作系统在创建进程是,需要为该进程重新分配系统资源,但创建线程的代价则小得多,因此,使用多线程来实现多任务并发执行比使用多进程的效率高
3.python语言内置了多线程支持功能,而不是单纯地作为底层操作系统的调度方式,从而简化了python的多线程编程
https://static.52pojie.cn/static/image/hrline/1.gif
四.线程实现
1.threading模块
import threading #这是python中的多线程库
import time #时间库
def example(name1): #函数中的两个参数分别要对应下面args中的参数,数量不同会报错
print("线程正在运行",name1)
time.sleep(1) #沉睡一秒
print("线程运行结束",name1)
if__name__ == "__main__": #程序的入口,类似C语言中的main()
#第一个参数target是线程函数变量
#第二个参数args是一个数组变量参数
#如果只传递一个值,就只需要一个值,
#如果需要传递多个参数,那么还可以继续传递下去其他的参数,
#其中的逗号不能少,元组中只包含一个元素时,需要在元素后面添加逗号
t1 = threading.Thread(target=example,args = ("t1",))
t2 = threading.Thread(target=example,args = ("t2",))
#上面两句是声明,我们要用start方法启动
t1.start()
t2.start()
这是运行结果,但是有一个点,我们线程运行是t1,t2顺序,但是运行结束会出现t2,t1的顺序,原因是我们线程调度是由操作系统层面来进行操作的,它的资源分配是由OS来执行的,实际上它的底层是一种GIL锁资源抢占的方式,关于GIL我也介绍一下吧
上图中表示的是两个线程在双核CPU上得执行情况。两个线程均为CPU密集型运算线程。绿色部分表示该线程在运行,且在执行有用的计算,红色部分为线程被调度唤醒,但是无法获取GIL导致无法进行有效运算等待的时间,它们不能同时运行,所以是一种资源抢占的方式,谁先结束,谁就能先抢到,这个一定程度上无法进行人为的控制,当有至少一个CPU密集型线程存在,那么多线程效率会由于GIL而大幅下降
2.自定义线程
继承threading.Thread来自定义线程类,其本质是重构Thread类中的run方法
import threading
import time
class MyThread(threading.Thread):
def __init__(self,n): #重写初始化方法(构造方法)
super(MyThread,self).__init__() #重构run函数必须要写
self.n = n
def run(self): #重写run方法(调度方法)
print("task run:",self.n)
time.sleep(1)
print("task finish:",self.n)
if __name__ == "__main__":
t1 = MyThread(n="t1")
t2 = MyThread(n="t2")
t1.start()
t2.start()
这个自定义线程我们一般用不到,除非你是需要在人家的基础上多增加一些实现的方法才用它
3.守护线程:我们看下面的这个例子,使用setDaemon(Ture)把所有的子线程都变成了主线程的守护线程,因此当主线程结束后,子线程也随之结束,所以当主线程结束后,整个程序就退出了。
import threading
import time
class MyThread(threading.Thread):
def __init__(self,n): #重写初始化方法(构造方法)
super(MyThread,self).__init__() #重构run函数必须要写
self.n = n
def run(self): #重写run方法(调度方法)
print("task run:",self.n)
time.sleep(1)
print("task finish:",self.n)
if __name__ == "__main__":
t1 = MyThread(n="t1")
t2 = MyThread(n="t2")
t1.setDaemon(True) #这两句就是增加守护线程
t2.setDaemon(True)
t1.start()
t2.start()
运行结果是这样的
结果我们发现和我们预想的不一样,只有run,却没有finish,因为设置了守护线程,所以主线程结束程序就结束了,我写一行代码大家看一下
大家可以发现我们没有将彻底运行结束,那我们如果要等子线程彻底运行完的话就要使用join方法,这个就是等待的意思,等待子线程全部运行结束再停止
4.共享全局变量
import threading
import time
g_num = 100 #定义一个值为100的全局变量
def work1():
global g_num #在整个程序运行期间把g_num当作常量
for i in range(3):
g_num = g_num + 1
print(g_num)
def work2():
global g_num
print(g_num)
if __main__ == "__main__":
t1 = threading.Thread(target=work1)
t1.start()
time.sleep(1)
t2 = threading.Thread(target=work2)
t2.start()
其实在这儿程序中,只是多了一个global,来声明一下这个g_num共享,如果没有这个global的话,程序就会错误,因为在程序觉得在子程序中找不到g_num这个变量
5.互斥锁
由于线程之间是随即调度,并且每个线程可能只执行n条数据之后,当多个线程同时修改同一条数据是可能会出现脏数据,所以,出现了线程锁,根据我的理解就是,当多个线程进行资源抢占的时候,我这个线程在修改变量的时候,其他线程都不能进行修改,也不会把锁放开,只有我把操作全部完成之后,才会把锁放开
接下来我们看两个程序,分别就是用了互斥锁和没用互斥锁,我们比较一下他们的运行结果
(1)没用互斥锁
from threading import Thread,Lock
import os,time
def work():
global n
temp = n #n赋值给temp
time.sleep(0.1)
n = temp - 1
if __name__ == "__main__":
n = 100 #全局变量
l = []
for i in range(100): #100个线程运行work函数
p = Thread(target = work)
l.append(p)
p.start()
for p in l:
p.join() #等待每个线程都运行完毕
print(n)
(2)用了互斥锁
from threading import Thread,Lock
import os,time
def work():
global n
lock.acquire() #上锁
temp = n #n赋值给temp
time.sleep(0.1)
n = temp - 1
lock.release() #释放锁
if __name__ == "__main__":
lock = Lock() #锁机制
n = 100 #全局变量
l = []
for i in range(100): #100个线程运行work函数
p = Thread(target = work)
l.append(p)
p.start()
for p in l:
p.join() #等待每个线程都运行完毕
print(n)
两个程序的运行结果:
没上锁的运行结果是99,但上了锁的运行结果却是0,很奇怪对不对,其实我们稍微一分析,就能知道,最终我们想要的结果应该是0,是99的原因是出现了线程安全的问题
6.递归锁
其实和互斥锁一样的,只不过递归锁支持嵌套使用,我没用过,估计大家用到的可能性也不大,这里我就不介绍了
7.信号量
也就是我们通常说的几线程运行,我们设置最多允许几个线程同时运行,只有之前的运行完,把信号量交换回来,它才会继续运行
import threading
import time
def run(n,semaphore):
semaphore.acquire()
time.sleep(1)
print(f"run the thread:{n}")
semaphore.release()
if __name__ == "__main__":
num = 0
semaphore = threading.BoundedSemaphore(5) #最多同时允许三个线程同时运行
for i in range(22):
t = threading.Thread(target=run,args =("t-%s"%i,semaphore))
t.start()
while threading.active_count()!=1:
pass
else:
print("-----all threads done-----")
这个程序我们总共线程是21,但是最多同时运行的线程数只有5,因为我们设置的信号量是5,当然,这个信号量你也可以自己指定
https://static.52pojie.cn/static/image/hrline/1.gif
后言:其实上面说了这么多,大家一般使用的话只需要掌握普通的创建方法和互斥锁即可,当然了,还有一部分我没有说,比如事件等等,但我上面说的这些已经足够日常使用了,好的,感谢大家的观看 好!从明天起开始学习 感谢分享。 谢谢楼主分享,很实用的教材 可否提供一个简单例子,谢谢分享。 有错别字,但不碍事,看了楼主的主题,对多线程又多了一些理论上的了解,比较用心的总结{:1_921:} 先收藏慢慢学 304775988 发表于 2021-2-22 23:26
有错别字,但不碍事,看了楼主的主题,对多线程又多了一些理论上的了解,比较用心的总结
纯手打,有错别字也正常,{:1_924:} hshcompass 发表于 2021-2-22 23:20
可否提供一个简单例子,谢谢分享。
上面的源码就是例子呀 lms1206 发表于 2021-2-22 22:56
好!从明天起开始学习
那你一定要开始哦,加油{:1_918:}