10-多线程+GIL锁

目录

认识GIL 

多线程

 线程间通信


认识GIL 

# GIL:global interpreter lock
# python中的一个线程对应于C语言中的一个线程
# python在最初的时候,就给程序加了一把GIL锁,GIL使得同一时刻只有一个线程运行在一个cpu上执行字节码,无法将多个线程映射到多个cpu上
# GIL无法利用多核优势:如果我们运行一个进程,这个进程里的线程就只能在一个cpu里运行,而不能在其他cpu上运行
# 相比C/C++/JAVA,python确实慢,在一些特殊场景下,Python比C++慢100~200倍
# python速度慢的原因:
    # 1、动态类型语言,边解释边执行,而且要随时检查数据类型
    # 2、GIL导致

# GIL:全局解释器锁
# python中对象的管理,是使用引用计数器进行的,当引用数为0则释放对象。撤销引用分为两步操作,1、是引用数减1,2是判断引用数为0则释放对象
# 比如线程A和线程B同时引用对象num,则num的引用数为2
# 当线程A和线程B都想撤销对num的引用时,A先撤销,让引用数减1,B紧接着也撤销,引用数再次减1,这时引用数为0,B这边就提前释放对象。
# 但紧接着A要判断该对象的引用数并决定要不要释放时,对象已经不存在了。程序就要报错,或是祸乱其他数据了

# 规避GIL带来的限制
# 1、多线程threading机制依然是有用的,用于IO密集型计算
    # 因为在IO期间,线程会释放GIL,实现CPU和IO的并行,因此多线程用于IO密集型计算,依然可以大幅度提升速度
    # 但是多线程用于CPU密集型计算时,只会更加拖慢速度
# 2、使用multiprocessing的多进程机制实现并行计算,利用多核CPU优势
# 使用dis.dis获取字节码
import dis
def add(a):
    a = a + 1
print(dis.dis(add))
运行结果如下
 6           0 LOAD_FAST                0 (a)
              2 LOAD_CONST               1 (1)
              4 BINARY_ADD
              6 STORE_FAST               0 (a)
              8 LOAD_CONST               0 (None)
             10 RETURN_VALUE
None
# GIL是根据执行的字节码行数以及时间片、或是遇到io操作会主动释放,例如运行了100行字节码后,就释放GIL
# 多线程在io操作频繁的时候,非常适用,因为GIL遇到io操作会主动释放

多线程

# 没有加锁时,多线程对同一个数据操作会出现问题
import threading
total = 0
def add():
    global total
    for i in range(1000000):
        total += 1

def desc():
    global total
    for i in range(1000000):
        total -= 1
thread1 = threading.Thread(target = add)
thread2 = threading.Thread(target = desc)
thread1.start()
thread2.start()

thread1.join()
thread2.join()
print(total) # 每次运行结果都不一样
# 操作系统能够调动的最小单元是线程,进程对系统资源消耗
# 对于io操作来说,多线程和多进程性能差别不大


# 注意:
    # 1、创建多个子线程时,程序里本身还有一个主线程
    # 2、主线程结束,子线程仍会自动执行,直至结束
    # 3、要设置主线程结束,子线程就立刻被kill掉,需要对子线程设置为守护线程setDaemon
    # 4、要设置子线程结束后,才能接着往下运行主线程程序,需要对子线程设置阻塞join()
# 多线程的应用方式:1、通过Thread类实例化 2、通过集成Thread来实现多线程

1、通过Thread类实例化
    # 用一个线程爬取列表1,再用另一个线程去爬取详情页,几乎同时进行,因为遇到io时,GIL会释放的
    # 爬取列表1,等待返回时,就可以让另一个线程去爬取详情页
import time
import threading
def get_detail_html(url):
    # 爬取文章详情页
    print("get detail html started")
    time.sleep(2)
    print("get detail html end")
def get_detail_url(url):
    # 爬取文章列表页
    print("get detail url started")
    time.sleep(3)
    print("get detail url end")
if __name__ == "__main__":

    thread1 = threading.Thread(target=get_detail_html,args=("",))
    thread2 = threading.Thread(target=get_detail_url,args = ("",))
    start_time = time.time()
    # setDaemon是守护线程,当主线程退出时,将子线程也关闭
    # 如果没有设置守护线程,那么当主线程退出时,子线程也将继续运行到结束
    # thread1.setDaemon(True)
    thread2.setDaemon(True)
    thread1.start()
    thread2.start()
    # 如果需要子线程运行结束后,再让主线程往下运行,那就需要对子线程进行阻塞
    thread1.join()
    thread2.join()
    # 当主线程退出时
    print(f"last time:{time.time()-start_time}")
2、通过继承Thread来实现多线程

    # 继承Thread类后,可重载__init__方法和run方法,但尽量不要继承start方法

class GetDetailHtml(threading.Thread):
    def __init__(self,name):
        super().__init__(name=name)
    # 重载run方法
    def run(self):
        print("get detail html started")
        time.sleep(2)
        print("get detail html end")

class GetDetalUrl(threading.Thread):
    def __init__(self,name):
        super().__init__(name=name)
    def run(self):
        print("get detail url started")
        time.sleep(3)
        print("get detail url end")

if __name__ == "__main__":
    thread1 = GetDetailHtml("get_detail_html")
    thread2 = GetDetailUrl("get_detail_url")
    start_time = time.time()
    thread1.start()
    thread2.start()

    thread1.join()
    thread2.join()
    print(f"last_time{time.time()-start_time}")

 线程间通信

# 线程间的通信,get_detail_url和get_detail_html需要合作,才能抓取到详情页
    # 线程通信方式:
        # 1、共享变量(global,或创建一个py文件存储变量),但变量的改变容易出错
        # 2、通过queue的方式,进行线程间同步

