网络编程(3) - 多任务-线程

109 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

3.1,多任务的介绍

  1. 现实生活中很多事情是同时进行的,唱歌跳舞,手掌握方向盘,脚踩油门

  2. 多任务代码案例

    import time
    import threading
    
    def sing():
        for i in range(5):
            print('正在唱歌')
            time.sleep(1)
            
    def dance():
        for i in range(5):
            print('正在跳舞')
            time.sleep(1)
            
    def main():
        t1 = threading.Thread(target=sing)
        t2 = threading.Thread(target=dance)
        t1.start()
        t2.start()
    

    运行结果:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AuF3jFI5-1592062197135)(assets/.png)]

  3. 并发和并行

    1. 并发(运行的程序大于cpu核数):假的多任务,类似单片机的动态扫描,如果cpu只有1个核,他是这样运行的:先给第一个程序运行0.001秒,让第二个程序0.001秒,第三个0.001秒,像这样循环,达到肉眼看不到的速度,就变成了多任务

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SUycrLa2-1592062197139)(assets/.png)]

    2. 并行(运行的程序小于cpu核数):真正的多任务,一个核处理一个程序

3.2,线程(重点)

python的 thread 模块时比较底层的模块,python的 threading 模块时对thread做了一些包装的,可以更加方便的被使用

  1. 使用threading模块

    单线程执行
     # encoding = utf-8
     import time
     
     def sayHi():
     	print('hello, 今天天气真好')
         time.sleep(1)
         
     if __name__ == '__main__':
         for _ in range(5):
             sayHi()
    
    多线程执行
     # encoding = utf-8
     import time
     import threading
     
     def sayHi():
     	print('hello, 今天天气真好')
     	time.sleep(1)
         
     if __name__ == '__main__':
         for _ in range(5):
             t1 = threading(target=sayHi)	# 创建一条子线程
             t1.start()
    
     import threading
     import time
     
     
     class MyThread(threading.Thread):
         def run(self):
             for i in range(3):
                 time.sleep(1)
                 msg = 'I\'m ' + self.name + '@' + str(i)
                 print(msg)
     
         def login(self):
             pass
     
         def register(self):
             pass
     
     if __name__ == '__main__':
         t = MyThread()
         t1 = MyThread()
         t.start()
         t1.start()
    
    多线程执行流程:

    ​ 主线程执行,执行到 t1.start() 的时候,开辟一条子线程,子线程去执行 t1 里面的任务,主线程不管,继续执行主线程,又碰到 t1.start() ,又开辟一条子线程,去执行 t2 里面的任务,等到主线程执行结束,看看有没有子线程在执行,有的话等待执行,没有结束程序

  2. 查看当前进程的数量

     def main():
         t1 = threading.Thread(target=sing)
         t2 = threading.Thread(target=dance)
         t1.start()
         t2.start()
     
         while True:
             threading_length = len(threading.enumerate())
             print('当前运行的程序有 {} 个'.format(threading_length))
             if threading_length <= 1:
                 break
             sleep(0.5)
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KtfNKzfT-1592062197141)(assets/.png)]

3.2,线程资源冲突

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FNkNqTbp-1592062197144)(assets/.png)]

3.2,线程锁

  • threading.Lock() 线程锁

      from threading import Thread, Lock  # 导入Lock锁模块
      
      x = 0
      n = 100000
      lock = Lock()
      
      
      def hello(n):
          global x
          for i in range(n):
              lock.acquire()	# 线程上锁,其他线程不能访问当前线程使用的变量
              x += 1
              lock.release()	# 线程解锁,变量x可以被其他线程访问
      
      def hello1(n):
          global x
          for i in range(n):
              with lock:		# 第二种方式:使用上下文管理,自动上锁解锁
                  a -= 1
      
      i = Thread(target=hello, args=(n,))
      d = Thread(target=hello1, args=(n,))
      
      i.start()
      d.start()
      i.join()
      d.join()
    
  • threading.RLock() 可重入的锁

    from threading import Thread, RLock  
    # 在同一个线程里面,可以连续调用多次acquire,但一定要注意acquire的次数要和release的次数相同
    
    x = 0
    n = 100000
    lock = RLock()
    
    
    def hello(n):
        global x
        for i in range(n):
            lock.acquire()	# 线程上锁,其他线程不能访问当前线程使用的变量
            lock.acquire()
            lock.acquire()
            do_something()
            x += 1
            lock.release()	# 线程解锁,变量x可以被其他线程访问
            lock.release()
            lock.release()
            
    def do_something(lock):
        lock.acquire()
        # do something
        lock.release()
    	
    def hello1(n):
        global x
        for i in range(n):
            with lock:		# 第二种方式:使用上下文管理,自动上锁解锁
                do_something()
                a -= 1
    
    i = Thread(target=hello, args=(n,))
    d = Thread(target=hello1, args=(n,))
    
    i.start()
    d.start()
    i.join()
    d.join()
    

3.3,死锁

