首页 > 技术文章 > 多线程

xiongsheng 2019-09-18 21:53 原文

多线程

1.今日概要

  • 爬虫
  • 线程的概念及与进程的区别?
  • 多线程的应用
  • 线程安全(单例模式)
  • GIL(全局解释器锁)

2. 内容回顾&补充

  1. 面向对象继承

    class Thread(object):
    	def __init__(self):
    		pass
    	def start(self):
    		self.run()
    		
    	def run(self):
    		print('thread.run')
    	
    obj = Thread()
    obj.start()
    
    -----------------------------------
    class MyThread(Thread):
    	def run():
    		print('mythread.run')
    	
    	
    obj = MyThread()
    obj.start()
    
  2. 面向对象的上下文管理

    class Foo:
    	
    	def __enter__(...):
    		pass
    	
    	def __exit__(...):
    		pass 
    		
    lock = Foo()
    with lock:
    	pass 
    
  3. 单例模式

    class Foo:
        instance = None
    
        def __init__(self, name):
            self.name = name
        def __new__(cls, *args, **kwargs):
            # 返回空对象
            if cls.instance:
                return cls.instance
            cls.instance = object.__new__(cls)
            return cls.instance
    
    obj1 = Foo('日魔')
    obj2 = Foo('SB')
    
    print(obj1,obj2)
    
  4. socketserver的实现原理

    内部创建进程或线程实现并发.
    

3. 今日详细

3.1 爬虫下载图片的案例

  • 安装和使用

    1. 安装第三方模块
    	pip3 install requests
    
    2. 使用
    	import requests
    	url = 'https://www3.autoimg.cn/newsdfs/g26/M02/35/A9/120x90_0_autohomecar__ChsEe12AXQ6AOOH_AAFocMs8nzU621.jpg'
    
    	# python伪造浏览器向地址发送请求
    	rep = requests.get(url)
    
    	# 请求返回回来的字节
    	# print(rep.content)
    
    	with open('xxxxxx.jpg',mode='wb') as f:
    		f.write(rep.content)
    
  • 下载图片示例

    import requests
    
    url_list = [
    	'https://www3.autoimg.cn/newsdfs/g26/M02/35/A9/120x90_0_autohomecar__ChsEe12AXQ6AOOH_AAFocMs8nzU621.jpg',
    	'https://www2.autoimg.cn/newsdfs/g30/M01/3C/E2/120x90_0_autohomecar__ChcCSV2BBICAUntfAADjJFd6800429.jpg',
    	'https://www3.autoimg.cn/newsdfs/g26/M0B/3C/65/120x90_0_autohomecar__ChcCP12BFCmAIO83AAGq7vK0sGY193.jpg'
    ]
    
    
    for url in url_list:
    	ret = requests.get(url)
    	file_name = url.rsplit('/',maxsplit=1)[-1]
    	with open(file_name,mode='wb') as f:
    		# 下载小文件
    		# f.write(ret.content)
    
    		# 下载大文件
    		for chunk in ret.iter_content():
    			f.write(chunk)
    

注意:以上代码是基于串行来实现.

3.2 线程的概念&与进程的区别?

形象的关系

  • 工厂 -> 应用程序
  • 车间 -> 进程
  • 工人 -> 线程

进程和线程的区别?

进程是计算机资源分配的最小单位.
线程是计算机中可以被cpu调度的最小单位.
一个进程中可以有多个线程,同一个进程中的线程可以共享此进程中的资源,一个进程中至少有一个线程(一个应用程序中至少有一个进程)

在Python中因为有GIL锁,他同.....


默认进程之间无法进行资源共享,如果主要想要通讯可以基于:文件/网络/Queue.

