python之网络编程-多线程

Posted yaoqi17

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了python之网络编程-多线程相关的知识,希望对你有一定的参考价值。

多线程

  1. 线程的理论知识

    1. 什么是线程

      当开启一个进程的时候:内存中开辟空间,加载资源与数据,调用CPU执行,可能还会使用这个空间的资源。

      定义:每个进程都有一个地址空间,而且默认就有一个控制线程。进程只是把资源集中到一起(进程可以认为是一个含有代码的空间),而线程才是CPU的执行单位。

      多线程:在一个进程中存在多个控制线程,多个控制线程共享该进程的地址空间。

    2. 线程vs进程

      • 开启多进程开销大,开启线程开销非常小
      • 开启多进程的速度慢,开启多线程速度快
      • 进程之间数据不能直接共享(通过队列可以),同一个进程下的线程之间的数据可以共享。

      总结:进程:划分空间,加载资源,静态的;线程:执行代码,执行能力,动态的

    3. 多线程的应用场景

      并发:一个CPU来回切换(线程间的切换)

      多进程并发:开启多个进程,每个进程里面的主进程执行任务

      多线程并发:开启1个(或多个)进程,每个进程里面多个线程执行任务

      当一个程序中包含三个不同的任务时,就可以使用多线程,由于每个线程之间可以直接共享数据。

  2. 开启线程的两种方式

    与开启多进程相似,只是引用模块为threading。

    • 方式一:

      from threading import Thread
      def task():
          print('打印子线程')
      if __name__ == '__main__':
          t = Thread(target=task)
          t.start()
          print('主线程')
      # 打印子线程
      # 主线程
    • 方式二

      from threading import Thread
      class MyThread(Thread):
          def run(self):
              print('打印子线程')
      if __name__ == '__main__':
          t = MyThread()
          t.start()
          print('主线程')
      # 打印子线程
      # 主线程
  3. 线程与进程对比

    1. 速度对比

      开启进程的速度比开启线程的速度慢得多

    2. pid

      线程的pid就是所属进程的pid

    3. 线程之间共享数据资源,进程之间理论上时隔离的

    4. 不同的进程直接就是竞争关系;而同一个进程的线程之间时合作关系

    为什么要用多线程?(如果多个任务共用一个地址空间那么必须在一个进程内开启多个线程)

    • 多线程共享一个进程的地址空间
    • 线程比进程更轻量级,线程更容易撤销,创建一个线程比创建一个进程要快10-100倍,在有大量线程需要动态和快速修改时,这一特性很有用
    • 若多个线程都是cpu密集型的那么并不能获得性能上的增强,但如果存在大量计算和大量的i/o处理,拥有多个线程允许这些活动彼此重叠运行,从而加快程序执行的速度
    • 再多CPU系统中,为了最大限度的利用多核,可以开启多个线程,比开进程开销小的多(并不适合python)
  4. 线程的其他方法

    # Thread实例对象的方法:
    isAlive()    # 判断线程是否是活动的
    getName()    # 返回线程名
    setName()    # 设置线程名
    # threading模块提供的一些方法:
    threading.currentThread()    # 返回当前的线程变量
    threading.enumerate()    # 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程
    threading.activeCount()  # 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果
  5. 守护线程

    无论是进程还是线程,都遵循守护线程(或进程)会等待主线程(或进程)结束后被终止。

    注意:

    • 对于主进程来说,运行结束指的是主进程代码运行完毕

      主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束

    • 对于主线程来说,运行结束指的是主线程所在的进程内的所有非守护线程全都运行完毕,主线程才算结束。

      主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束

    from threading import Thread
    import time
    def task():
        time.sleep(1)
        print('打印task')
    
    def task1():
        time.sleep(3)
        print('打印task1')
    if __name__ == '__main__':
        t1 = Thread(target=task)
        t2 = Thread(target=task1)
        print('主进程') # 必须等到task1执行完,主进程才能结束
    # 主进程
    # 打印task
    # 打印task1
  6. 互斥锁

    多线程的同步锁与多进程的同步锁是一个道理,就是多个线程抢占同一个数据(资源)时,我们要保证数据的安全,合理的顺序。

    # 不加锁抢占同一资源
    from threading import Thread
    import time
    x = 100
    
    def task():
        global x
        temp = x
        time.sleep(1)
        temp -= 1
        x = temp
    
    if __name__ == '__main__':
        t_l = []
        for i in range(100):
            t = Thread(target=task)
            t_l.append(t)
            t.start()
        for i in t_l:
            i.join()
        print(f'主线程x')
    # 主线程99
    '''
    所有的线程同一时间拿到的x都是100,执行结束全都是99
    '''

    加锁保证了数据安全

    from threading import Thread
    from threading import Lock
    import time
    x = 100
    def task(lock):
        lock.acquire()
        global x
        temp = x
        time.sleep(0.1)
        temp -= 1
        x = temp
        lock.release()
    if __name__ == '__main__':
        t_l = []
        lock = Lock()
        for i in range(100):
            t = Thread(target=task,args=(lock,))
            t_l.append(t)
            t.start()
        for i in t_l:
            i.join()
        print(f'主线程x')
    # 主进程0

    互斥锁与join的区别?

    互斥锁是随机强锁,公平的

    join是提前安排好顺序,虽然是串行,但不公平

  7. 死锁现象,递归锁

    所谓死锁:是指两个或两个以上的进程或线程在执行过程中,因为争夺资源而造成的一种互相等待的现象,若无外力作用,他们将无法推进下去,此时称系统处于死锁状态或系统产生了死锁,这些永远在相互等待的进程称为死锁进程。

    from threading import Thread
    from threading import Lock
    import time
    lock_A = Lock()
    lock_B = Lock()
    class MyThread(Thread):
        def run(self):
            self.f1()
            self.f2()
        def f1(self):
            lock_A.acquire()
            print(f'self.name拿到了锁A')
            lock_B.acquire()
            print(f'self.name拿到了锁B')
            lock_B.release()
            lock_A.release()
        def f2(self):
            lock_B.acquire()
            print(f'self.name拿到了锁B')
            time.sleep(1)
            lock_A.acquire()
            print(f'self.name拿到了锁A')
            lock_A.release()
            lock_B.release()
    if __name__ == '__main__':
        for i in range(1,4):
            t = MyThread()
            t.start()
    # Thread-1拿到了锁A
    # Thread-1拿到了锁B
    # Thread-1拿到了锁B
    # Thread-2拿到了锁A
    # 锁死了....

    遇到死锁现象可通过 递归锁解决。

    在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。

    这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁:

    from threading import Thread
    from threading import RLock
    import time
    lock_A = RLock()
    class MyThread(Thread):
        def run(self):
            self.f1()
            self.f2()
        def f1(self):
            lock_A.acquire()
            print(f'self.name拿到了锁A')
            lock_A.acquire()
            print(f'self.name拿到了锁B')
            lock_A.release()
            lock_A.release()
        def f2(self):
            lock_A.acquire()
            print(f'self.name拿到了锁B')
            time.sleep(1)
            lock_A.acquire()
            print(f'self.name拿到了锁A')
            lock_A.release()
            lock_A.release()
    if __name__ == '__main__':
        for i in range(1,4):
            t = MyThread()
            t.start()
    # Thread-1拿到了锁A
    # Thread-1拿到了锁B
    # Thread-1拿到了锁B
    # Thread-1拿到了锁A
    # Thread-2拿到了锁A
    # Thread-2拿到了锁B
    # Thread-2拿到了锁B
    # Thread-2拿到了锁A
    # Thread-3拿到了锁A
    # Thread-3拿到了锁B
    # Thread-3拿到了锁B
    # Thread-3拿到了锁A
    '''
    递归锁是一把锁,锁上有记录,只要acquire一次,锁上就计数1次, acquire2次,锁上就计数2次,
    release1次,减一,
    只要递归锁计数不为0,其他线程不能抢.
    '''
  8. 信号量Semaphore

    信号量允许多个线程或进程同时进入

    Semaphore管理一个内置计数器,当调用acquire()时内置计数器-1,当调用release()时内置计数器+1,当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。

    from threading import Thread
    from threading import current_thread
    from threading import Semaphore
    import time,random
    lock = Semaphore(4)
    def task():
        lock.acquire()
        print(f'current_thread.name正在连接')
        time.sleep(random.randint(1,3))
        lock.release()
    if __name__ == '__main__':
        for i in range(10):
            t = Thread(target=task)
            t.start()
    # Thread-1正在连接
    # Thread-2正在连接
    # Thread-3正在连接
    # Thread-4正在连接
    
    # Thread-5正在连接
    # Thread-6正在连接
    
    # Thread-7正在连接
    # Thread-8正在连接
    
    # Thread-9正在连接
    # Thread-10正在连接

以上是关于python之网络编程-多线程的主要内容,如果未能解决你的问题,请参考以下文章

python并发编程之多线程

python并发编程之多线程理论部分

Python并发编程之多线程

Python并发编程之多线程

Python并发编程之创建多线程的几种方法

python 并发编程之多线程