python进程、线程和协程(三)

python 多线程

线程是CPU调度的最小单位

说到python多线程,就必须提到的点是GIL

GIL

GIL(Global Interpreter Lock)全局解释器锁
GIL是python用于同步线程的一种机制,这种机制使得计算机在任何时刻仅有一个线程在执行。
即在执行的每一个 Python 线程,都会先锁住自己,以阻止别的线程执行。

GIL底层实现原理
GIL

  • 当一个线程在运行时会持有GIL
  • 当该线程在遇到IO(读写内存、发送网络等)操作时会释放GIL

既然GIL对程序执行的性能影响这么大,那么python语言在设计之初为什么要引入GIL呢?
答:是为了解决多线程之间数据数据完整性和状态同步问题

python 使用引用计数来管理内存,所有 python 中创建的实例,都会配备一个引用计数,来记录有多少个指针来指向它。当实例的引用计数的值为 0 时,会自动释放其所占的内存。

假设有两个 python 线程同时引用 变量a,那么双方就都会尝试操作该数据,很有可能造成引用计数的条件竞争,导致引用计数只增加 1(实际应增加 2),这造成的后果是,当第一个线程结束时,会把引用计数减少 1,此时可能已经达到释放内存的条件(引用计数为 0),当第 2 个线程再次试图访问 a 时,就无法找到有效的内存了。

所以,python 引进 GIL,可以最大程度上规避类似内存管理这样复杂的竞争风险问题。

python 多线程

from threading import Thread
p = Thread(target=func, args=('test',))
p.start()
p.join()

案例实现:

blog_spider.py

import requests

urls = [
    f"https://www.cnblogs.com/#p{page}"
    for page in range(1, 51)
]


def crawl(url):
    r = requests.get(url)
    print(url, len(r.text))

multi_thread_crawl.py

import threading
import blog_spider
import time


def single_thread():
    for url in blog_spider.urls:
        blog_spider.crawl(url)


def multi_thread():
    threads = []
    for url in blog_spider.urls:
        threads.append(
            threading.Thread(target=blog_spider.crawl, args=(url,))
        )

    for thread in threads:
    	# 开启线程
        thread.start()

    for thread in threads:
    	# 等待线程完成
        thread.join()


if __name__ == '__main__':
    start = time.time()
    single_thread()
    end = time.time()
    print("single_thread cost:", end - start)
    start = time.time()
    multi_thread()
    end = time.time()
    print("multi_thread cost:", end - start)

线程安全

当python引入了GIL来确保计算机在某一时刻只用一个线程在运行时,为什么还要考虑线程安全的问题呢?
:这里先要说明一下在GIL底层实现原理中线程释放GIL的问题。
在python中有另外一个机制,叫做间隔式检查(check_interval),
意思是 python 解释器会去轮询检查线程 GIL 的锁住情况,每隔一段时间,Python 解释器就会强制当前线程去释放 GIL,这样别的线程才能有执行的机会。python3以后,这个间隔时间大致为 15 毫秒。
也就是说线程在运行过程中并不是一直运行,而是互相切换,以达到一个CPU和IO的平衡点。
了解了这种强制释放GIL的抢占机制,也就不难理解要在线程切换过程中考虑线程安全的问题了。
因为代码在执行过程中的非原子性,导致在代码执行过程中的任何时候都有切换线程的可能性,所有在代码中的变量就会出现混淆错误的情况,出现数据安全的问题。

1 def draw(account, amount):
2	if account.balance >= amount:
3		account.balance -= amount

例:以上代码在执行第二、三行的时候切换线程,就会出现错误。

Lock用于解决线程安全问题

用法代码:

import threading

lock  = threading.Lock()

with lock:
	# do something

线程通信

python中的线程通信可以使用 queue.Queue 模块
Python 的 Queue 模块中提供了同步的、线程安全的队列类,包括FIFO(先入先出)队列Queue,LIFO(后入先出)队列LifoQueue,和优先级队列 PriorityQueue。

这些队列都实现了锁原语,能够在多线程中直接使用,可以使用队列来实现线程间的同步。

Queue 模块中的常用方法:

  • Queue.qsize() 返回队列的大小
  • Queue.empty() 如果队列为空,返回True,反之False
  • Queue.full() 如果队列满了,返回True,反之False
  • Queue.full 与 maxsize 大小对应
  • Queue.get([block[, timeout]])获取队列,timeout等待时间
  • Queue.get_nowait() 相当Queue.get(False)
  • Queue.put(item) 写入队列,timeout等待时间
  • Queue.put_nowait(item) 相当Queue.put(item, False)
  • Queue.task_done() 在完成一项工作之后,Queue.task_done()函数向任务已经完成的队列发送一个信号
  • Queue.join() 实际上意味着等到队列为空,再执行别的操作
