Python多任务

多任务

  • 多任务含义:

    • 生活中:一边听歌,一边跳舞 ,开车手操控方向盘,眼睛看路,脚踩油门
    • 电脑:同时运行多个应用程序,例如qq,微信,浏览器同时在电脑上运行
  • 并发和并行

    • 并发:任务数大于核心数,通过操作系统调度算法实现多个任务“同时”执行,实际上是通过快速切换任务,看上去是一起执行的
    • 并行:任务数小于核心数,任务是真正一起执行
  • 进程:正在运行的一个程序我们可以说是一个进程 ,是系统进行资源分配和调用的独立单元,每 一个进程都有自己独立的内存空间和系统资源

  • 程序:运行起来的应用程序就称之为进程。也就是说当程序不运行的时候我们称之为程序, 当程序运行起来他就是一个进程。通俗的理解就是不运行的时候是程序,运行起来就是进程。 程序只有一个,但是进程可以有多个

  • 线程:线程是进程中的一条执行线路或者流程,程序执行的最小单位,线程是任务调度的最小单 位。

    • 由于进程是资源拥有者,创建、撤消与切换存在较大的内存开销,因此需要引入轻型进程 即线程,进程是资源分配的最小单位,线程是 CPU 调度的最小单位(程序真正执行的时候调 用的是线程)每一个进程中至少有一个线程
  • 进程和线程关系:一个进行可以有一个或者多个线程,但是一个线程只属于一个进程,一个进程中的多个线程是一种竞争关系

线程

?

#一边下载歌曲,一边听歌
import time
import threading

def listen():
    for i in range(1,6):
        print("正在听歌")
        time.sleep(1)

def down_load():
    for i in range(1,6):
        print("正在下载歌曲")
        time.sleep(1)

# down_load()
# sing()

if __name__ == ‘__main__‘:
    #创建线程
    t1 = threading.Thread(target=down_load)
    t2 = threading.Thread(target=listen)
    #执行线程
    t1.start()
    t2.start()
执行顺序:首先程序运行时,程序从上往下走,遇到if __name__ == ‘__main__‘:,创建了两条线程,我们称之为子线程,程序运行时的线程我们称之为主线程
然后子线程根据target=xxx,开始执行指定的函数
  • 线程注意点

    线程何时开启,何时结束
    
    1.子线程何时开启,何时运行
    	当调用start()时,开启线程,在运行线程的代码
    2.子线程何时结束
    	子线程把target指向的函数中的语句执行完毕,当前子线程结束
    3.查看线程数量
    	通过threading.enumerate()可以枚举当前运行的所有线程
    4.主线程何时结束
    	所有子线程执行完毕后,主线程才结束
  • 查看线程执行情况

    import threading
    import time
    
    def test1():
        for i in range(1,6):
            time.sleep(1)
            print("--子线程1--%d"%i)
            print("子线程1中查看线程情况:",threading.enumerate())
    
    def test2():
        for i in range(1,6):
            time.sleep(1)
            print("--子线程2--%d"%i)
            print("子线程2中查看线程情况:",threading.enumerate())
    def main():
        print("创建线程之前的线程情况:",threading.enumerate())
    
        #创建线程
        t1 = threading.Thread(target=test1)
        t2 = threading.Thread(target=test2)
        time.sleep(1)
        print("创建线程之后的线程情况:", threading.enumerate())
    
        #开启线程
        t1.start()
        t2.start()
        time.sleep(1)
        print("调用start之后的线程情况:", threading.enumerate())
        time.sleep(6)
        print("等待子线程执行结束后的线程情况:", threading.enumerate())
    if __name__ == ‘__main__‘:
        main()
  • args传参

    • 给函数传递参数,使用线程关键字args=( )进行传递参数,传递的参数是一个元组

      import time
      import threading
      
      def listen(num):
          for i in range(1,num):
              print(f"正在听歌--{i}")
              time.sleep(1)
      
      def down_load(num):
          for i in range(1,num):
              print(f"正在下载歌曲--{i}")
              time.sleep(1)
      def main():
          t1 = threading.Thread(target=down_load,args=(5,))
          t2 = threading.Thread(target=listen,args=(3,))
          t1.start()
          t2.start()
      
      if __name__ == ‘__main__‘:
          main()
  • join()方法

    • 功能:当前线程执行完之后,其他线程才会继续执行

      import time
      import threading
      
      def listen(num):
          for i in range(1,num):
              print(f"正在听歌--{i}")
              time.sleep(1)
      
      def down_load(num):
          for i in range(1,num):
              print(f"正在下载歌曲--{i}")
              time.sleep(1)
      def main():
          #下载完歌曲,才可以听歌
          t1 = threading.Thread(target=down_load,args=(5,))
          t2 = threading.Thread(target=listen,args=(5,))
          t1.start()
          t1.join() #在t1执行完之后,t2和主线程才执行
          t2.start()
          t2.join() # 在t1,t2执行完后,再继续执行主线程
      
      if __name__ == ‘__main__‘:
          main()
          print("程序执行结束了")

