作者@failymao
本文为作者原创转载请注明出处https://www.cnblogs.com/failymao/p/10505911.html
python线程入门
正常情况下我们在启动一个程序的时候。这个程序会先启动一个进程启动之后这个进程会启动起来一个线程。这个线程再去处理事务。也就是说真正干活的是线程进程这玩意只负责向系统要内存
要资源但是进程自己是不干活的。默认情况下只有一个进程只会拉起来一个线程。
多线程顾名思义就是同样在一个进程的情况同时拉起来多个线程
。真正干活的是线程。进程与线程的关系就像是工厂和工人的关系, 要想一个工厂运行起来,至少有一个工,当然如果工人多, 那么效率就变高了。因为只有一个进程所以多线程在提高效率的同时并没有向系统伸手要更多的内存资源。因此使用起来性价比还是很高的。但是虽然多线程不会消耗更多的内存但是每个线程却需要CPU的的参与。
可以这样理解: 工厂虽然是固定的大小,可以容纳很多工人取干活, 但是工人干活儿需要人来协调, 如果工人太多, 对于一个固定的厂, CPU相当于厂长, 厂长的精力也是有限的, 当厂长(CPU忙不过不过来的时候效率也一样会有影响. 所以工人线程的数量最好还是在厂长cpu的能力内核数范围之内比较好。
线程与进程
硬件发展
cpu 切片由之前的串行处理到后来实行分片同时执行一颗线程处理效率更快.多核CPU 同一时刻可以执行多个线程。
软件发展
同一个进程下的多个线程可能在多颗CPU上执行一个线程不可能同时在多个cpu上。其他语言会出现
Python语言不会出现同一个进程下的多线程同时出现在多个CPU上全局解释器锁GRL. 限定了当一个进程下的多个线程处理的时候只能在一个CPU上执行当一个线程被处理的时候其他的线程只会等候这样的缺点处理效率很低。
单进程单线程
多线程单进程的程序
多线程多进程的程序。
进程的开销通常比线程昂贵,因为线程自动共享内存地址空间和文件描述符. 意味着, 创建进程比创建线程会花费更多的资源和时间
在执行一些
sleep/read/write/recv/send
这些会导致阻塞的函数时当前线程会主动放弃GIL然后调用相应的系统API完成后再重新申请GIL。因此GIL也并不是导致Python的多线程完全没用在一些IO等待的场合Python多线程还是发挥了作用当然如果多线程都是用于CPU密集的代码那多线程的执行效率就明显会比单线程的低。
程序, 线程与 进程关系
一个程序可能有多个多个进程, 某一个进程里面可以包含多个线程.
线程
创建线程
如何实现: 使用
threading
模块创建一个简单的线程
启动一个线程就是把一个
函数传入
并创建Thread
实例然后调用start()
开始执行启动一个线程
#!/usr/bin/env python#-*-coding:utf-8-*-import time, threading# 线程执行的代码:def thread_demo(): print('thread %s is running...' % threading.current_thread().name) n = 0 while n < 5: n = n + 1 print('thread %s >>> %s' % (threading.current_thread().name, n)) time.sleep(1) print('thread %s ended.' % threading.current_thread().name)if __name__ == "__main__": print('thread %s is running...' % threading.current_thread().name) t = threading.Thread(target=thread_demo, name= "subThread") t.start() t.join() print('thread %s ended.' % threading.current_thread().name)
启动多个线程
#!/usr/bin/env python#-*-coding:utf-8-*-import threadingimport timedef foo(num): for i in range(num): time.sleep(0.5) print iif __name__ == "__main__": for x in range(3): # 启动多个线程 t=threading.Thread(target=foo,args=(3,)) t.setDaemon(False) t.start() # 通过join方法让线程逐条执行 # t.join()#output0001111222333
其实这就是三个线程并行运行同时输出所以把结果都输出到一起引起。正是这种乱才整明白了确实三个函数在同时运行。
如果想让结果看起来规则一些可以考虑使用
join()
方法,join()可以理解为, 函数是按顺序执行的. 但是有时候这并不是我们想要的. 虽然创建了多个线程没有了并行还要多线程干嘛, 因此join方法不能随便乱用的
threading
模块可以直接用来创建一个线程.t. start()
用来启动线程t.join
使多线程按顺序执行threading.Thread
实例化线程类.每实例化一个类,相当于开启一个线程, 参数target
传递函数,name
用来定义子线程的名字.
线程之Join方法正确姿势
先看代码
#!/usr/bin/env python#-*-coding:utf-8-*-import threadingimport timedef foo(num): for i in range(num): time.sleep(0.5) print(i)if __name__ == "__main__": """ 创建一个列表用于存储要启动多线程的实例 """ print("MainThread is running....") threads = [] for x in range(3): t = threading.Thread(target=foo, args=(3,)) # 把多线程的实例追加入列表要启动几个线程就追加几个实例 threads.append(t) for thr in threads: # 把列表中的实例遍历出来后调用start()方法以线程启动运行 thr.start() for thr in threads: """ isAlive()方法可以返回True或False用来判断是否还有没有运行结束 的线程。如果有的话就让主线程等待线程结束之后最后再结束。 """ if thr.isAlive(): thr.join() print("MainThread over")
t.setDaemon()
方法的时候我们知道主线程相当于程序的主运行流程。程序运行的时候最先启动的一定就是主线程主线程负责拉起子线程用于干活。例子中运行函数foo()线程其实都是子线程。因此可以说多线程其实就是多个子线程。那么程序运行完最后一个退出的也肯定就是主线程。因此上例中最后再遍历一遍threads列表的目的就是查看还是否有没有退出的子线程只要还有子线程是活的没有退出。通过join()
方法强制程序流程不可以走到主线程退出的那个步骤。只有等子线程都退出之后才能根据join()方法
的规则顺序执行到主线程退出的步骤。即最后 输出MainThread over
线程锁(Lock)
多线程和多进程最大的不同在于
多进程中同一个变量各自有一份拷贝存在于每个进程中互不影响
而多线程中所有变量都由所有线程共享所以任何一个变量都可以被任何一个线程修改
因此线程之间共享数据最大的危险在于多个线程同时改一个变量把内容给改乱了。一个混乱的例子
#!/usr/bin/env python#-*-coding:utf-8-*-import timeimport threading# 假定这是你的银行存款:balance = 0 # 余额def change_it(m): '''正常情况下: 存多少,取多少,金额应该不变''' global balance balance = balance + m balance = balance - mdef run_thread(n): for i in range(100000): change_it(n)if __name__ == "_mian__": t1 = threading.Thread(target=run_thread, args=(3,)) t2 = threading.Thread(target=run_thread, args=(4,)) t1.start() t2.start() t1.join() t2.join() print(balance)
定义了一个共享变量
balance
初始值为0
并且启动两个线程先存后取理论上结果应该为0
但是由于线程的调度是由操作系统决定的当t1、t2交替执行时只要循环次数足够多balance
的结果就不一定是0
了。原因是因为
高级语言的一条语句在CPU执行时是若干条语句
即使一个简单的计算balance = balance + x
也分两步可看以下两步
x = balance + n balance = x
数据错误的原因是因为修改
balance
需要多条语句而执行这几条语句时线程可能中断从而导致多个线程把同一个对象的内容改乱了。
两个线程同时一存一取就可能导致余额不对你肯定不希望你的银行存款莫名其妙地变成了负数所以我们必须确保一个线程在修改
balance
的时候别的线程一定不能改。如果我们要确保
balance
计算正确就要给change_it()
上一把锁当某个线程开始执行change_it()
时我们说该线程因为获得了锁因此其他线程不能同时执行change_it()
只能等待直到锁被释放后获得该锁以后才能改。由于锁只有一个无论多少线程同一时刻最多只有一个线程持有该锁所以不会造成修改的冲突。创建一个锁就是通过threading.Lock()
来实现
为线程上一把锁
#!/usr/bin/env python#-*-coding:utf-8-*-import timeimport threading# 假定这是你的银行存款:balance = 0 # 余额lock = threading.Lock()def change_it(m): '''正常情况下: 存多少,取多少,金额应该不变''' global balance balance = balance + m balance = balance - mdef run_thread(n): for i in range(100000): lock.acquire() try: # 放心地改吧: change_it(n) finally: # 改完了一定要释放锁: lock.release()if __name__ == "_mian__": t1 = threading.Thread(target=run_thread, args=(3,)) t2 = threading.Thread(target=run_thread, args=(4,)) t1.start() t2.start() t1.join() t2.join() print(balance)
当多个线程同时执行
lock.acquire()
时只有一个线程能成功地获取锁
然后继续执行代码其他线程就继续等待直到获得锁
为止。获得锁的线程用完后一定要释放锁
否则那些苦苦等待锁的线程将永远等待下去成为死线程。所以我们用try...finally
来确保锁一定会被释放。好处 确保了某段关键代码只能由一个线程从头到尾完整地执行比如多线程操作数据库
坏处首先是阻止了多线程并发执行包含锁的某段代码实际上只能以单线程模式执行效率就大大地下降了 其次由于可以存在多个锁不同的线程持有不同的锁并试图获取对方持有的锁时可能会造成死锁导致多个线程全部挂起既不能执行也无法结束只能靠操作系统强制终止。
总结
如何创建一个线程
使用 threading.Thread 参数包含 target args name 等
如何创建多线程
使用 for 语句
什么是守护线程 什么是主线程
t.setDaemon(True)
表示的是后台线程 表示程序流程主线程跑完之后直接就关闭了然后退出了根本不管子线程是否执行完默认
t.setDaemon(False)
, 表示前台线程主线程执行过程中子线程也在进行主线程执行完毕后等待子线程都执行完成后程序才会停止.
join() 方法 使用正确姿势: 使用线程池, 谨慎使用
线程锁:
threading.Lock()
,解决线程间共享内存,同时对一个变量进行修改时造成数据混乱, 应用有: 比如多个多个线程对数据库同一个数据进行修改
参考
python 并发执行之多线程
Python3 多进程和多线程