import queue
q = queue.Queue()
q.put(value)
value = q.get()

案例:

blog_spider.py

import requests
from bs4 import BeautifulSoup

urls = [
    f"https://www.cnblogs.com/#p{page}"
    for page in range(1, 51)
]


def crawl(url):
    r = requests.get(url)
    return r.text


def parse(html):
    soup = BeautifulSoup(html, "html.parse")
    links = soup.find_all("a", class_="post-item-title")
    return [(link["href"], link.get_text()) for link in links]

producer_consumer_spider.py

import queue
import time
import random
import threading
import blog_spider


def do_crawl(url_queue: queue.Queue, html_queue: queue.Queue):
    while True:
        url = url_queue.get()
        html = blog_spider.crawl(url)
        html_queue.put(html)
        print(threading.currentThread().name, f"crawl {url}",
              "url_queue.size = ", url_queue.qsize())
        time.sleep(random.randint(1, 2))


def do_parse(html_queue: queue.Queue, fout):
    while True:
        html = html_queue.get()
        results = blog_spider.parse(html)
        for result in results:
            fout.write(str(result) + "\n")
        print(threading.currentThread().name, "results.size", len(results),
              "html_queue.size = ", html_queue.qsize())
        time.sleep(random.randint(1, 2))


if __name__ == '__main__':
    url_queue = queue.Queue()
    html_queue = queue.Queue()
    for url in blog_spider.urls:
        url_queue.put(url)

    for item in range(3):
        t = threading.Thread(target=do_crawl, args=(url_queue, html_queue), name=f"crawl{item}")
        t.start()
    fout = open("data.txt", "w")
    for item in range(2):
        t = threading.Thread(target=do_parse, args=(html_queue, fout), name=f"parse{item}")
        t.start()

线程池技术

线程池的基类是 concurrent.futures 模块中的 Executor,Executor 提供了两个子类,即 ThreadPoolExecutor 和 ProcessPoolExecutor,其中 ThreadPoolExecutor 用于创建线程池,而 ProcessPoolExecutor 用于创建进程池。

如果使用线程池/进程池来管理并发编程,那么只要将相应的 task 函数提交给线程池/进程池,剩下的事情就由线程池/进程池来搞定。

Exectuor 提供了如下常用方法:

  • submit(fn, *args, **kwargs):将 fn 函数提交给线程池。*args 代表传给 fn 函数的参数,*kwargs 代表以关键字参数的形式为 fn 函数传入参数。
  • map(func, *iterables, timeout=None, chunksize=1):该函数类似于全局函数 map(func, *iterables),只是该函数将会启动多个线程,以异步方式立即对 iterables 执行 map 处理。

程序将 task 函数提交(submit)给线程池后,submit 方法会返回一个 Future 对象,Future 类主要用于获取线程任务函数的返回值。

Future 提供了如下方法:

  • result(timeout=None):获取该 Future 代表的线程任务最后返回的结果。如果 Future 代表的线程任务还未完成,该方法将会阻塞当前线程,其中 timeout 参数指定最多阻塞多少秒。
  • exception(timeout=None):获取该 Future 代表的线程任务所引发的异常。如果该任务成功完成,没有异常,则该方法返回 None。
  • add_done_callback(fn):为该 Future 代表的线程任务注册一个“回调函数”,当该任务成功完成时,程序会自动触发该 fn 函数。
from concurrent.futures import ThreadPoolExecutor
with ThreadPoolExecutor() as pool:
	# 方法一
	result = pool.map(func, ["test", ])
	# 方法二
	future = pool.submit(func, ["test", ])
	result = future.result()

案例:

producer_consumer_spider.py

import concurrent.futures
import blog_spider

# crawl
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as pool:
	# map方法
    htmls = pool.map(blog_spider.crawl, blog_spider.urls)
    htmls = list(zip(blog_spider.urls, htmls))
    for url, html in htmls:
        print(url, html)

print("crawl over")

# parse
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as pool:
    futures = {}
    for url, html in htmls:
    	# submit方法
        future = pool.submit(blog_spider.parse, html)
        futures[future] = url

    # for future, url in futures.items():
    #     print(url, future.result())

    for future in concurrent.futures.as_completed(futures):
        url = futures[future]
        print(url, future.result())

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值