异步编程(二)----------------多线程
多线程和线程池并不是一回事
多线程是根据实际情况建立多个线程,线程池是一次性创建多个线程。
简单来说,目前有10个任务。多线程技术就是为10个任务建立10个线程。线程池可以一次性创建5个线程,来一个任务,就从线程池里取走一个线程,直到5个线程全部取走;同理,某一个线程任务结束之后,要归还给线程池。线程池有点像现在的租“充电宝”,充电宝就这么多,借完了再借就需要等,用完了还要放回去。
先来说用python实现多线程技术。python中的多线程技术是基于threading模块,先来一段代码,实现多线程技术。
import threading #计算0-9的平方和 sum1 = 0 sum2 = 0 def square_1(): global sum1 for i1 in range(10): sum1 = sum1+i1**2 #计算0-9的立方和 def square_2(): global sum2 for i2 in range(10): sum2 = sum2+i2**3 if __name__ == '__main__': s1 = threading.Thread(target=square_1,name = 's1') s2 = threading.Thread(target=square_2, name = 's2') s1.start() s2.start() print(sum1,sum2)
输出:
285 2025
这段小程序的意思是:square_1()计算0-9的平方和,square_2()计算0-9的立方和。这个中间还复习了下全局变量。。。在函数square_1()和square_2()使用全局变量要用global 强调下,否则报错。
通过threading.Thread() 创建线程,参数target=函数名(注意函数名后边没有括号),实例化对象s1、s2。通过s1.start()、s2.start() 启动线程。需要注意的是,两个线程从微观看是串行完成的,在CPU内部,一会执行进程s1、一会执行进程s2,没有规律,最后s1和s2谁也先执行完也不确定。就算交换s1.start()和s2.start(),谁先计算完也不确定。但是从宏观来看,是并发完成的。验证下:交换s1.start()、s2.start() ,结果:
285 2025
两次执行结果都一样,可以断定执行顺序和start()顺序是没关系的,但是又能说明一个问题,2次方比3次方运算量小,所以先执行完。
下一个问题:如果线程需要传参,怎么办呢?例如刚才的例子,需要在main()中设定参数:
import threading #计算0-9的平方和 sum1 = 0 sum2 = 0 def square_1(nb1): global sum1 for i1 in range(nb1): sum1 = sum1+i1**2 #计算0-9的立方和 def square_2(nb2): global sum2 for i2 in range(nb2): sum2 = sum2+i2**3 if __name__ == '__main__': s1 = threading.Thread(target=square_1,args=(10,),name = 's1') s2 = threading.Thread(target=square_2,args=(10,),name = 's2') s2.start() s1.start() print(sum1,sum2)
将函数定义中的10改为形参,在线程创建中添加参数args=(),这个括号代表是一个元组,将实参一个一个传进去,如果只有一个实参,就写为(10,),括号里边的逗号‘,’是不能够省略的。
下一个问题:多线程真的能节省时间吗?
为了验证,写一段代码,大体思路是用多线程计算平方和 和立方和,再用串行的方式计算平方和 和立方和。为了让时间久一点,将计算量加大。
import time import threading #计算0-9的平方和 sum1 = 0 sum2 = 0 def square_1(nb1): global sum1 for i1 in range(nb1): sum1 = sum1+i1**2 #计算0-9的立方和 def square_2(nb2): global sum2 for i2 in range(nb2): sum2 = sum2+i2**3 if __name__ == '__main__': start_time = time.time() s1 = threading.Thread(target=square_1,args=(1000,),name = 's1') s2 = threading.Thread(target=square_2,args=(1000,),name = 's2') s2.start() s1.start() s1.join() s2.join() end_time = time.time() print('多线程时间:',end_time-start_time) start_time = time.time() square_1(1000) square_2(1000) end_time = time.time() print('串行:', end_time - start_time)
输出:
多线程时间: 0.0019998550415039062
串行: 0.0010001659393310547
发现多线程的时间居然还要比串行时间要多一倍。观察代码,也没有发现问题。这是因为python中自带的GIL锁,有兴趣大家可以自行百度一下。这个GIL锁说白了,就是保证一个进程中只有一个线程在执行。那为什么多线程时间更久呢?应该一样才对呀,其实线程之间来回切换也要有时间开销。
很多同学就要说了,多线程没有研究的必要?既然比串行工作效率还要低,为什么还要研究它?
这就要说我们平常借助计算机工作的两大类任务:一类是CPU密集型,一类是IO密集型。顾明思议:CPU密集型是指任务以计算为主,IO密集型是指任务以IO为主。像刚才写的程序就以CPU计算为主。
CPU密集型任务使用多线程反而会降低效率,但是IO密集型任务使用多线程就可以提高效率,比如爬虫就是IO密集型。
细心的同学发现上一段代码中,比上上一段代码多了一个jion(),这个是干嘛的呢?来一个例子说明一下:
import threading import time def func1(): print('NO.1') time.sleep(0.5) print('NO.2') def func2(): print('NO.a') time.sleep(0.5) print('NO.b') if __name__ == '__main__': f1 = threading.Thread(target=func1) f2 = threading.Thread(target=func2) f1.start() f2.start() print('NO.A') print('NO.B')
输出:
NO.1 NO.a NO.A NO.B NO.2 NO.b
发现结果和我们预想的不一样,如果按照之前的知识,主函数中的print('NO.A')、print('NO.B')应该在两个线程执行完之后再执行,结果并非如此。
线程并没有等待主进程,怎么办呢?jion()可以解决,现在我们添加两个线程的jion():
import threading import time def func1(): print('NO.1') time.sleep(0.5) print('NO.2') def func2(): print('NO.a') time.sleep(0.5) print('NO.b') if __name__ == '__main__': f1 = threading.Thread(target=func1,name = 'f1') f2 = threading.Thread(target=func2,name = 'f2') f1.start() f2.start() f1.join() f1.join() print('NO.A') print('NO.B')
输出:
NO.1 NO.a NO.b NO.2 NO.A NO.B
这样就解决了这个问题,jion()可以让线程结束之后再执行下边的进程。
细心的同学又说了,线程f1和f2的执行毫无规律啊,要想变得有规律,怎么办呢?比如交叉运行?一人执行一步,谁也别抢?或者说有一个全局变量或内存空间,两个线程都需要访问,但是一个线程在访问的时候,另外一个线程不能访问,怎么操作?这就是操作系统中的生产者消费者的问题。解决方式考“锁”。例如A、B两个人都要进一个屋子,这个屋子不能让AB同时访问,可以给A、B每人一把锁。A进门的时候看门上有没有锁,没有锁才进去,进去的同时把门锁上,出来的时候再把锁打开。同理B。python中的threading模块已经写好了“锁”,下边上代码,先看没有锁是什么情况:
import threading import time # 定义线程运行函数 def ji(): for i in range(0,10,2): print(i) time.sleep(0.5) def ou():for i in range(1,10,2): print(i) time.sleep(0.5) if __name__ == '__main__': th = threading.Thread(target=ji) th2 = threading.Thread(target=ou) th.start() th2.start()
输出:
0
1
23
5
4
76
89
再看加锁之后:
代码:
import threading import time # 定义线程运行函数 def ou(): for i in range(0,10,2): lock_ou.acquire() print(i) lock_ji.release() time.sleep(0.5) def ji()for i in range(1,10,2): lock_ji.acquire() print(i) lock_ou.release() time.sleep(0.5) if __name__ == '__main__': lock_ji = threading.Lock() lock_ou = threading.Lock() th = threading.Thread(target=ji) th2 = threading.Thread(target=ou) lock_ji.acquire() th.start() th2.start()
输出:
0
1
2
3
4
5
6
7
8
9
代码功能上看起来很简单,一个线程输出奇数、一个线程输出偶数。如果没有“锁”,输出会很乱,一会奇数一会偶数,甚至连换行都来及输出,甚至以下输出两个换行符。加上“锁”之后,就符合我们的设想,一人一步,谁也别抢。
“锁”是通过threading.Lock()创建lock_ji、lock_ou对象,调用lock_ji.acquire()、 lock_ou.acquire()上锁,lock_ji.release()、lock_ou.release()释放锁,为了保证先输出偶数,在主函数中,有lock_ji.acquire()。有同学说了,为啥用两把锁呢?因为一把锁实现不了。。。不信的同学可以试一下,说白了还是《操作系统》中生产者、消费者的问题。。。
注意代码中锁的位置:
在def ji()中,先给ou上锁,执行完print之后再释放ji的锁;在def ji()中,先给ji上锁,执行完print之后再释放ou的锁。再联合主函数中的先给ji上锁,整体理解就没有问题了。
对应到操作系统中,print就是临界区资源。
补充:
利用消息队列接收进程返回值。
import time import threading from queue import Queue #计算0-9的平方和 def square_1(nb1,q): sum1 = 0 for i1 in range(nb1): sum1 +=i1**2 time.sleep(1) q.put(sum1) #计算0-9的立方和 def square_2(nb2,q): sum2 = 0 for i2 in range(nb2): sum2 += i2 ** 3 q.put(sum2) if __name__ == '__main__': q = Queue() s1 = threading.Thread(target=square_1,args=(10,q),name = 's1') s2 = threading.Thread(target=square_2,args=(10,q),name = 's2') s1.start() s2.start() s1.join() s2.join() print(q.get(),q.get())
输出:
2025 285
实例化对象q = Queue(),q.put(参数) 将参数加入到消息队列中,q.get()从消息队列中取出。遵循FIFO,即先进先出,第一个q.put()到消息队列的,第一个q.get()被得到。
在函数定义形参中,要加入q,在进程创建threading.Thread()中,参数args要加上q。完毕。。。
好了,python中的多线程技术就介绍到这里,下一步研究线程池的问题。不过我自己还是有疑问,如果不停的创建线程肯定不行,那如何结束线程呢?如何阻塞、挂起线程呢?又不得而知。。。