setDaemon() 方法

  • setDaemon()将当前线程设置成守护线程,来守护主线程

  • 当主线程结束后,守护线程也就结束,不管是否执行完成,即主线程结束后不等待守护线程,立即结束

  • 应用场景:qq多个聊天窗口,就可以设置为守护线程

    #一边下载歌曲,一边听歌
    import time
    import threading
    
    def listen():
        for i in range(1,6):
            print("正在听歌")
            time.sleep(1)
    def down_load():
        for i in range(1,6):
            print("正在下载歌曲")
            time.sleep(0.5)
    
    def main():
        #创建线程
        t1 = threading.Thread(target=down_load)
        t2 = threading.Thread(target=listen)
        # t1.setDaemon(True)
        t2.setDaemon(True)
        # 开启线程
        t1.start()
        t2.start()
    if __name__ == ‘__main__‘:
        main()
        print("程序执行结束了")
  • threading模块提供的方法

    • threading.currentThread():返回当前的线程变量

    • threading.enumerate():返回一个包含所有正在运行的线程list。正在运行的线程:启动后,结束前,不包括启动前和终止后的线程

    • threading.activeCount():返回正在运行的线程数量,与len(threading.enumerate())有相同的结果

    • 线程.getName():获取线程名称

    • 线程.setName():设置线程名称

    • 线程.is_alive():判断线程存活状态

使用继承方式开启线程

  • 定义一个类继承threading.Thread

    复写父类的run()方法

    import threading
    import time
    
    class MyThread(threading.Thread):
        def run(self):
            for i in range(3):
               time.sleep(1)
               msg = f"I‘m {self.name} @{i}"
               print(msg)
    
    def test1():
        for i in range(5):
            #创建线程
            t = MyThread()
            #开启线程
            t.start()
    
    def main():
        test1()
    main()

    线程之间共享全局变量

    import threading
    import time
    
    g_num = 100
    
    def work1():
        global g_num
        for i in range(3):
            g_num+=1
        print(f"--in work1,g_num is {g_num}")
    
    def work2():
        global g_num
        print(f"--in work2,g_num is {g_num}")
    
    print("创建线程之前g_num:",g_num)
    t1 = threading.Thread(target=work1)
    t1.start()
    time.sleep(1)
    t2 = threading.Thread(target=work2)
    t2.start()
    
    def work1(g_nums):
        g_nums.append(44)
        print(f"--in work1,g_num is {g_nums}")
    
    def work2(g_nums):
        time.sleep(0.5)
        print(f"--in work2,g_num is {g_nums}")
    
    g_nums = [11,22,33]
    t1 = threading.Thread(target=work1,args=(g_nums,))
    t1.start()
    t2 = threading.Thread(target=work2,args=(g_nums,))
    t2.start()
    • 总结:在一个进程内的所有线程共享全局变量,很方便在多个线程共享数据
    • 缺点:多线程对全局变量随意更改可能会造成全局变量的混乱(即线程非混乱)
  • 多线程开发可能会遇到的问题

    t1,t2两个线程都要对全局变量g_num(默认是0)进行加1运算,t1和t2都各对g_num 加10次,那么g_num的最终结果应该是20
    
    但是由于是多线程同时操作,可能会出现以下情况:
    
    在g_num=0时,t1取的g_num=0,此时系统把t1调为“sleeping”状态,把t2转换为“running”状态,t2也获得g_num=0
    然后t2对得到的值进行加1操作并赋值给g_num,使得g_num=1
    然后系统又把把t2转换为“sleeping”状态,把t1调为“running”状态,线程t1把之前获取到0加1后赋值给g_num,使用g_num=1
    这样导致虽然t1和t2都对g_num 加1,但结果仍是g_num=1
    
    
    g_num = 0
    
    def work1(num):
        global  g_num
        for i in range(num):
            g_num+=1 #g_num=g_num+1
        print(f"--in work1,g_num is {g_num}")
    
    def work2(num):
        global  g_num #work1执行10次执行切换到work2
        for i in range(num):
            g_num+=1 #g_num=g_num+1
        print(f"--in work2,g_num is {g_num}")
    print("创建线程之前g_num:",g_num)
    t1 = threading.Thread(target=work1,args=(100,))
    t1.start()
    t2 = threading.Thread(target=work2,args=(100,))
    t2.start()
    #如果多个线程对同一个全局变量进行操作,会出现资源竞争,从而数据不准确,即会遇到线程安全问题

同步和异步

  • 同步:同步就是协同步调,按预定的先后次序进行运行。同是协同的意思
  • 异步:一个异步过程调用发出后,调用者在没得到结果之前可以进行后续操作,同步和异步是对应的,两个线程要么同步,要不异步

