异步编程(二)----------------多线程

多线程和线程池并不是一回事

多线程是根据实际情况建立多个线程,线程池是一次性创建多个线程。

简单来说,目前有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中的多线程技术就介绍到这里,下一步研究线程池的问题。不过我自己还是有疑问,如果不停的创建线程肯定不行,那如何结束线程呢?如何阻塞、挂起线程呢?又不得而知。。。

 

posted @ 2021-01-16 14:46  理工—王栋轩  阅读(132)  评论(0编辑  收藏  举报