开启线程的两种方式
import threadingimport timedef sayhi(name): time.sleep(2) print('%s 好帅!' %name)if __name__ == '__main__': t=threading.Thread(target=sayhi,args=('kay',)) t.start() print('主线程')
import threadingimport timeclass Sayhi(threading.Thread): def __init__(self,name): super().__init__() self.name=name def run(self): time.sleep(2) print('%s 真的很帅!' % self.name)if __name__ == '__main__': t = Sayhi('kay') t.start() print('主线程')
在一个进程下开启多个线程与在一个进程下开启多个子进程的区别
from threading import Threadfrom multiprocessing import Processimport osdef work(): print('hello')if __name__ == '__main__': #在主进程下开启线程 t=Thread(target=work) t.start() print('主线程/主进程') ''' 打印结果: hello 主线程/主进程 ''' #在主进程下开启子进程 t=Process(target=work) t.start() print('主线程/主进程') ''' 打印结果: 主线程/主进程 hello '''
from threading import Threadfrom multiprocessing import Processimport osdef work(): print('hello',os.getpid())if __name__ == '__main__': #part1:在主进程下开启多个线程,每个线程都跟主进程的pid一样 t1=Thread(target=work) t2=Thread(target=work) t1.start() t2.start() print('主线程/主进程pid',os.getpid()) #part2:开多个进程,每个进程都有不同的pid p1=Process(target=work) p2=Process(target=work) p1.start() p2.start() print('主线程/主进程pid',os.getpid())
from threading import Threadfrom multiprocessing import Processimport osdef work(): global n n=0if __name__ == '__main__': # n=100 # p=Process(target=work) # p.start() # p.join() # print('主',n) #毫无疑问子进程p已经将自己的全局的n改成了0,但改的仅仅是它自己的,查看父进程的n仍然为100 n=1 t=Thread(target=work) t.start() t.join() print('主',n) #查看结果为0,因为同一进程内的线程之间共享进程内的数据
线程相关的其他方法
Thread实例对象的方法 # isAlive(): 返回线程是否活动的。 # getName(): 返回线程名。 # setName(): 设置线程名。threading模块提供的一些方法: # threading.currentThread(): 返回当前的线程变量。 # threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。 # threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
from threading import Threadimport threadingfrom multiprocessing import Processimport osdef work(): import time time.sleep(3) print(threading.current_thread().getName())if __name__ == '__main__': #在主进程下开启线程 t=Thread(target=work) t.start() print(threading.current_thread().getName()) print(threading.current_thread()) #主线程 print(threading.enumerate()) #连同主线程在内有两个运行的线程 print(threading.active_count()) print('主线程/主进程') ''' 打印结果: MainThread <_MainThread(MainThread, started 140735268892672)> [<_MainThread(MainThread, started 140735268892672)>,] 主线程/主进程 Thread-1 '''
主线程等待子线程结束
from threading import Threadimport timedef sayhi(name): time.sleep(2) print('%s say hello' %name)if __name__ == '__main__': t=Thread(target=sayhi,args=('kay',)) t.start() t.join() print('主线程') print(t.is_alive()) ''' kay say hello 主线程 False '''
守护线程
无论是进程还是线程,都遵循:守护xxx会等待主xxx运行完毕后被销毁
需要强调的是:运行完毕并非终止运行
#1.对主进程来说,运行完毕指的是主进程代码运行完毕#2.对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕
详解:
#1 主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束,#2 主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束。
from threading import Threadimport timedef sayhi(name): time.sleep(2) print('%s say hello' %name)if __name__ == '__main__': t=Thread(target=sayhi,args=('egon',)) t.setDaemon(True) #必须在t.start()之前设置 t.start() print('主线程') print(t.is_alive()) ''' 主线程 True '''
from threading import Threadimport timedef foo(): print(123) time.sleep(1) print("end123")def bar(): print(456) time.sleep(3) print("end456")t1=Thread(target=foo)t2=Thread(target=bar)t1.daemon=Truet1.start()t2.start()print("main-------")迷惑人的例子
同步锁
三个需要注意的点:#1.线程抢的是GIL锁,GIL锁相当于执行权限,拿到执行权限后才能拿到互斥锁Lock,其他线程也可以抢到GIL,但如果发现Lock仍然没有被释放则阻塞,即便是拿到执行权限GIL也要立刻交出来#2.join是等待所有,即整体串行,而锁只是锁住修改共享数据的部分,即部分串行,要想保证数据安全的根本原理在于让并发变成串行,join与互斥锁都可以实现,毫无疑问,互斥锁的部分串行效率要更高#3. 一定要看本小节最后的GIL与互斥锁的经典分析
GIL
作用:Python内置的一个全局解释器锁,锁的作用就是保证同一时刻一个进程中只有一个线程可以被cpu调度。
成因:Python语言的创始人在开发这门语言时,目的快速把语言开发出来,如果加上GIL锁(C语言加锁),切换时按照100条字节指令来进行线程间的切换。
过程分析:
所有线程抢的是GIL锁,或者说所有线程抢的是执行权限
线程1抢到GIL锁,拿到执行权限,开始执行,然后加了一把Lock,还没有执行完毕,即线程1还未释放Lock,有可能线程2抢到GIL锁,开始执行,执行过程中发现Lock还没有被线程1释放,于是线程2进入阻塞,被夺走执行权限,有可能线程1拿到GIL,然后正常执行到释放Lock。。。这就导致了串行运行的效果
既然是串行,那我们执行
t1.start()
t1.join()
t2.start()
t2.join()
需知join是等待t1所有的代码执行完,相当于锁住了t1的所有代码,而Lock只是锁住一部分操作共享数据的代码。
因为Python解释器帮你自动定期进行内存回收,你可以理解为python解释器里有一个独立的线程,每过一段时间它起wake up做一次全局轮询看看哪些内存数据是可以被清空的,此时你自己的程序 里的线程和 py解释器自己的线程是并发运行的,假设你的线程删除了一个变量,py解释器的垃圾回收线程在清空这个变量的过程中的clearing时刻,可能一个其它线程正好又重新给这个还没来及得清空的内存空间赋值了,结果就有可能新赋值的数据被删除了,为了解决类似的问题,python解释器简单粗暴的加了锁,即当一个线程运行时,其它人都不能动,这样就解决了上述的问题, 这可以说是Python早期版本的遗留问题。
from threading import Threadimport os,timedef work(): global n temp=n time.sleep(0.1) n=temp-1if __name__ == '__main__': n=100 l=[] for i in range(100): p=Thread(target=work) l.append(p) p.start() for p in l: p.join() print(n) #结果可能为99
锁通常被用来实现对共享资源的同步访问。为每一个共享资源创建一个Lock对象,当你需要访问该资源时,调用acquire方法来获取锁对象(如果其它线程已经获得了该锁,则当前线程需等待其被释放),待资源访问完后,再调用release方法释放锁:
import timeimport threadinglock = threading.RLock()n = 10def task(i): print('这段代码不加锁',i) lock.acquire() # 加锁,此区域的代码同一时刻只能有一个线程执行 global n time.sleep(1) print('当前线程',i,'读取到的n值为:',n) n = i time.sleep(1) print('当前线程',i,'修改n值为:',n) lock.release() # 释放锁for i in range(10): t = threading.Thread(target=task,args=(i,)) t.start()"""结果:这段代码不加锁 0这段代码不加锁 1这段代码不加锁 2这段代码不加锁 3这段代码不加锁 4这段代码不加锁 5这段代码不加锁 6这段代码不加锁 7这段代码不加锁 8这段代码不加锁 9当前线程 0 读取到的n值为: 10当前线程 0 修改n值为: 0当前线程 1 读取到的n值为: 0当前线程 1 修改n值为: 1当前线程 2 读取到的n值为: 1当前线程 2 修改n值为: 2当前线程 3 读取到的n值为: 2当前线程 3 修改n值为: 3当前线程 4 读取到的n值为: 3当前线程 4 修改n值为: 4当前线程 5 读取到的n值为: 4当前线程 5 修改n值为: 5当前线程 6 读取到的n值为: 5当前线程 6 修改n值为: 6当前线程 7 读取到的n值为: 6当前线程 7 修改n值为: 7当前线程 8 读取到的n值为: 7当前线程 8 修改n值为: 8当前线程 9 读取到的n值为: 8当前线程 9 修改n值为: 9"""
GIL锁与互斥锁综合分析(重点!)
分析: #1.100个线程去抢GIL锁,即抢执行权限 #2. 肯定有一个线程先抢到GIL(暂且称为线程1),然后开始执行,一旦执行就会拿到lock.acquire() #3. 极有可能线程1还未运行完毕,就有另外一个线程2抢到GIL,然后开始运行,但线程2发现互斥锁lock还未被线程1释放,于是阻塞,被迫交出执行权限,即释放GIL #4.直到线程1重新抢到GIL,开始从上次暂停的位置继续执行,直到正常释放互斥锁lock,然后其他的线程再重复2 3 4的过程
#不加锁:并发执行,速度快,数据不安全from threading import current_thread,Thread,Lockimport os,timedef task(): global n print('%s is running' %current_thread().getName()) temp=n time.sleep(0.5) n=temp-1if __name__ == '__main__': n=100 lock=Lock() threads=[] start_time=time.time() for i in range(100): t=Thread(target=task) threads.append(t) t.start() for t in threads: t.join() stop_time=time.time() print('主:%s n:%s' %(stop_time-start_time,n))'''Thread-1 is runningThread-2 is running......Thread-100 is running主:0.5216062068939209 n:99'''#不加锁:未加锁部分并发执行,加锁部分串行执行,速度慢,数据安全from threading import current_thread,Thread,Lockimport os,timedef task(): #未加锁的代码并发运行 time.sleep(3) print('%s start to run' %current_thread().getName()) global n #加锁的代码串行运行 lock.acquire() temp=n time.sleep(0.5) n=temp-1 lock.release()if __name__ == '__main__': n=100 lock=Lock() threads=[] start_time=time.time() for i in range(100): t=Thread(target=task) threads.append(t) t.start() for t in threads: t.join() stop_time=time.time() print('主:%s n:%s' %(stop_time-start_time,n))'''Thread-1 is runningThread-2 is running......Thread-100 is running主:53.294203758239746 n:0'''#有的初学者可能有疑问:既然加锁会让运行变成串行,那么我在start之后立即使用join,就不用加锁了啊,也是串行的效果啊#没错:在start之后立刻使用jion,肯定会将100个任务的执行变成串行,毫无疑问,最终n的结果也肯定是0,是安全的,但问题是#start后立即join:任务内的所有代码都是串行执行的,而加锁,只是加锁的部分即修改共享数据的部分是串行的#单从保证数据安全方面,二者都可以实现,但很明显是加锁的效率更高.from threading import current_thread,Thread,Lockimport os,timedef task(): time.sleep(3) print('%s start to run' %current_thread().getName()) global n temp=n time.sleep(0.5) n=temp-1if __name__ == '__main__': n=100 lock=Lock() start_time=time.time() for i in range(100): t=Thread(target=task) t.start() t.join() stop_time=time.time() print('主:%s n:%s' %(stop_time-start_time,n))'''Thread-1 start to runThread-2 start to run......Thread-100 start to run主:350.6937336921692 n:0 #耗时是多么的恐怖'''
总结:
创建线程的原因:
- 由于线程是cpu工作的最小单元,创建线程可以利用多核有事实现并行操作(Java/C#)
- 注意:线程是为了工作
创建进程的原因:
- 进程和进程之间做数据隔离(Java/C#)
- 注意:进程是为了提供环境让线程工作
python中存在一个GIL锁,即:
- 全局解释器锁,用于限制一个进程中同一时刻只有一个线程被cpu调度,默认GIL锁在执行100个cpu指令停止(过期时间)
- 后果:多线程无法利用多核优势
- 解决:开多个进程处理(浪费资源),IO密集型(多线程)/计算密集型(多进程)