1、线程
在python中,想要实现多任务除了使用进程,还可以使用线程来完成,线程是实现多任务的另一种形式
线程是进程中执行代码的一个分支,每个执行分支(线程),要想工作执行代码,需要CPU进行调度。
线程是CPU调度的基本单位,每个进程都最少有一个线程,而这个线程就是我们常说的主线程
(1)线程的作用
多线程可以完成多任务
程序默认会有一个主线程,程序员自己创建的线程可以成为子线程,多线程可以完成多任务
多线程,多个线程是在一个进程里创建的,多线程资源共享,共享全局变量
#线程的缘起
资源分配需要分配内存空间,分配cpu:
分配的内存空间存放着临时要处理的数据等,比如要执行的代码,数据
而这些内存空间是有限的,不能无限分配
目前配置高的主机,5万个并发已是上限.线程概念应用而生.
(2)线程的特点
线程是比较轻量级,能干更多的活,一个进程中的所有线程资源是共享的.
一个进程至少有一个线程在工作
一个进程里包含了多个线程,线程之间是异步并发
进程实际上是不干活的,线程才是干活的那一位
进程既可以并发,也可以并行
线程只能并发,不能并行
同一时间,一个进程中的多条线程只能被一个CPU执行
(3)线程的缺陷
#python中的线程可以并发,但是不能并行(同一个进程下的多个线程不能分开被多个cpu同时执行)
#原因:
全局解释器锁(Cpython解释器特有) GIL锁:
同一时间,一个进程下的多个线程只能有一个被cpu执行,不能实现线程的并行操作
python是解释型语言,执行一句编译一句,而不是一次性全部编译成功,不能提前规划,都是临时调度
容易造成cpu执行调度异常.所以加了一把锁叫GIL
#想要并行的解决办法:
(1)用多进程间接实现线程的并行,并不理想,开辟空间消耗资源
(2)换一个Pypy,Jpython解释器 也不好用,兼容性问题
#程序分为计算密集型和io密集型
对于计算密集型程序会过度依赖cpu,但网页,爬虫,OA办公,这种io密集型的程序里,python多线程绰绰有余
计算密集型,Python比较吃力
1、GIL锁不是python的特点。而是cpython的特点。
2、在cpython解释器中,GIL是一把互斥锁,用来保证进程中同一个时刻只有一个线程在执行。
3、在没有GIL锁的情况下,有可能多线程在执行一个代码的同时,垃圾回收机制对所执行代码的变量直接进行回收,其他的线程再使用该变量时会导致运行错误。
4、总结
对于IO密集型应用,即便有GIL存在,由于IO操作会导致GIL释放,其他线程能够获得执行权限。由于多线程的通讯成本低于多进程,因此偏向使用多线程。
对于计算密集型应用,由于CPU一直处于被占用状态,GIL锁直到规定时间才会释放,然后才会切换状态,导致多线程处于绝对的劣势,此时可以采用多进程+协程。
进程是资源分配的最小单元
线程是cpu执行调度的最小单元
Python先发明的线程,后来添加的进程。所以线程的使用方法与进程一样
#(1) 一个进程里包含了多个线程,线程之间是异步并发
from threading import Thread from multiprocessing import Process import os , time , random def func(i): time.sleep(random.uniform(0.1,0.9)) print("当前进程号:{}".format(os.getpid()) , i) if __name__ == "__main__": #线程里面这句话可以不加 for i in range(10): t = Thread(target=func,args=(i,)) t.start() print(os.getpid())
如下可见,子线程中的进程号与主进程号一样,并且子线程和主进程之间是异步并发的
#(2) 并发的多进程和多线程之间,多线程的速度更快
#多线程速度 def func(i): print( "当前进程号:{} , 参数是{} ".format(os.getpid() , i) ) if __name__ == "__main__": lst = [] startime = time.time() for i in range(10000): t = Thread(target=func,args=(i,)) t.start() lst.append(t) # print(lst) for i in lst: i.join() endtime = time.time() print("运行的时间是{}".format(endtime - startime) ) # 运行的时间是1.8805944919586182 #多进程速度 if __name__ == "__main__": lst = [] startime = time.time() for i in range(10000): p = Process(target=func,args=(i,)) p.start() lst.append(p) # print(lst) for i in lst: i.join() endtime = time.time() print("运行的时间是{}".format(endtime - startime) ) # 运行的时间是101.68004035949707
多进程:1000个进程耗时9.06秒
多线程:多线程之间,数据共享,可以直接操作数据。1000个线程耗时0.087秒,差距巨大,所以对于IO密集型,还是用多线程较快
多进程执行计算密集型,如果各个进程间各计算各的,不用共享数据,由于可以使用多核,比多线程快。
如果是各个进程间数据共享,同步计算最终结果,多进程反而非常慢,远远慢于多线程
#(3) 多线程之间,数据共享
num = 100 lst = [] def func(): global num num -= 1 for i in range(100): t = Thread(target=func) t.start() lst.append(t) for i in lst: i.join() print(num)
多线程之间共享数据,可以直接操作
2、自定义线程类
#用类定义线程,必须手动调用父类__init__方法,必须重载父类run方法,定义自己线程类逻辑
from threading import Thread
import os,time
(1)必须继承父类Thread,来自定义线程类
class MyThread(Thread): def __init__(self,name): # 手动调用父类的构造方法 super().__init__() # 自定义当前类需要传递的参数 self.name = name def run(self): print( "当前进程号{},name={}".format(os.getpid() , self.name) ) if __name__ == "__main__": t = MyThread("我是线程") t.start() print( "当前进程号{}".format(os.getpid()) )
新版:
#自定义线程类
from threading import Thread,current_thread # (1)必须继承父类Thread,来自定义线程类 class MyThread(Thread): def __init__(self,name): # 手动调用父类的构造方法 super().__init__() # 自定义当前类需要传递的参数 self.name = name def run(self): print(f"当前线程的线程号是{current_thread().ident}") if __name__ == '__main__': for i in range(10): t = MyThread(f"线程{i}") t.start()
(2)线程中的相关属性
# 线程.is_alive() 检测线程是否仍然存在 # 线程.setName() 设置线程名字 # 线程.getName() 获取线程名字 # 1.currentThread().ident 查看线程id号 新版用current_thread().ident # 2.enumerate() 返回目前正在运行的线程列表 # 3.activeCount() 返回目前正在运行的线程数量
getName,setName被弃用了
新版:
def func(): time.sleep(1) if __name__ == "__main__": t = Thread(target=func) t.start() # 检测线程是否仍然存在 print( t.is_alive() ) # 线程.getName() 获取线程名字 print(t.getName()) # 设置线程名字 t.setName("抓API接口") print(t.getName()) from threading import currentThread from threading import enumerate from threading import activeCount def func(): time.sleep(0.1) print("当前子线程号id是{},进程号{}".format( currentThread().ident ,os.getpid()) ) if __name__ == "__main__": t = Thread(target=func) t.start() print("当前主线程号id是{},进程号{}".format( currentThread().ident ,os.getpid()) ) for i in range(5): t = Thread(target=func) t.start() # 返回目前正在运行的线程列表 lst = enumerate() print(lst,len(lst)) # 返回目前正在运行的线程数量 (了解) print(activeCount())
新版:
# ### 线程中的相关属性 import time from threading import Thread,current_thread from threading import enumerate from threading import active_count def func(): time.sleep(1) print(f"当前子线程的线程号{current_thread().ident}") if __name__ == '__main__': for i in range(5): t = Thread(target=func) t.start() #设置线程名 t.name = f"抓API接口{i}" # 返回目前正在运行的线程列表 lst = enumerate() print(lst,len(lst)) # 返回目前正在运行的线程数量 (了解) print(active_count())
当前运行线程数量,一个主线程,5个子线程
3、守护线程
等待所有线程全部执行完毕之后,自己再终止程序,守护所有线程
from threading import Thread import time def func1(): while True: time.sleep(1) print("我是函数func1") def func2(): print("我是func2 start ... ") time.sleep(3) print("我是func2 end ... ") def func3(): print("我是func3 start ... ") time.sleep(6) print("我是func3 end ... ") if __name__ == "__main__": t = Thread(target=func1) t2 = Thread(target=func2) t3 = Thread(target=func3) # 设置守护线程 (启动前设置) 新版本该方法已弃用 t.setDaemon(True) t.start() t2.start() t3.start() print("主线程执行结束.... ")
新版本设置守护线程的setDaemon()方法已弃用,改为了通过属性设置
守护线程是等所有线程执行结束,自己再终止程序,守护所有线程
4、线程中的数据安全问题
#多线程之间共享全局变量,出现数据错误问题
import threading #全局变量 g_sum = 0 def sum1(): for i in range(1000000): # 不可变类型用方法修改数据,要声明为全局变量 global g_sum g_sum += 1 print('sum1', g_sum) def sum2(): for i in range(1000000): global g_sum g_sum += 1 print('sum2', g_sum) if __name__ == '__main__': sum1_thread = threading.Thread(target=sum1) sum2_thread = threading.Thread(target=sum2) sum1_thread.start() sum2_thread.start()
sum1 911662
sum2 1434503
出现的结果不是2百万,由于两个线程是随机执行的,有可能几乎同时对数据进行操作,导致少加的问题
解决办法:上锁
互斥锁:
互斥锁:对共享数据进行锁定,保证同一时刻只能有一个线程去操作
注意:
互斥锁是多个线程一起去抢,抢到锁的线程先执行,没抢到锁的线程需要等待,等互斥锁使用完释放后,
其他等待的线程再去抢这个锁
线程不安全,执行结果不对
多个线程可能同时操作数据,拿到的数据一样,操作后的结果不一样。造成混乱。比如几乎同时拿到1,运算需要时间,但不同线程读取得到的数据是一样的。
线程1拿到数据加1,得到2,线程2拿到数据减1,得到0
线程1往原来数据存2,线程2往原来存0,以后面放的为准,下次操作又会出现同样的混乱。
上锁:不同的线程采用同步计算,当一个线程运算完,才释放锁,让另一个线程运算
from threading import Thread , Lock import time n = 0 def func1(lock): global n lock.acquire() for i in range(1000000): n += 1 lock.release() def func2(lock): global n # with语法可以简化上锁+解锁的操作,自动完成 with lock: for i in range(1000000): n -= 1 if __name__ == "__main__": lst = [] lock = Lock() start = time.time() for i in range(10): t1 = Thread(target=func1 ,args=(lock,) ) t1.start() t2 = Thread(target=func2 ,args=(lock,) ) t2.start() lst.append(t1) lst.append(t2) for i in lst: i.join() # print(lst,len(lst)) end = time.time() print("主线程执行结束... 当前n结果为{} ,用时{}".format(n , end-start))
给线程上锁,执行结果准确 。with语法,可以简化上锁,解锁操作,自动完成