python 并发之多线程

python 并发之多线程
线程 - Thread

线程是一条时间线,假设这一条时间线是烧水喝茶的事件,可以先开始烧水,在等待水开的时候(IO操作)去刷一下杯子,本质是时间(CPU资源)的合理利用

  1. 启动和停止线程

    开启一个线程只需要调用Thread库函数,但是并发可不是简单的调库而已

    from threading import Thread 
    
    def func(param):
        time.sleep(1)
        return 
    a = Thread(target=func,args=(param,)) # 创建一个执行func函数的线程
    b = Thread(target=func,args=(param,))
    
    # 注意这其实启动了三个线程,一个主线程(这个程序的运行),两个子线程
    a.start() 
    b.start()
    
    • __init__(self, group=None, target=None, name=None,args=(), kwargs=None, *, daemon=None):
      
      • group:指定所创建的线程隶属于哪个线程组
      • target:指定所创建的线程要调度的目标方法;
      • args:以元组的方式,为 target 指定的方法传递参数,args=(param,)[^一定要这样]
      • kwargs:以字典的方式,为 target 指定的方法传递参数
      • daemon:指定所创建的线程是否为守护线程(后台线程,只要主线程执行完毕,不管子线程有没有结束,都会被强制退出)
    • .start() 开启线程

    • .join() 等待此线程运行结束,再运行主进程(普通情况下,主线程只要将子线程开启,就会开始运行接下来的部分,不管子线程有没有结束

    • .is_alive() 判断此线程是否结束

    • .getName() 获取此线程名字,若没有指定,则默认Thread-num

    • .setName() 设置线程名字

    • .setDaemon(True).daemon=True [^这是使用了setter的效果] 设置此线程为守护线程,注意,这个方法一定要用在.start()方法之前,活跃的线程无法再被设置为守护线程

    自定义线程

    继承式自定义线程就是重写了Thread的run方法

    class MyThread(Thread):
        """自定义线程开始提示"""
        def __init__(self):
            super().__init__()
        def run(self):
            print(f'{self.getName()}线程开始!')
    
    a = MyThread()
    a.start()
    # Thread-1线程开始!
    

    一次性开启多个线程

    • 自定义开启

      from threading import Thread
      import time
      
      class MyThread(Thread):
          """自定义线程开始提示"""
          def __init__(self):
              super().__init__()
          def run(self):
              print(f'{self.getName()}线程开始!')
              time.sleep(2) # 模拟IO操作
      
      start = time.time()
      pool = []
      for i in range(10):
          t = MyThread()
          pool.append(t)
          t.start()
      # 将开启的线程实例添加进列表,统一添加跟随主线程,
      for i in pool:
          i.join()
      
      print(f"耗时{time.time()-start}")
      # Thread-1线程开始!
      # Thread-2线程开始!
      # Thread-3线程开始!
      # Thread-4线程开始!
      # Thread-5线程开始!
      # Thread-6线程开始!
      # Thread-7线程开始!
      # Thread-8线程开始!
      # Thread-9线程开始!
      # Thread-10线程开始!
      # 耗时2.0030786991119385
      
    • 线程池

      线程池要导入的东西很多

      线程在被加入线程池就会自动执行

      线程池引入了 Future对象,理解的不够,以后补充

      • submit(fn, *args, **kwargs):将 fn 函数提交给线程池。*args 代表传给 fn 函数的参数,*kwargs 代表以关键字参数的形式为 fn 函数传入参数。
      • map(func, *iterables, timeout=None, chunksize=1):该函数类似于全局函数 map(func, *iterables),只是该函数将会启动多个线程,以异步方式立即对iterables执行 map 处理。
      • shutdown(wait=True):关闭线程池。
      import time
      from concurrent.futures import ThreadPoolExecutor, wait, ALL_COMPLETED
      
      def mythread(name):
          print(f'开始线程{name}')
          time.sleep(2)
      
      # 创建一个包含2条线程的线程池
      start = time.time()
      pool = ThreadPoolExecutor(max_workers=2) # 规定最大并发线程,超出会被阻塞,直到释放资源
      
      test1 = pool.submit(mythread,'参数1')
      test2 = pool.submit(mythread,'参数2') # 返回一个 Future 对象
      # 或者
      # pool.map(mythread,['参数一','参数2'])
      
      # 等待所有线程执行完毕,
      wait([test1,test2], return_when=ALL_COMPLETED)
      
      print(test1.done()) # 判断 test1 线程是否执行完毕
      print(f"耗时{time.time()-start}")
      # 关闭进程池
      # 也可以使用 with 自动管理上下文
      pool.shutdown() 
      

      Future 提供了如下方法:

      • cancel():取消该 Future 代表的线程任务。如果该任务正在执行,不可取消,则该方法返回 False;否则,程序会取消该任务,并返回 True。
      • cancelled():返回 Future 代表的线程任务是否被成功取消。
      • done():如果该 Future 代表的线程任务被成功取消或执行完成,则该方法返回 True。
      • result(timeout=None):获取该 Future 代表的线程任务最后返回的结果。如果 Future 代表的线程任务还未完成,该方法将会阻塞当前线程,其中 timeout 参数指定最多阻塞多少秒。
      • exception(timeout=None):获取该 Future 代表的线程任务所引发的异常。如果该任务成功完成,没有异常,则该方法返回 None。
      • add_done_callback(func):为该 Future 代表的线程任务注册一个“回调函数”,当该任务成功完成时,程序会自动触发该 func 函数。
      • wait(fs, timeout=None, return_when=ALL_COMPLETED) 是主线程阻塞,fs 是一个Future对象的序列,当all_completed时,结束等待
  2. 线程间通信

    线程间通信通常使用队列或队列来完成,使用线程队列有一个要注意的问题是,向队列中添加数据项时并不会复制此数据项,线程间通信实际上是在线程间传递对象引用。如果你担心对象的共享状态,那你最好只传递不可修改的数据结构(如:整型、字符串或者元组)或者一个对象的深拷贝

    • 生产者-消费者模型

      如果生产线程处理速度很快,而消费线程处理速度很慢,那么生产线程就必须等待消费线程处理完,才能继续生产数据。同样的道理,如果消费线程的处理能力大于生产线程,那么消费线程就必须等待生产线程

      • .get(block=True, timeout=None)出队,允许阻塞,当阻塞超时队列依然为空时,报错
      • .put(block=True, timeout=None) 入队,允许阻塞,当阻塞超时队列依然满时,报错
      • .join() 在队列中有未完成任务时阻塞,队列为空时,正常执行
      • .test_done() 执行一次 put 会让未完成任务 +1 ,但是执行 get 并不会让未完成任务 -1 ,需要使用 task_done 让未完成任务 -1 ,否则 join 就无法判断
        队列为空时执行会报错:ValueError: task_done() called too many times
      • .quize()获取当前队列数据量,.full() 判断当前队列是否已满,.empty() 判断当前队列是否为空,这三个方法都不可靠,因为在生产者-消费者模型中,一直在不停地入队出队,会导致不准确
      import queue
      import threading
      import time
      def producter():
          #生产者
          for i in range(3):
              q.put(i)
              print(f'初始化添加{i}')
          for i in range(3,10):
              time.sleep(1.5)
              q.put(i)
              print(f'生产者添加{i}')
      def customer():
          #消费者
          while True:
              temp = q.get()
              q.task_done()
              print(f"消费者取出{temp}")
              time.sleep(1)
      q = queue.Queue()
      t1 = threading.Thread(target=producter)
      t2 = threading.Thread(target=customer)
      t1.start()
      t2.start()
      q.join() # 在队列中有未完成任务时阻塞,队列为空时,正常执行
      print('这时队列空了')
      
    • 队列种类

      • FIFO 先进先出
      • LIFO 后进先出
      • Priority 优先队列
      • deque 双端队列(即双向列表)
  3. 线程的高阶
    • 锁与信号量

      当两个线程同时争抢操作一个资源时,就会发生不可预知的事情(线程不安全),对这个资源加锁,同一时间只能有一个线程获取到这个资源的使用权

      • 互斥锁

        • .acquire() 获得锁

        • .release() 释放锁

        • .locked() 检查是否获得锁

        import time
        import threading,Lock
        def run():
            lock.acquire()    #修改数据前加锁
            global num
            num +=1
            lock.release()    #修改完后释放
            
        lock=Lock()
        num=0
        threads = []
        for i in range(1000):
            t=threading.Thread(target=run)
            t.start()
            threads.append(t)
        for t in threads:  # 将开启的线程实例添加进列表,统一添加跟随主线程,
            t.join()
        print(num) 
        
      • 信号量

        互斥锁,在同一时间只允许一把锁获取资源,而信号量允许同一时间多把多访问资源

        • .acquire() 获得锁

        • .release() 释放锁

        from threading import BoundedSemaphore,Thread
        # BoundedSemaphore 内部维护了一个列表,每次 acquire 都计数加一
        def run():
            semaphore.acquire()    #修改数据前加锁
            global num
            num +=1
            semaphore.release()    #修改完后释放
            
        semaphore = BoundedSemaphore(5)
        
        • 主要用在数据库应用中,比如连接数据库的连接,限制同时连接的数量,如数据库连接池。这是线程不安全的。
    • 事件 Event 参考

      python线程的事件用于主线程控制其他线程的执行,事件是一个简单的线程同步对象,跟一把互斥锁很像

      事件处理的机制:全局定义了一个“Flag”,当flag值为“False”,那么event.wait()就会阻塞,当flag值为“True”,那么event.wait()便不再阻塞。

      • .set() 将flag设为True ,就像是获取锁
      • .clear() 将flag设为False,就像是释放锁
      • .is_set() 检查当前flag的bool
      • wait(timeout=None) 若 flag=True则继续运行,若为False则阻塞,知道flag=True
      #利用Event类模拟红绿灯
      import threading
      import time
      
      event = threading.Event()
      
      def lighter():
          count = 0
          event.set()     #初始值为绿灯
          while True:
              if 5 < count <=10 :
                  event.clear()  # 红灯,清除标志位
                  print("\33[41;1mred light is on...\033[0m")
              elif count > 10:
                  event.set()  # 绿灯,设置标志位
                  count = 0
              else:
                  print("\33[42;1mgreen light is on...\033[0m")
      
              time.sleep(1)
              count += 1
      
      def car(name):
          while True:
              if event.is_set():      #判断是否设置了标志位
                  print("[%s] running..."%name)
                  time.sleep(1)
              else:
                  print("[%s] sees red light,waiting..."%name)
                  event.wait()
                  print("[%s] green light is on,start going..."%name)
      
      light = threading.Thread(target=lighter,)
      light.start()
      
      car = threading.Thread(target=car,args=("MINI",))
      car.start()
      
参考
遗留的问题
  • 线程既然是一个时间线,为什么还需要锁?
  • python线程无法真正实现多线程,那为什么程序的运行速度确实加快了呢?

​ 线程能提高速度,但是python由于GIL多的存在,并不能发挥出现在机器多核CPU的优势,大意了,一个线程就有太多要整理,还有进程,协程,异步IO,

💃 现在是2021.6.14,最近黑客要杀疯了啊,又勒索了麦当劳,这一连串的动作,会不会带动网络安全这个行业的进步呢,有点期待

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值