zoukankan      html  css  js  c++  java
  • 异步编程(二)----------------多线程

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

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

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

  • 相关阅读:
    数据库(六):多表关系
    数据库(五):约束关系
    数据库(四):数据类型
    数据库(三):存储引擎
    数据库(二):初识sql语句
    数据库(一):初识数据库
    番外:socketserver用法
    ~~并发编程(十六):协程理论~~
    ~~并发编程(十五):进程池线程池~~
    ~~并发编程(十四):Queue~~
  • 原文地址:https://www.cnblogs.com/lgwdx/p/14285942.html
Copyright © 2011-2022 走看看