3.3 多线程的应用

  • 快速应用

    import threading
    
    def task(arg):
    	pass
    # 实例化一个线程对象
    t = threading.Thread(target=task,args=('xxx',))
    # 将线程提交给cpu
    t.start()
    
    import threading
    def task(arg):
    	ret = requests.get(arg)
    	file_name = arg.rsplit('/', maxsplit=1)[-1]
    	with open(file_name, mode='wb') as f:
    		f.write(ret.content)
    		
    for url in url_list:
    	# 实例化一个线程对象
    	t = threading.Thread(target=task,args=(url,))
    	# 将线程提交给cpu
    	t.start()
    
    
  • 常见方法

    • t.start() ,将线程提交给cpu,由cpu来进行调度.

    • t.join() , 等待

      import threading
      
      
      # 示例1
      """
      loop = 10000000
      number = 0
      
      def _add(count):
          global number
          for i in range(count):
              number += 1
      
      
      t = threading.Thread(target=_add,args=(loop,))
      t.start()
      
      t.join()
      print(number)
      """
      
      # 示例2
      """
      loop = 10000000
      number = 0
      
      def _add(count):
          global number
          for i in range(count):
              number += 1
      
      def _sub(count):
          global number
          for i in range(count):
              number -= 1
      
      t1 = threading.Thread(target=_add,args=(loop,))
      t2 = threading.Thread(target=_sub,args=(loop,))
      t1.start()
      t2.start()
      
      print(number)
      """
      
      
      # 示例3
      """
      loop = 10000000
      number = 0
      
      def _add(count):
          global number
          for i in range(count):
              number += 1
      
      def _sub(count):
          global number
          for i in range(count):
              number -= 1
      
      t1 = threading.Thread(target=_add,args=(loop,))
      t2 = threading.Thread(target=_sub,args=(loop,))
      t1.start()
      t2.start()
      t1.join() # t1线程执行完毕,才继续往后走
      t2.join() # t2线程执行完毕,才继续往后走
      
      print(number)
      """
      
      # 示例4
      """
      loop = 10000000
      number = 0
      
      def _add(count):
          global number
          for i in range(count):
              number += 1
      
      def _sub(count):
          global number
          for i in range(count):
              number -= 1
      
      t1 = threading.Thread(target=_add,args=(loop,))
      t2 = threading.Thread(target=_sub,args=(loop,))
      t1.start()
      t1.join() # t1线程执行完毕,才继续往后走
      t2.start()
      t2.join() # t2线程执行完毕,才继续往后走
      
      print(number)
      """
      
      
    • t.setDaemon() ,设置成为守护线程

      import threading
      import time
      
      def task(arg):
          time.sleep(5)
          print('任务')
      
      
      t = threading.Thread(target=task,args=(11,))
      t.setDaemon(True)
      t.start()
      
      print('END')
      
      
    • 线程名称的设置和获取

      import threading
      
      def task(arg):
          # 获取当前执行此代码的线程
          name = threading.current_thread().getName()
          print(name)
      
      for i in range(10):
          t = threading.Thread(target=task,args=(11,))
          t.setName('日魔-%s' %i )
          t.start()
      
      
    • run() , 自定义线程时,cpu调度执行的方法

      class RiMo(threading.Thread):
          def run(self):
              print('执行此线程',self._args)
      
      obj = RiMo(args=(100,))
      obj.start()
      
      

练习题: 基于socket 和 多线程实现类似于socketserver模块的功能.

import socket
import threading


def task(connect,address):
    pass


server = socket.socket()
server.bind(('127.0.0.1',9000))
server.listen(5)
while True:
    conn,addr = server.accept()
    # 处理用户请求
    t = threading.Thread(target=task,args=(conn,addr,))
    t.start()

1568779047375

3.4 线程安全

多个线程同时去操作一个"东西",不要存在数据混乱.

线程安全: logging模块 / 列表

线程不安全: 自己做文件操作 / 同时修改一个数字

使用锁来保证数据安全,来了多个线程,使用锁让他们排队,逐一执行.

  • Lock

    import threading
    import time
    
    
    num = 0
    # 线程锁
    lock = threading.Lock()
    
    
    def task():
        global num
        # # 申请锁
        # lock.acquire()
        # num += 1
        # time.sleep(0.2)
        # print(num)
        # # 释放锁
        # lock.release()
    
        with lock:
            num += 1
            time.sleep(0.2)
            print(num)
    
    for i in range(10):
        t = threading.Thread(target=task)
        t.start()
    
    
  • RLock,递归锁支持上多次锁

    import threading
    import time
    
    
    num = 0
    # 线程锁
    lock = threading.RLock()
    
    
    def task():
        global num
        # 申请锁
        lock.acquire()
        num += 1
        lock.acquire()
        time.sleep(0.2)
        print(num)
        # 释放锁
        lock.release()
        lock.release()
    
    
    for i in range(10):
        t = threading.Thread(target=task)
        t.start()
    
    

练习题: 基于线程锁完成一个单例模式.

import threading
import time
class Singleton:
    instance = None
    lock = threading.RLock()

    def __init__(self, name):
        self.name = name
    def __new__(cls, *args, **kwargs):

        if cls.instance:
            return cls.instance
        with cls.lock:
            if cls.instance:
                return cls.instance
            time.sleep(0.1)
            cls.instance = object.__new__(cls)
        return cls.instance

def task():
    obj = Singleton('x')
    print(obj)

for i in range(10):
    t = threading.Thread(target=task)
    t.start()

# 执行1000行代码

data = Singleton('asdfasdf')
print(data)

3.5 GIL

GIL,全局解释器锁.

同一时刻保证一个进程中只有一个线程可以被cpu调度,所以在使用Python开发时要注意:
	计算密集型,用多进程.
	IO密集型,用多线程. 

  • Python中如果创建多现场无法应用计算机的多核优势.

4.重点总结

  • 初识爬虫
  • 单例模式 ,重点面试 (需要默写)
    • 为什么要加锁?
    • 为什么要做判断?
  • 进程和线程的区别? ,重点.面试
  • GIL锁, 重点面试
  • 线程的常用功能: start/join , 重点

推荐阅读