目录
一、什么是线程
二、开启线程的两种方式
三、多线程与多进程的区别
四、守护线程
五、GIL全局解释器锁
六、死锁和递归锁
七、信号量、Event、定时器
八、线程queue
九、进程池和线程池
一、什么是线程
1.1 概念
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈)但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行。
1.2 进程和线程的区别
1、一个程序至少有一个进程,一个进程至少有一个线程。(进程可以理解成线程的容器)
2、进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
3、线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
4、进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。
二、开启线程的两种方式
2.1 threading模块介绍
threading模块的接口与multiprocess模块很相像。二者在使用层面,有很大的相似性,因而不再详细介绍。
2.2 开启线程的两种方式
方式一:调用内置的类
1 import time,random 2 from threading import Thread 3 4 5 def piao(name): 6 print("%s is piaoing" % name) 7 time.sleep(random.randrange(1,5)) 8 print("%s piao end" % name) 9 10 11 if __name__ =="__main__": 12 t1 = Thread(target=piao,args=("egon",)) 13 14 t1.start() 15 print("主进程") #每开一个进程,默认有一个线程 16 #所以当前有一个进程一个线程
方式二:自定义类
1 import time 2 from threading import Thread 3 4 5 class MyThread(Thread): 6 def __init__(self,name): 7 super().__init__() 8 self.name = name 9 10 def run(self): 11 print("%s is piaoing" % self.name) 12 time.sleep(2) 13 print("%s is running" % self.name) 14 15 16 if __name__ == "__main__": 17 t1 = MyThread("egon") 18 t1.start() 19 print("主进程")
三、多线程与多进程的区别
1 创建进程的开销远大于线程,可利用time模块来对比所耗时间。
1 from threading import Thread 2 import time 3 4 def work(start): 5 stop = time.time() 6 print("开启线程所用时间为%.5f秒" % (stop - start)) 7 8 9 if __name__ == '__main__': 10 start = time.time() 11 t=Thread(target=work,args=(start,)) 12 t.start() 13 14 15 #运行结果如下: 16 开启线程所用时间为0.00050秒
1 from multiprocessing import Process 2 import time 3 4 def work(start): 5 stop = time.time() 6 print("开启子进程所用时间为%.5f秒" % (stop - start)) 7 8 9 if __name__ == '__main__': 10 start = time.time() 11 p=Process(target=work,args=(start,)) 12 p.start() 13 14 15 #运行结果如下: 16 开启子进程所用时间为0.14338秒
2 在主进程下开启多个线程,各线程与主线程pid相同;而开启多个进程,每个进程都用不同的pid。因为同一进程内多个线程共享该进程的内存空间,而各进程间内存空间相互隔离。
1 from threading import Thread 2 import os 3 4 def work(): 5 print('hello',os.getpid()) 6 7 if __name__ == '__main__': 8 t1=Thread(target=work) 9 t2=Thread(target=work) 10 t1.start() 11 t2.start() 12 print('主线程/主进程pid',os.getpid()) 13 14 运行结果如下: 15 hello 21188 16 hello 21188 17 主线程/主进程pid 21188
1 from multiprocessing import Process 2 import os 3 4 def work(): 5 print('hello',os.getpid()) 6 7 if __name__ == '__main__': 8 p1=Process(target=work) 9 p2=Process(target=work) 10 p1.start() 11 p2.start() 12 print('主线程/主进程',os.getpid()) 13 14 15 #运行结果如下 16 主线程/主进程 22292 17 hello 1160 18 hello 23292
四、守护线程
无论是进程还是线程,都遵循:守护xxx会等待主xxx运行完毕后被销毁。需要强调的是:运行完毕并非终止运行。
1、对主进程来说,运行完毕指的是主进程代码运行完毕
2、对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕
即:
1、主进程在其代码结束后就已经算运行完毕(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程)才会结束。
2、主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束。
验证如下:
1 from threading import Thread 2 import time 3 4 def foo(): 5 print(123) 6 time.sleep(1) 7 print("end123") 8 9 def bar(): 10 print(456) 11 time.sleep(3) 12 print("end456") 13 14 if __name__ == '__main__': 15 t1=Thread(target=foo) 16 t2=Thread(target=bar) 17 18 t1.daemon=True 19 t1.start() 20 t2.start() 21 print("main-------") 22 23 24 #运行结果如下 25 123 26 456 27 main------- 28 end123 29 end456
线程t1,t2洗后开启,打印出123,456.紧接着主线程打印出main-------。此时,主线程需要等待非守护线程t2结束,在此期间,t1线程打印end123,然后t2打印end456
五、GIL全局解释器锁
GIL的全称是:Global Interpreter Lock,意思就是全局解释器锁,这个GIL并不是python的特性,他是只在Cpython解释器里引入的一个概念,而在其他的语言编写的解释器里就没有这个GIL。所以,GIL并不是Python的特性,Python完全可以不依赖于GIL。
GIL与Lock:
GIL本质也是互斥锁。锁的目的是为了保护共享的数据,同一时间只能有一个线程来修改共享的数据,而保护不同的数据就应该加不同的锁。所以,GIL 与Lock是两把锁,保护的数据不一样,前者是解释器级别的(当然保护的就是解释器级别的数据,比如垃圾回收的数据),后者是保护用户自己开发的应用程序的数据,很明显GIL不负责这件事,只能用户自定义加锁处理,即Lock。
GIL与多线程:
cpu是用来做计算的。多cpu意味着可以有多个核并行完成计算,所以多核提升的是计算性能。但每个cpu一旦遇到I/O阻塞,仍然需要等待,所以多核对I/O操作没什么用处。我们可以来验证。
1 from multiprocessing import Process 2 from threading import Thread 3 import os,time 4 5 def work(): 6 res=0 7 for i in range(100000000): 8 res*=i 9 10 if __name__ == '__main__': 11 l=[] 12 print(os.cpu_count()) #本机为4核 13 start=time.time() 14 for i in range(4): 15 p=Process(target=work) #耗时4s多 16 #p=Thread(target=work) #耗时17s多 17 l.append(p) 18 p.start() 19 for p in l: 20 p.join() 21 stop=time.time() 22 print('run time is %s' %(stop-start))
1 from multiprocessing import Process 2 from threading import Thread 3 import threading 4 import os,time 5 def work(): 6 time.sleep(2) 7 print('===>') 8 9 if __name__ == '__main__': 10 l=[] 11 print(os.cpu_count()) #本机为4核 12 start=time.time() 13 for i in range(400): 14 p=Process(target=work) #耗时17s多,大部分时间耗费在创建进程上 15 #p=Thread(target=work) #耗时2s多 16 l.append(p) 17 p.start() 18 for p in l: 19 p.join() 20 stop=time.time() 21 print('run time is %s' %(stop-start))
结论:
多线程适用于IO密集型,如socket,爬虫,web。
多进程适用于计算密集型,如金融分析。
六、死锁和递归锁
6.1 死锁现象
所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。形象化的比喻:好比A、B两个人,A被锁在自己家里,手里只有B的房门钥匙,而B被锁在A家里,手里只有A的家门钥匙,此时,A、B就只能无尽的等待下去。
1 from threading import Thread,Lock 2 import time 3 mutexA=Lock() 4 mutexB=Lock() 5 6 class MyThread(Thread): 7 def run(self): 8 self.func1() 9 self.func2() 10 def func1(self): 11 mutexA.acquire() 12 print('\033[41m%s 拿到A锁\033[0m' %self.name) 13 14 mutexB.acquire() 15 print('\033[42m%s 拿到B锁\033[0m' %self.name) 16 mutexB.release() 17 18 mutexA.release() 19 20 def func2(self): 21 mutexB.acquire() 22 print('\033[43m%s 拿到B锁\033[0m' %self.name) 23 time.sleep(2) 24 25 mutexA.acquire() 26 print('\033[44m%s 拿到A锁\033[0m' %self.name) 27 mutexA.release() 28 29 mutexB.release() 30 31 if __name__ == '__main__': 32 for i in range(10): 33 t=MyThread() 34 t.start() 35 36 #执行结果如下: 37 Thread-1 拿到A锁 38 Thread-1 拿到B锁 39 Thread-1 拿到B锁 40 Thread-2 拿到A锁 41 #此时出现死锁,整个程序阻塞住
对上述代码的执行过程分析如下:
- thread1首先抢到了A锁,此时thread1没有释放A锁,紧接着执行代码mutexB.acquire(),抢到了B锁。thread1在抢B锁时候,没有其他线程与thread1争抢,因为A锁没有释放,其他线程只能等待。
- thread1继续往下执行,先后释放B锁,A锁,就执行完func1代码。然后继续执行func2代码,但是在func2中,执行代码 mutexB.acquire()抢到B锁后,然后进入睡眠状态。
- 在thread1执行完func1的同时,thread2抢到了A锁,接下来要抢B锁,但是B锁被thread1获得,此时thread2在此处卡住。
- thread1睡眠结束后往下走,需要抢A锁,但是A锁已被thread2获得尚未释放,所以thread1在此卡住。此时,整个程序出现死锁现象。
要想避免出现死锁,通过手动加锁和解锁,步骤繁琐,容易出错,此时引出递归锁的概念。
6.2 递归锁
递归锁,在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。
这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁,二者的区别是:递归锁可以连续acquire多次,而互斥锁只能acquire一次。
1 from threading import Thread,RLock 2 import time 3 4 # mutexA=RLock() 5 # mutexB=mutexA 6 mutexB=mutexA=RLock() 7 8 class MyThread(Thread): 9 def run(self): 10 self.f1() 11 self.f2() 12 13 def f1(self): 14 mutexA.acquire() 15 print("%s 拿到了A锁"%self.name) 16 17 mutexB.acquire() 18 print("%s 拿到了B锁" % self.name) 19 mutexB.release() 20 21 mutexA.release() 22 23 def f2(self): 24 mutexB.acquire() 25 print("%s 拿到了B锁"%self.name) 26 time.sleep(0.1) 27 28 mutexA.acquire() 29 print("%s 拿到了A锁" % self.name) 30 mutexA.release() 31 32 mutexB.release() 33 34 if __name__ =="__main__": 35 for i in range(10): 36 t = MyThread() 37 t.start()
七、信号量、Event、定时器
7.1 信号量semaphore
信号量也是一把锁,它限定了可以同时有多少个任务拿到锁并执行。
semaphore是一个内置的计数器
- 每当调用acquire()时,内置计数器-1
- 每当调用release()时,内置计数器+1
- 计数器不能小于0,当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。
1 from threading import Thread,Semaphore,currentThread 2 import time,random 3 4 sm = Semaphore(3) 5 6 def task(): 7 with sm: 8 print("%s in" % currentThread().getName()) 9 time.sleep(random.randrange(1,3)) 10 11 if __name__ == "__main__": 12 for i in range(10): 13 t = Thread(target=task) 14 t.start()
执行上述代码可以发现,最多只允许3个线程同时运行。在实际工作中,为防止计算机泵机,可以引入信号量限制一个时间点内的线程数量。
7.2 事件Event
线程的一个关键特性是每个线程都是独立运行且状态不可预测。如果程序中的其他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就会变得非常棘手。为了解决这些问题,我们需要使用threading库中的Event对象。 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在 初始情况下,Event对象中的信号标志被设置为假。如果有线程等待一个Event对象, 而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行。所以,Event的主要作用就是用于主线程控制其他线程的执行。
Event用法:
event=threading.Event() #设置一个事件实例
event.set() #设置标志位
event.isSet():返回event的状态值
event.clear() #清空标志位
event.wait() #等待设置标志位
一个形象化的比喻:多个学生在上课,但是学生在上课期间不能做别的事,一定要等到老师发下课指令后,才可以休息。代码如下:
1 from threading import Thread,Event 2 import time 3 4 event = Event() 5 #event.wait() 6 #event.set() 7 8 def student(name): 9 print("学生%s 正在听课" % name) 10 event.wait() 11 print("学生%s 正在休息" % name) 12 13 def teacher(name): 14 print("老师%s 正在授课" % name) 15 time.sleep(7) 16 event.set() 17 18 if __name__ == "__main__": 19 stu1 = Thread(target=student,args=("alex",)) 20 stu2 = Thread(target=student, args=("egon",)) 21 stu3 = Thread(target=student, args=("wxx",)) 22 stu4 = Thread(target=student, args=("john",)) 23 24 t1 = Thread(target=teacher,args=("chuan",)) 25 26 stu1.start() 27 stu2.start() 28 stu3.start() 29 stu4.start() 30 31 t1.start()
7.3 定时器
定时器,指定n秒后执行某操作。
1 from threading import Timer 2 3 def hello(): 4 print("hello, world") 5 6 t = Timer(1, hello) 7 t.start() # after 1 seconds, "hello, world" will be printed
八、线程queue
Queue模块实现了多生产者多消费者队列, 尤其适合多线程编程.Queue类中实现了所有需要的锁原语, Queue模块实现了三种类型队列:
- FIFO(先进先出)队列, 第一加入队列的任务, 被第一个取出;
- LIFO(后进先出)队列,最后加入队列的任务, 被第一个取出(操作类似与栈, 总是从栈顶取出);
- PriorityQueue(优先级)队列, 保持队列数据有序, 最小值被先取出。
1 import queue 2 3 q=queue.Queue() 4 q.put('first') 5 q.put('second') 6 q.put('third') 7 8 print(q.get()) 9 print(q.get()) 10 print(q.get()) 11 12 13 ''' 14 结果(先进先出): 15 first 16 second 17 third 18 '''
1 import queue 2 3 q=queue.LifoQueue() 4 q.put('first') 5 q.put('second') 6 q.put('third') 7 8 print(q.get()) 9 print(q.get()) 10 print(q.get()) 11 12 ''' 13 结果(后进先出): 14 third 15 second 16 first 17 '''
1 import queue 2 3 q=queue.PriorityQueue() 4 #put进入一个元组,元组的第一个元素是优先级(通常是数字,也可以是非数字之间的比较),数字越小优先级越高 5 q.put((20,'a')) 6 q.put((10,'b')) 7 q.put((30,'c')) 8 9 print(q.get()) 10 print(q.get()) 11 print(q.get()) 12 13 ''' 14 结果(数字越小优先级越高,优先级高的优先出队): 15 (10, 'b') 16 (20, 'a') 17 (30, 'c') 18 '''
九、进程池和线程池
concurrent.futures模块是用来创建并行的任务,提供了更高级别的接口,为了异步执行调用
ThreadPoolExecutor:线程池,提供异步调用
ProcessPoolExecutor: 进程池,提供异步调用
1 from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor 2 import os,time,random 3 4 def task(n): 5 print('[%s] is running'%os.getpid()) 6 time.sleep(random.randint(1,3)) #I/O密集型的,,一般用线程,用了进程耗时长 7 return n**2 8 9 if __name__ == '__main__': 10 start = time.time() 11 p = ProcessPoolExecutor() 12 for i in range(10): #现在是开了10个任务, 那么如果是上百个任务呢,就不能无线的开进程,那么就得考虑控制线程数了,那么就得考虑到池了 13 obj = p.submit(task,i).result() #submit提交任务,reslut(timeout=None)取得结果 14 p.shutdown() #相当于close和join方法 15 print('='*30) 16 print(time.time() - start) #本机22秒
1 from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor 2 import os,time,random 3 4 def task(n): 5 print('[%s] is running'%os.getpid()) 6 time.sleep(random.randint(1,3)) 7 return n**2 8 9 if __name__ == '__main__': 10 start = time.time() 11 p = ProcessPoolExecutor() 12 l = [] 13 for i in range(10): 14 obj = p.submit(task,i) 15 l.append(obj) 16 p.shutdown() 17 print('='*30) 18 print([obj.result() for obj in l]) 19 print(time.time() - start) #本机执行5秒
总结:
- 同步意味着有序,异步意味着无序;
- 同步调用提交完任务后,就在原地等待任务执行完毕,拿到结果,再执行下一行代码,导致程序是串行执行;
- 异步调用:提交完任务后,不等待任务执行完毕
- 线程池与进程池使用方法类似
map方法
map(fn, *iterables, timeout=None),第一个参数fn是线程执行的函数;第二个参数接受一个可迭代对象;第三个参数timeout跟wait()的timeout一样,但由于map是返回线程执行的结果,如果timeout小于线程执行时间会抛异常TimeoutError。map的返回是有序的,它会根据第二个参数的顺序返回执行的结果:
1 import time 2 from concurrent.futures import ThreadPoolExecutor 3 4 5 def get_thread_time(times): 6 time.sleep(times) 7 return times 8 9 10 start = time.time() 11 executor = ThreadPoolExecutor(max_workers=4) 12 13 i = 1 14 for result in executor.map(get_thread_time,[2,3,1,4]): 15 print("task{}:{}".format(i, result)) 16 i += 1 17 18 #执行结果如下 19 task1:2 20 task2:3 21 task3:1 22 task4:4