关于python多线程以及线程池的使用(GIL/threading/Lock/Condition/semaphore/ThreadPoolExecutor/map/as_complete/wait)

1.GIL&threading
GIL的认识:
同一时间一个进程里只允许一个线程运行,只能在单个CPU上运行,无法将多个线程映射到CPU上执行
1.这与python的解释器cpython有关,与语言本身无关
2.多个进程可以利用多个CPU,但进程内的多个线程也只能利用单个CPU
3.对与IO密集型操作(如数据保存,数据请求),使用多线程优于多进程,因为对资源的消耗更小
4.对与CPU密集型操作(与数据计算处理相关,如计算斐波那契),使用多进程更有优势
5.GIL并不是一成不变只在同一时间运行一个线程,而是根据时间片划分的,遇到IO操作会优先处理,如下示例说明了GIL的问题

import threading

total = 0

def add():
    global total
    for i in range(10000000):
        total += 1

def desc():
    global total
    for i in range(10000000):
        total -= 1

if __name__ == '__main__':
    task1 = threading.Thread(target=add)
    task2 = threading.Thread(target=desc)
    task1.start()
    task2.start()

    task1.join()
    task2.join()
    print(total)  #运行后会发现total的数值不为0,且每次结果都不一样

示例说明:
以上通过主线程创立两个分线程,实际上一共有3个线程,主线程mian以及两个分线程task1和task2
1.输出结果每次都为不同的数字,因为task1没有执行完,GIL就切换到task2运行
2.默认情况下:主线程与分线程并发执行,所以可能出现主线程已执行完,但是分线程还没执行完,而分线程默认情况下会继续执行下去直到完成
3.如果在主线程中加入join(),那么主线程会阻塞,会等待分线程完全执行完才继续往下执行
4.如果在主线程中加入setDaemon(True)-称为守护线程,那么主线程执行完成后不管分线程有没有执行完,都会把分线程kill掉退出。

2.Lock
那么,要想实现以上各线程通过共享全局变量的方式进行通信,避免GIL的影响,可以使用线程锁:
from threading import Lock,Rlock
#其中Lock容易造成死锁,Rlock在同一线程中使用不会造成死锁,但是锁了多少次lock.acquire()就要释放多少次lock.release()

import threading
from threading import Lock

total = 0
lock = Lock()

def add():
    global total
    global lock
    for i in range(10000):
        lock.acquire()
        total += 1
        lock.release()


def desc():
    global total
    global lock
    for i in range(10000):
        lock.acquire()
        total -= 1
        lock.release()


if __name__ == '__main__':
    task1 = threading.Thread(target=add)
    task2 = threading.Thread(target=desc)
    task1.start()
    task2.start()

    task1.join()
    task2.join()
    print(total)

示例说明:
以上通过Lock就能避免因GIL的存在。造成资源竞争使得total不为0。
但运行会消耗比原来更长的时间。

3.queue
通过共享变量+锁的方式容易造成数据混乱及死锁,当多个线程同时调用一个共享变量的时候尤其容易出现问题,为解决以上问题,可以使用Queue
1.Queue是线程安全的,from queue import Queue是多线程中的Queue,只能用在多线程中,在多进程中不适用
2.使用规范:
Queue(maxsize=1000),设置一个最大值,避免内存消耗过大
3.常用方法:
put():把数据放入队列
get():把数据从队列取出,它是一个阻塞的方法,当取完后会停止并阻塞在当前位置,可以设置block=False,变成非阻塞,当数据取完后会抛出异常
join():从queue上进行线程阻塞,只有在queue发送task_done()后才会释放
task_done():释放线程,与join()配合使用

import threading
from queue import Queue

class GenUrl(threading.Thread):
    def __init__(self, queue):
        super().__init__()
        self.queue = queue

    def run(self):
        for i in range(200):
            url = "https://baidu.com/?page={}".format(i)
            self.queue.put(url)
            print("put {} success".format(i))


class RequestHtml(threading.Thread):
    def __init__(self, queue):
        super().__init__()
        self.queue = queue

    def run(self):
        while not self.queue.empty():		#循环取queue内的数据,直到为空,实际
            for response in range(self.queue.qsize()):
                print("get url: {} success".format(self.queue.get()))