互斥锁

  • 当多个线程几乎同时修改一个共享数据的时候,需要进行同步控制,线程同步能够保证多个 线程安全的访问竞争资源(全局内容),最简单的同步机制就是使用互斥锁

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

  • 使用过程:当某个线程要更改共享数据时,先将其锁定,此时资源的状态是“锁定”,其他线程不能访问,直到该线程释放资源,将资源改为“非锁定”状态,其他线程才可以再次锁定该资源。

    import threading
    #创建锁
    mutex = threading.Lock()
    
    #锁定
    mutex.acquire()
    
    #释放
    mutex.release()
    注意:如果这个锁之前没有上锁,那么acquire不会堵塞
    如果在调用acquire 对这个锁上锁之前,他已经被其他线程上了锁,此时acquire会堵塞,直到锁被解锁位置
    #使用互斥锁完成2个线程对同一全局变量各加100万次操作
    g_num = 0
    def work1(num):
        global  g_num
        for i in range(num):
            # 锁定
            mutex.acquire()
            g_num+=1
            # 释放
            mutex.release()
    
    
    def work2(num):
        global  g_num #work1执行10次执行切换到work2
        for i in range(num):
            # 锁定
            mutex.acquire()
            g_num += 1
            # 释放
            mutex.release()
    
    #创建锁
    mutex = threading.Lock()
    
    t1 = threading.Thread(target=work1,args=(1000000,))
    t1.start()
    t2 = threading.Thread(target=work2,args=(1000000,))
    t2.start()
    
    while len(threading.enumerate())!=1:
        time.sleep(1)
    print(f"--最终结果,g_num is {g_num}")
  • 死锁

    在多个线程共享资源的时候,如果两个线程分别占有一部分资源,并且同时等待对方的资源, 就会造成死锁现象,尽管死锁很少发生,但是一旦发生就会造成应用程序停止相应

    import time
    
    def test1():
        #lock1上锁
        lock1.acquire()
        print("--test1开始执行--")
        time.sleep(1)
        # lock2上锁
        lock2.acquire()
        print("--test1执行完成--")
        lock2.release() #lock2解锁
        lock1.release() #lock1解锁
    
    def test2():
        # lock2上锁
        lock2.acquire()
        print("--test2开始执行--")
        time.sleep(1)
        # lock1上锁
        lock1.acquire()
        print("--test2执行完成--")
        lock1.release()  # lock1解锁
        lock2.release()  # lock2解锁
    lock1 = threading.Lock()
    lock2 = threading.Lock()
    if __name__ == ‘__main__‘:
        t1 = threading.Thread(target=test1)
        t2 = threading.Thread(target=test2)
        t1.start()
        t2.start()
  • 总结 :在多线程开发中,全局变量是多个线程都共享的数据,而局部变量等是各自线程的,是非共 享的

生产者和消费者

  • 队列:Python 的 Queue 模块中提供了同步的、线程安全的队列类,包括 FIFO(先入先出)队列 Queue, LIFO(后入先出)队列 LifoQueue,和优先级队列 PriorityQueue。这些队列都实现了锁原语 (可以理解为原子操作,即要么不做,要么就做完),能够在多线程中直接使用。可以使用 队列来实现线程间的同步

  • from queue import Queue
    #1.创建队列对象
    # q = Queue(maxsize=3) #3表示最多存放3个数据
    #参数maxsize 是队列中允许的最大项,如果省略次参数,则无大小限制,
    #返回值q是队列对象
    # 2.put()方法 , 项队列中存放数据 如果q为空,此方法阻塞,直到队列可用为止
    # 3.get()方法 ,返回队列中的一个项目,如果队列为空,此方法阻塞,直到队列可用为止
    # 4.get_nowait(), 不等待,直接抛出异常
    # 5.full() , 如果q已满,返回True
    # 6.empty(),  如果q已空,返回True
    # 7.qsize(), q中数据的个数
    
    q = Queue(maxsize=3)
    q.put("张伟强")
    # print(q.empty())
    q.put("刘江")
    q.put("张威")
    print(q.full())
    # q.put("徐伟明")
    print(q.get())
    print(q.get())
    print(q.get())
    # print(q.get(timeout=3))
    # print(q.get_nowait())
    print(q.qsize())
  • 生产者与消费者模式

    • 生产者:生产数据的线程

    • 消费者:处理数据的线程

    • 目的:平衡生产者和消费者的工作能力来提高程序的整体处理数据的速度

      import threading
      import queue
      import time
      
      def producer(name): #生产者
          count = 1
          while True:
              print("%s生产了包子%d"%(name,count))
              q.put(count)
              count+=1
              time.sleep(0.2)
              print("包子总数",q.qsize())
      def costom(name): #消费者
          while True:
              print("%s吃了包子%d"%(name,q.get()))
              time.sleep(1)
      if __name__ == ‘__main__‘:
          q = queue.Queue(maxsize=3)
          t1 = threading.Thread(target=producer,args=("张大厨",))
          t2 = threading.Thread(target=costom, args=("吃货刘",))
          t3 = threading.Thread(target=costom, args=("吃货朱",))
          t1.start()
          t2.start()
          t3.start()
    • 为什么使用生产者和消费者模式

      • 在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当 中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理 完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必 须等待生产者。为了解决这个问题于是引入了生产者和消费者模式
    • 什么是生产者消费者模式

      • 生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼 此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力

相关推荐