# 线程间通信方法1:共享变量

import variables
detail_url_list = []

def get_detail_html(detail_url_list):
    # detail_url_list = variables.detail_url_list
    # 爬取文章详情页
    while True:
        if len(variables.detail_url_list):
            url = variables.detail_url_list.pop()
            print("get detail html started")
            time.sleep(2)
            print("get detail html end")

def get_detail_url():
    # detail_url_list = variables.detail_url_list
    # 爬取文章列表页
    print("get detail url started")
    time.sleep(3)
    while True:
        for i in range(20):
            varialble.detail_url_list.append(f"http://prohectsedu.com/{i}")
        print("get detail url end")

if __name__ == "__main__":
    thread_detail_url = threading.Thread(target=get_detail_url)
    for i in range(10):
        html_thread = threading.Thread(target=get_detail_html)
        html_thread.start()
# 2、通过queue的方式,进行线程间同步
    # queue本质是deque的对象,而deque在字节码的层级上就已经实现了线程安全
    # qsize可判断queue的长度,empty判断是否为空,full判断是否满了,如果队列满了,put会自动阻塞
    # put_nowait\get_nowait实际就是调用了put\get,只不过加了block参数
# 2、通过queue的方式,进行线程间同步

from queue import Queue
import time
import threading
def get_detail_html(queue):
    # 爬取文章详情页
    while True:
        # queue中的get方法是阻塞的,如果queue为空,就会阻塞,直到queue不为空
        url = queue.get()
        print("get detail html started")
        time.sleep(2)
        print("get detail html end")

def get_detail_url(queue):
        # 爬取文章列表页
        while True:
            print("get detail url started")
            time.sleep(3)
            for i in range(20):
                queue.put(f"http://prohectsedu.com/{i}")
            print("get detail url end")
if __name__ == "__main__":
    detail_url_queue = Queue(maxsize = 1000)
    thread_detail_url = threading.Thread(target=get_detail_url,args=(detail_url_queue))
    for i in range(10):
        html_thread = threading.Thread(target=get_detail_html,args=(detail_url_queue))
        html_thread.start()
    start_time = time.time()
    detail_url_queue.task_done()
    detail_url_queue.join()
# 进程之间不共享全局变量,但线程共享全局变量,因此多线程操作全局变量会容易出现数据错误,因此需要线程等待(join)或是互斥锁
# 但线程等待或是互斥锁,都会让多线程任务变成单任务,执行性能会降低
# 创建进程的资源开销要比创建线程的资源开销大,因为创建进程要复制资源给子进程,另外也会自动创建出一条子进程的主线程
# 进程是操作系统资源分配的基本单位,线程是CPU调度的基本单位。就是操作系统将线程分配给某个CPU去执行,而CPU会将调度过来的进程去安排线程。
# 线程不能够独立执行,必须依存在进程中
# 多进程开发比单进程多线程开发稳定性要强。


# 进程优缺点:
    # 优点:可以用多核
    # 缺点:资源开销大

# 线程优缺点:
    # 资源开销小
    # 不能使用多核

# 跟计算密集型的相关操作使用多进程,使用多核提高计算能力。
# 多线程:文件写入、文件下载,i/o操作数据写入和读写。

# 单线程串行:IO和CPU计算是头尾衔接的
# 多线程并发:IO和CPU计算可以是并行的,但IO和IO不能并行,CPU和CPU不能并行_threading
# 多CPU并行:IO\CPU都能并行_multiprocssing
# 多机器并行:hadoop/hive/spark

# python对并发编程的支持
# 多线程:threading,利用CPU和IO可以同时执行的原理,让CPU不会干巴巴等待IO完成
# 多进程:multiprocess,利用多核CPU的能力,真正的并行执行任务
# 异步IO:asyncio,在单线程利用CPU和IO同时执行的原理,实现函数异步执行。

# 使用Lokc对资源加锁,防止冲突访问。
# 使用Queue实现不同线程/进程之间的数据通信,实现生产者-消费者模式
# 使用线程池Pool/进程池Pool,简化线程/进程的任务提交、等待结束、获取结果
# 使用subprocess启动外部程序的进程,并进行输入输出交互

# python并发编程有三种方式:多线程Thread、多进程Process、多协程Coroutine

# CPU密集型计算、IO密集型计算
# CPU密集型:也叫做计算密集型,是指I/O在很短的时间就可以完成,CPU需要大量的计算和处理,特点是CPU占用率非常高:压缩解压缩、加密解密、正则表达式搜索
# IO密集型:系统运作大部分的状况是CPU在等IO(硬盘内存)的读写操作,CPU占用率比较低,例如文件处理程序、网络爬虫程序、读写数据库程序

# 一个线程中可以启动N个协程
# 多协程优缺点:
    # 优点:内存开销最少、启动协程数量最多
    # 缺点:支持的库有限制(例如request,不支持协程)、代码实现复杂
# 多协程使用于:IO密集型计算、需要超多任务运行、但有现成库支持的场景
# 线程的生命周期:新建、就绪(start)、运行、阻塞、终止
# 新建线程需要分配资源,终止线程系统需要回收资源
# 重用线程,则可以减去新建和终止的开销——使用线程池

# 线程池好处:
    # 1、提升性能:减去了大量新建、终止的开销
    # 2、使用场景:适合处理突发性大量请求或需要大量线程完成任务,但实际任务处理时间较短
    # 3、防御功能:能有效避免系统因为创建线程过多,而导致系统负荷过大相应变慢等问题
    # 4、代码又是:使用线程池的语法简洁方便

# ThreadPoolExecutor的使用方法
    # 1、map函数,map的结果和入参是顺序对应的
    # 2、future模式,更强大,如果用as_completed顺序是不定的

你可能感兴趣的:(python高级,python)