if __name__ == '__main__':
    queue = Queue(maxsize=500)
    genurl = GenUrl(queue)
    requesthtml = RequestHtml(queue)
    genurl.start()
    requesthtml.start()
    genurl.join()
    requesthtml.join()

    print("done")

示例说明:
while not self.queue.empty(),这句循环取queue内的数据,直到为空退出。实际运行时因线程并发的关系,数据put进去一半的时候,get那边就取完了,此时判断为空,跳出循环,达不到打印所有数据的目的因此还需要加入一些逻辑判断或者使用条件变量condition控制

4.Condition
Condition条件变量,可以在程式上实现满足条件后再执行,(有__enter__(self)和__exit__(self,*args)这两个魔法方法可以使用with语句)
使用注意:
1.wait():等待某个条件变量的通知
2.notify():通知某个在等待的线程
3.使用了with 语句相当于使用了condition.acquire() 以及condition.release(),如果不使用with就要使用这两个,否则代码会报错
4.使用 condition 语句启动顺序很重要!

import threading
from threading import Condition
from queue import Queue

class GenUrl(threading.Thread):
    def __init__(self, queue, con):
        super().__init__()
        self.queue = queue
        self.con = con

    def run(self):
        with self.con:
            for i in range(200):
                url = "https://baidu.com/?page={}".format(i)
                self.queue.put(url)
                print("put {} success".format(i))
            self.con.notify()

class RequestHtml(threading.Thread):
    def __init__(self, queue, con):
        super().__init__()
        self.queue = queue
        self.con = con

    def run(self):
        with self.con:
            self.con.wait()
            while not self.queue.empty():
                for response in range(self.queue.qsize()):
                    print("get url: {} success".format(self.queue.get()))


if __name__ == '__main__':
    con = Condition()
    queue = Queue(maxsize=500)
    genurl = GenUrl(queue, con)
    requesthtml = RequestHtml(queue, con)
    requesthtml.start()
    genurl.start()
    genurl.join()
    requesthtml.join()

    print("done")

示例说明:用了condition后,输出变成了先获取所有的url,再去get htm,且无法有效控制每次请求执行的数量,在实际应用中,如果同一个IP一次性发几百个请求url,那将会很容易被封锁。这时需要使用semaphore控制请求数量。

5.semaphore
程序运行时是一下子就运行完了,如果有效的控制每次同时运行的数量呢?这时可以使用semaphore.
semaphore:是用于控制进入数量的锁,如控制爬虫每次请求网站的数量。
使用说明:
from threading import semaphore
sem = semaphore(3) #传入value=3,并发数设置为3
在实现方法中使用sem.acquire()控制执行,使用sem.release()结束控制执行的响应

import threading
import time
from threading import Condition, Semaphore
from queue import Queue


class GenUrl(threading.Thread):
    def __init__(self, queue, sem):
        super().__init__()
        self.queue = queue
        self.sem = sem

    def run(self):
        for i in range(200):
            url = "https://baidu.com/?page={}".format(i)
            self.queue.put(url)
            print("put {} success".format(i))
            self.sem.acquire()
            requesthtml = RequestHtml(self.queue, self.sem)
            requesthtml.start()

class RequestHtml(threading.Thread):
    def __init__(self, queue, sem):
        super().__init__()
        self.queue = queue
        self.sem = sem

    def run(self):
        time.sleep(5)
        while not self.queue.empty():
            for response in range(self.queue.qsize()):
                print("get url: {} success".format(self.queue.get()))
                self.sem.release()



if __name__ == '__main__':
    sem = Semaphore(2)
    queue = Queue(maxsize=500)
    genurl = GenUrl(queue, sem)
    genurl.start()
    genurl.join()
    print("done")

6.线程池ThreadPoolExecutor
使用semaphore便能控制每次执行的次数了。但还有更简单的方法,就是使用线程池。线程池不仅仅是数量控制还可以在主线程中获取某个线程的状态或者某一个任务的状态,以及返回值。同时当一个线程完成的时候主线程能立即知道,且futures这个包是专门用作线程池进程池编程的,它的多线程和多进程编码接口基本一致,后期在维护切换时更加方便。