如果A锁已经锁上,后面的程序想要用A锁就必须等待A锁释放

如果B锁已经锁上,后面的程序想要用B锁就必须等待B锁释放

如果a程序锁了A锁,b程序锁了B锁,a程序想要用B锁,等待。b程序想要用A锁,等待。就变成了死锁

下面的程序就说明了这个问题

  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i0bPllf5-1592062197148)(assets/.png)]

3.4,condition锁

条件变量,用于复杂的线程间同步

  1. condition 的启动顺序很重要
  2. 在调用 with cond 之后才能调用 wait 方法或者 notify 方法
  3. condition 有两层锁,一把底层锁会在线程调用了 wait 方法的时候释放,上面的锁会在每次调用 wait 的时候分配一把锁放入 cond 的等待队列中,等待 notify 唤醒
from threading import Condition, Thread

class XiaoAi(Thread):
    def __init__(self, cond):
        super().__init__(name="小爱")
        self.cond = cond

    def run(self):
        with self.cond:
            self.cond.wait()
            print('{} : 你好呀,天猫精灵'.format(self.name))
            self.cond.notify()

            self.cond.wait()
            print('{} : 我们来对古诗把'.format(self.name))
            self.cond.notify()

class TianMao(Thread):
    def __init__(self, cond):
        super().__init__(name="天猫精灵")
        self.cond = cond

    def run(self):
        with self.cond:
            print('{} : 小爱同学你好'.format(self.name))
            self.cond.notify()

            self.cond.wait()
            print('{} : 好啊'.format(self.name))
            self.cond.notify()

            self.cond.wait()
            print('{} : gun'.format(self.name))



if __name__ == '__main__':
    cond = Condition()
    xiaoai = XiaoAi(cond)
    tianmao = TianMao(cond)

    xiaoai.start()
    tianmao.start()

3.5,信号量Semaphore

  1. 是用于控制进入数量的锁
  2. 文件,读,写一般只是用于一个线程写,读可以允许多个
  3. 做爬虫,控制并发数
class HtmlSpider(Thread):
    def __init__(self, url, sem):
        super().__init__()
        self.url = url
        self.sem = sem

    def run(self):
        time.sleep(2)
        print('got html text success')
        self.sem.release()


class UrlProducer(Thread):
    def __init__(self, sem):
        super().__init__()
        self.sem = sem

    def run(self):
        for i in range(20):
            self.sem.acquire()
            html_thread = HtmlSpider('https://www.baidu.com/{}'.format(i), self.sem)
            html_thread.start()


if __name__ == '__main__':
    sem = Semaphore(3)
    s_t = time.time()
    url_producer = UrlProducer(sem)
    url_producer.start()
    url_producer.join()

3.6,多线程udp聊天器

  • 思路:让接收和发送分别使用两条子线程,达到收发同步的目的

     # !/usr/bin/env python
     # _*_ coding:utf-8 _*_
     # author:满怀心 2019/8/3 10:26
     """
     # code is far away from bugs with the god animal protecting
         I love animals. They taste delicious.
                   ┏┓      ┏┓
                 ┏┛┻━━━┛┻┓
                 ┃      ☃      ┃
                 ┃  ┳┛  ┗┳  ┃
                 ┃      ┻      ┃
                 ┗━┓      ┏━┛
                     ┃      ┗━━━┓
                     ┃  神兽保佑    ┣┓
                     ┃ 永无BUG!   ┏┛
                     ┗┓┓┏━┳┓┏┛
                       ┃┫┫  ┃┫┫
                       ┗┻┛  ┗┻┛
     """
     import argparse
     from socket import *
     import threading
     
     
     def send(udp_socket, dest_addr):
         while True:
             user_input = input('请输入想要发送的数据:\t')
             udp_socket.sendto(user_input.encode('utf-8'), dest_addr)
     
     
     def recv(udp_socket):
         while True:
             recv_data = udp_socket.recvfrom(1024)
             print('\n【{}】 -- 数据来自 {}:{}'.format(recv_data[0].decode('utf-8'), recv_data[1][0], recv_data[1][1]))
     
     
     def main(host, port):
         """主函数"""
         # 1. 创建udp套接字
         udp_socket = socket(AF_INET, SOCK_DGRAM)
     
         # 2. 绑定端口
         udp_socket.bind(('', 5566))
     
         # 3. 确定对方的地址
         dest_addr = (host, port)
     
         # 4. 发送数据+接收数据
         t_send = threading.Thread(target=send, args=(udp_socket, dest_addr))
         t_recv = threading.Thread(target=recv, args=(udp_socket,))
     
         # 5. 执行多线程
         t_send.start()
         t_recv.start()
     
     
     if __name__ == '__main__':
         parser = argparse.ArgumentParser()
         parser.add_argument('--host', action='store', dest='host', required=False)
         parser.add_argument('--port', action='store', dest='port', type=int, required=False)
         given_args = parser.parse_args()
         host = given_args.host
         port = given_args.port
         main(host, port)