美文网首页
网络爬虫:多任务-进程、线程

网络爬虫:多任务-进程、线程

作者: 暴走的金坤酸奶味 | 来源:发表于2018-12-30 21:32 被阅读0次

    多线程

    多任务的概念

    什么叫“多任务”呢?简单地说,就是操作系统可以同时运行多个任务。打个比方,你一边看电影,一边聊QQ,一边在用Word赶作业,这就是多任务,这时至少同时有3个任务正在运行。

    单核CPU如何执行多任务? 多核CPU如何执行多任务?

    真正的并行执行多任务只能在多核CPU上实现,但是,由于任务数量远远多于CPU的核心数量,所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。

    注意:

    • 并发:同时发起,单个执行 指的是任务数多余cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)

    • 并行:同时发起,同时执行(4核,4个任务) 指的是任务数小于等于cpu核数,即任务真的是一起执行的

    线程:是cpu执行的一个基本单元,暂用的资源非常少,并且线程和线程之间的资源
    是共享的,线程是依赖于进程而存在的,多线程一般适用于I/O密集型操作,线程的
    执行是无序的

    在python语言中,并不能够正真意义上实现多线程,因为CPython解释器
    有一个全局的GIL解释器锁,来保证同一时刻只有一个线程在执行

    线程

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

    1. 使用threading模块

    单线程执行

    import time
    
    def saySorry():
        for i in range(5):
            print("亲爱的,我错了,我能吃饭了吗?")
            time.sleep(1)
    
    def saydo():
        for i in range(5):
            print("亲爱的,我错了,我给你按摩")
            time.sleep(1)
    
    if __name__ == "__main__":
            saySorry()
            saydo()
    

    多线程执行

    import threading
    import time
    
    def saySorry():
        for i in range(5):
            print("亲爱的,我错了,我能吃饭了吗?")
            time.sleep(1)
    
    def do():
        for i in range(5):
            print("亲爱的,我错了,我给你按摩")
            time.sleep(1)
    
    if __name__ == "__main__":
        td1 = threading.Thread(target=saySorry)
        td1.start() #启动线程,即让线程开始执行
        td2 = threading.Thread(target=do)
        td2.start() #启动线程,即让线程开始执行
    
    运行结果
    """
    亲爱的,我错了,我能吃饭了吗?
    亲爱的,我错了,我给你按摩
    亲爱的,我错了,我给你按摩
    亲爱的,我错了,我能吃饭了吗?
    亲爱的,我错了,我给你按摩
    亲爱的,我错了,我能吃饭了吗?
    亲爱的,我错了,我能吃饭了吗?
    亲爱的,我错了,我给你按摩
    亲爱的,我错了,我给你按摩
    亲爱的,我错了,我能吃饭了吗?
    """
    

    threading.Thread参数介绍

    • target:线程执行的函数

    • name:线程名称

    • args:为目标函数,传递参数,元组类型 另外:注意daemon参数

    • 如果某个子线程的daemon属性为False,主线程结束时会检测该子线程是否结束,如果该子线程还在运行,则主线程会等待它完成后再退出

    • 如果某个子线程的daemon属性为True,主线程运行结束时不对这个子线程进行检查而直接退出,同时所有daemon值为True的子线程将随主线程一起结束,而不论是否运行完成。

    • 属性daemon的值默认为False,如果需要修改,必须在调用start()方法启动线程之前进行设置

    
    创建线程
    thread_sub1 = Thread(target=download_image,name="下载线程")
    
    是否守护进程(在开启线程之前设置)
    daemon:False,在主线程结束的时候,会检测子线程任务是否结束,
    如果子线程中任务没有结束,则会让子线程正常结束任务
    daemon:True 在主线程结束的时候,会检测子线程任务是否结束,
    如果子线程中任务没有结束,则会让子线程跟随主线程一起结束
    
    thread_sub1.daemon = False
    
    开启线程
    thread_sub2.start()
    
    join():阻塞,等待子线程中的任务执行完毕后,再回到主线程中继续执行
    
    thread_sub1.join()
    

    线程-注意点

    线程执行代码的封装 通过上一小节,能够看出,通过使用threading模块能完成多任务的程序开发,为了让每个线程的封装性更完美,所以使用threading模块时,往往会定义一个新的子类class,只要继承threading.Thread就可以了,然后重写run方法

    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) #name属性中保存的是当前线程的名字
                print(msg)
    
    
    if __name__ == '__main__':
        t = MyThread()
        t.start()
    

    多线程-共享全局变量

    元组当做实参传递到线程中

    from threading import Thread
    import time
    
    def work1(nums):
        nums.append(44)
        print("----in work1---",nums)
    
    
    def work2(nums):
        #延时一会,保证t1线程中的事情做完
        time.sleep(1)
        print("----in work2---",nums)
    
    g_nums = [11,22,33]
    
    t1 = Thread(target=work1, args=(g_nums,))
    t1.start()
    
    t2 = Thread(target=work2, args=(g_nums,))
    t2.start()
    
    运行结果:
    """
    ----in work1--- [11, 22, 33, 44]
    ----in work2--- [11, 22, 33, 44]
    """
    
    

    缺点就是,线程是对全局变量随意遂改可能造成多线程之间对全局变量的混乱(即线程非安全)

    互斥锁(重点)

    当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制

    线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。

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

    某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。

    threading模块中定义了Lock类,可以方便的处理锁定

    # 创建锁
    mutex = threading.Lock()
    
    # 锁定
    mutex.acquire()
    
    # 释放
    mutex.release()
    
    • 如果这个锁之前是没有上锁的,那么acquire不会堵塞
    • 如果在调用acquire对这个锁上锁之前 它已经被 其他线程上了锁,那么此时acquire会堵塞,直到这个锁被解锁为止

    使用互斥锁完成2个线程对同一个全局变量各加100万次的操作

    import threading
    import time
    
    g_num = 0
    
    def test1(num):
        global g_num
        for i in range(num):
            mutex.acquire()  # 上锁
            g_num += 1
            mutex.release()  # 解锁
    
        print("---test1---g_num=%d"%g_num)
    
    def test2(num):
        global g_num
        for i in range(num):
            mutex.acquire()  # 上锁
            g_num += 1
            mutex.release()  # 解锁
    
        print("---test2---g_num=%d"%g_num)
    
    # 创建一个互斥锁
    # 默认是未上锁的状态
    mutex = threading.Lock()
    
    # 创建2个线程,让他们各自对g_num加1000000次
    p1 = threading.Thread(target=test1, args=(1000000,))
    p1.start()
    
    p2 = threading.Thread(target=test2, args=(1000000,))
    p2.start()
    
    p1.join()
    p2.join()
    
    print("2个线程对同一个全局变量操作之后的最终结果是:%s" % g_num)
    
    """
    ---test1---g_num=1935969
    ---test2---g_num=2000000
    2个线程对同一个全局变量操作之后的最终结果是:2000000
    """
    不加锁(每次结果都不一样)
    """
    ---test2---g_num=1222294
    ---test1---g_num=1616727
    2个线程对同一个全局变量操作之后的最终结果是:1616727
    """
    
    

    可以看到最后的结果,加入互斥锁后,其结果与预期相符。

    上锁解锁过程 当一个线程调用锁的acquire()方法获得锁时,锁就进入“locked”状态。

    每次只有一个线程可以获得锁。如果此时另一个线程试图获得这个锁,该线程就会变为“blocked”状态,称为“阻塞”,直到拥有锁的线程调用锁的release()方法释放锁之后,锁进入“unlocked”状态。

    线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行(running)状态

    锁的好处:

    • 确保了某段关键代码只能由一个线程从头到尾完整地执行

    锁的坏处:

    • 阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了

    • 由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁

    死锁问题

    在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。

    尽管死锁很少发生,但一旦发生就会造成应用的停止响应。下面看一个死锁的例子

    import threading
    import time
    
    class MyThread1(threading.Thread):
        def run(self):
            # 对mutexA上锁
            mutexA.acquire()
    
            # mutexA上锁后,延时1秒,等待另外那个线程 把mutexB上锁
            print(self.name+'----do1---up----')
            time.sleep(1)
    
            # 此时会堵塞,因为这个mutexB已经被另外的线程抢先上锁了
            mutexB.acquire()
            print(self.name+'----do1---down----')
            mutexB.release()
    
            # 对mutexA解锁
            mutexA.release()
    
    class MyThread2(threading.Thread):
        def run(self):
            # 对mutexB上锁
            mutexB.acquire()
    
            # mutexB上锁后,延时1秒,等待另外那个线程 把mutexA上锁
            print(self.name+'----do2---up----')
            time.sleep(1)
    
            # 此时会堵塞,因为这个mutexA已经被另外的线程抢先上锁了
            mutexA.acquire()
            print(self.name+'----do2---down----')
            mutexA.release()
    
            # 对mutexB解锁
            mutexB.release()
    
    mutexA = threading.Lock()
    mutexB = threading.Lock()
    
    if __name__ == '__main__':
        t1 = MyThread1()
        t2 = MyThread2()
        t1.start()
        t2.start()
    

    相关文章

      网友评论

          本文标题:网络爬虫:多任务-进程、线程

          本文链接:https://www.haomeiwen.com/subject/spdblqtx.html