from concurrent.futures.thread import ThreadPoolExecutor  
 import time

 def get_html(times):
 	time.sleep(times)
 	print("get page {}".format(times))
 	return times

 executor = ThreadPoolExecutor(max_workers=2)

 task1 = executor.submit(target=get_html, (3))
 task2 = executor.submit(target=get_html, (2))

 print(task1.done())   # 输出False
 time.sleep(3)
 print(task1.done()) #输出True
 print(task1.result())  #输出3


import time
from concurrent.futures import ThreadPoolExecutor
from concurrent.futures import wait
from queue import Queue


def gen_url(queue):
    for i in range(20):
        url = "https://baidu.com/?page={}".format(i)
        queue.put(url)
        print("put {} success".format(i))
    return queue


def get_html(queue):
    while not queue.empty():
        time.sleep(1)
        print("get url: {} success".format(queue.get()))


if __name__ == '__main__':
    queue = Queue()
    urls = gen_url(queue)
    executor = ThreadPoolExecutor(max_workers=5)
    all_task = []
    for i in range(5):
        task = executor.submit(get_html,queue)
        all_task.append(task)
    wait(all_task, return_when='ALL_COMPLETED')
    print("===============done==================")
    print(queue.qsize())

上述代码:
1.ThreadPoolExecutor(max_workers=5),定义最大线程数为5
2.submit(target=get_html, queue),submit 方法进行提交开启线程,传入args的参数为元组
3.其他没写的方法:
task.done(),task为线程的返回值,实际上它是futures的对象,里面的done()方法判断线程是否有执行完成
task.result()可以输出子线程返回的结果,这是一个阻塞方法,因为需要等待结果的返回
task.cancel():在没有进入线程前可以取消,进入后不能取消,执行中也不会取消,返回布尔类型

7.线程池的其他方法:as_completed/map/wait

import time
from concurrent.futures import ThreadPoolExecutor, wait, as_completed
from queue import Queue


def gen_url(queue):
    for i in range(20):
        url = "https://baidu.com/?page={}".format(i)
        queue.put(url)
        print("put {} success".format(i))
    return queue


def get_html(queue):
    while not queue.empty():
        time.sleep(1)
        print("get url: {} success".format(queue.get()))

def pase_html(html):
    time.sleep(1)
    return "pase {} success".format(html)


if __name__ == '__main__':
    queue = Queue()
    urls = gen_url(queue)
    executor = ThreadPoolExecutor(max_workers=5)
    all_task = []
    for i in range(5):
        task = executor.submit(get_html,queue)
        all_task.append(task)
    wait(all_task,return_when='FIRST_COMPLETED')

    print("================使用as_completed模拟解析一个html====================")
    executor2 = ThreadPoolExecutor(max_workers=5)
    html = [i for i in range(20)]
    print("html 顺序为:{}".format(html))
    all_task2 = []
    for j in html:
        task2 = executor2.submit(pase_html,j)
        all_task2.append(task2)
    for future in as_completed(all_task2):
        print(future.result())

    print("================使用map模拟解析一个html====================")
    executor3 = ThreadPoolExecutor(max_workers=5)
    html = [i for i in range(20)]
    print("html 顺序为:{}".format(html))
    all_task3 = []
    for data in executor3.map(pase_html,html):
        print(data)
        all_task3.append(data)

    print("===============done==================")

示例说明:
1.as_complete()
from concurrent.futures import as_completed
as_compelete:获取已经成功的task返回
as_compelete实际上是一个生成器,会把已经完成的任务yield成future对象,因此可以直接通过for 循环取出返回的结果,结果的顺序是不唯一的,哪个先完成就先返回哪个。

2.map()
executor本身有一个map函数也能返回完成的结果
map的返回值是yield data.result(),excutor.map(),也是一个生成器,把已经完成的任务yield成future.result(),通过for 循环将所有值取出。返回结果的顺序与传入数据的顺序是一致的,这是与as_complete的重要区别。

3.wait()
from concurrent.futures import wait
wait 方法可以对线程阻塞
#源码:wait(fs, timeout=None, return_when=‘ALL_COMPLETED’)
return_when的可选参数有:
FIRST_COMPLETED # 当第一个完成的时候就往下执行,不阻塞
FIRST_EXEPTION
ALL_COMPLETED #完成所有才不阻塞
_AS_COMPLETED

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值