zoukankan      html  css  js  c++  java
  • python多线程实现多任务

    #转载请联系

    1.什么是线程?

    进程是操作系统分配程序执行资源的单位,而线程是进程的一个实体,是CPU调度和分配的单位。一个进程肯定有一个主线程,我们可以在一个进程里创建多个线程来实现多任务。

    -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

    2.一个程序实现多任务的方法

    如上图所示,实现多任务,我们可以用几种方法。

    (1)在主进程里面开启多个子进程,主进程和多个子进程一起处理任务。

             有关多个进程实现多任务,可以参考我的博文:https://www.cnblogs.com/chichung/p/9532962.html

    (2)在主进程里开启多个子线程,主线程和多个子线程一起处理任务。

    (3)在主进程里开启多个协程,多个协程一起处理任务。

             有关多个协程实现多任务,可以参考我的博文:https://www.cnblogs.com/chichung/p/9544566.html

    注意:因为用多个线程一起处理任务,会产生线程安全问题,所以在开发中一般使用多进程+多协程来实现多任务。

    -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

    3.多线程的创建方式

    import threading
    p1 = threading.Thread(target=[函数名],args=([要传入函数的参数]))
    p1.start()  # 启动p1线程

    我们来模拟一下多线程实现多任务。

    假如你在用网易云音乐一边听歌一边下载。网易云音乐就是一个进程。假设网易云音乐内部程序是用多线程来实现多任务的,网易云音乐开两个子线程。一个用来缓存音乐,用于现在的播放。一个用来下载用户要下载的音乐的。这时候的代码框架是这样的:

    import threading
    import time
    
    def listen_music(name):
        while True:
            time.sleep(1)
            print(name,"正在播放音乐")
    
    
    def download_music(name):
        while True:
            time.sleep(2)
            print(name,"正在下载音乐")
    
    
    if __name__ == '__main__':
        p1 = threading.Thread(target=listen_music,args=("网易云音乐",))
        p2 = threading.Thread(target=download_music,args=("网易云音乐",))
        p1.start()
        p2.start()
    
    输出:
    网易云音乐 正在播放音乐
    网易云音乐 正在下载音乐
    网易云音乐 正在播放音乐
    网易云音乐 正在播放音乐
    网易云音乐 正在下载音乐
    网易云音乐 正在播放音乐
    网易云音乐 正在播放音乐
    网易云音乐 正在下载音乐
    网易云音乐 正在播放音乐
    网易云音乐 正在播放音乐
    网易云音乐 正在播放音乐
    ......
    ......

    观察上面的输出代码可以知道:

    1.CPU是按照时间片轮询的方式来执行子线程的。cpu内部会合理分配时间片。时间片到a程序的时候,a程序如果在休眠,就会自动切换到b程序。

    2.严谨来说,CPU在某个时间点,只在执行一个任务,但是由于CPU运行速度和切换速度快,因为看起来像多个任务在一起执行而已。

    除了上面的方法创建线程,还有另一种方法。可以编写一个类,继承threaing.Thread类,然后重写父类的run方法。

    import threading
    import time
    
    
    class MyThread(threading.Thread):
        def run(self):
            for i in range(5):
                time.sleep(1)
                print(self.name,i)
    
    
    t1 = MyThread()
    t2 = MyThread()
    t3 = MyThread()
    t1.start()
    t2.start()
    t3.start()
    
    输出:
    Thread-1 0
    Thread-3 0
    Thread-2 0
    Thread-1 1
    Thread-2 1
    Thread-3 1
    Thread-1 2
    Thread-3 2
    Thread-2 2
    Thread-1 3
    Thread-2 3
    Thread-3 3
    Thread-1 4
    Thread-2 4
    Thread-3 4

    运行时无序的,说明已经启用了多任务。

    -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

    4.线程何时开启,何时结束

    (1)子线程何时开启,何时运行
       当调用thread.start()时 开启线程,再运行线程的代码
    (2)子线程何时结束
       子线程把target指向的函数中的语句执行完毕后,或者线程中的run函数代码执行完毕后,立即结束当前子线程
    (3)查看当前线程数量
       通过threading.enumerate()可枚举当前运行的所有线程
    (4)主线程何时结束
       所有子线程执行完毕后,主线程才结束
    import threading
    import time
    
    
    def run():
        for i in range(5):
            time.sleep(1)
            print(i)
    
    
    t1 = threading.Thread(target=run)
    t1.start()
    print("我会在哪里出现")
    
    输出:
    我会在哪里出现
    0
    1
    2
    3
    4

    为什么主进程(主线程)的代码会先出现呢?因为CPU采用时间片轮询的方式,如果轮询到子线程,发现他要休眠1s,他会先去运行主线程。所以说CPU的时间片轮询方式可以保证CPU的最佳运行。

    那如果我想主进程输出的那句话运行在结尾呢?该怎么办呢?这时候就需要用到 join() 方法了。

    -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

    5.线程的 join() 方法

    import threading
    import time
    
    
    def run():
        for i in range(5):
            time.sleep(1)
            print(i)
    
    
    t1 = threading.Thread(target=run)
    t1.start()
    t1.join()  
    print("我会在哪里出现")
    
    输出:
    0
    1
    2
    3
    4
    我会在哪里出现

    join() 方法可以阻塞主进程(注意只能阻塞主进程,其他子进程是不能阻塞的),直到 t1 子线程执行完,再解阻塞。

    -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

    6.线程可以共享全局变量

    这个稍微实验下就可以知道了,所以这里不废话。

    -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

    7.多线程共享全局变量出现的问题

    我们开两个子线程,全局变量是0,我们每个线程对他自加1,每个线程加一百万次,这时候就会出现问题了,来,看代码:

     1 import threading
     2 import time
     3 
     4 num = 0
     5 
     6 def work1(loop):
     7     global num
     8     for i in range(loop):
     9         # 等价于 num += 1
    10         temp = num
    11         num = temp + 1
    12     print(num)
    13 
    14 
    15 def work2(loop):
    16     global num
    17     for i in range(loop):
    18         # 等价于 num += 1
    19         temp = num
    20         num = temp + 1
    21     print(num)
    22 
    23 
    24 if __name__ == '__main__':
    25     t1 = threading.Thread(target=work1,args=(1000000,))
    26     t2 = threading.Thread(target=work2, args=(1000000,))
    27     t1.start()
    28     t2.start()
    29 
    30     while len(threading.enumerate()) != 1:
    31         time.sleep(1)
    32     print(num)
    33 
    34 输出:
    35 1459526  # 第一个子线程结束后全局变量一共加到这个数
    36 1588806  # 第二个子线程结束后全局变量一共加到这个数
    37 1588806  # 两个线程都结束后,全局变量一共加到这个数

    奇怪了,我不是每个线程都自加一百万次吗?照理来说,应该最后的结果是200万才对的呀。问题出在哪里呢?

    我们知道CPU是采用时间片轮询的方式进行几个线程的执行。

    假设我CPU先轮询到work1(),num此时为100,在我运行到第10行时,时间结束了!此时,赋值了,但是还没有自加!即temp=100,num=100。

    然后,时间片轮询到了work2(),进行赋值自加。num=101了。

    又回到work1()的断点处,num=temp+1,temp=100,所以num=101。

    就这样!num少了一次自加!

    在次数多了之后,这样的错误积累在一起,结果只得到158806!

    这就是线程安全问题

    -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

    8.互斥锁可以弥补部分线程安全问题。(互斥锁和GIL锁是不一样的东西!

    当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制

    线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。

    互斥锁为资源引入一个状态:锁定/非锁定

    某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。

    互斥锁有三个常用步骤

    lock = threading.Lock()  # 取得锁
    lock.acquire()  # 上锁
    lock.release()  # 解锁

    下面让我们用互斥锁来解决上面例子的线程安全问题。

    import threading
    import time
    
    num = 0
    lock = threading.Lock()  # 取得锁
    def work1(loop):
        global num
        for i in range(loop):
            # 等价于 num += 1
            lock.acquire()  # 上锁
            temp = num
            num = temp + 1
            lock.release()  # 解锁
        print(num)
    
    
    def work2(loop):
        global num
        for i in range(loop):
            # 等价于 num += 1
            lock.acquire()  # 上锁
            temp = num
            num = temp + 1
            lock.release()  # 解锁
        print(num)
    
    
    if __name__ == '__main__':
        t1 = threading.Thread(target=work1,args=(1000000,))
        t2 = threading.Thread(target=work2, args=(1000000,))
        t1.start()
        t2.start()
    
        while len(threading.enumerate()) != 1:
            time.sleep(1)
        print(num)
    
    输出:
    1945267  # 第一个子线程结束后全局变量一共加到这个数
    2000000  # 第二个子线程结束后全局变量一共加到这个数
    2000000  # 两个线程都结束后,全局变量一共加到这个数
  • 相关阅读:
    HTTP Status 500
    响应式导航页面
    Http状态码的种类及含义
    实时监听输入框值变化的完美方案:oninput & onpropertychange
    运动框架
    图片循环轮播
    html页面加载和解析流程
    js和jquery实现tab选项卡
    js遇到这样基础题,看你能不能作对呢
    sql统计重复数据
  • 原文地址:https://www.cnblogs.com/chichung/p/9566734.html
Copyright © 2011-2022 走看看