Python高级编程和异步IO并发编程


一、多线程、多进程和线程池编程

1、GIL

 gil global interpreter lock (全局解释器锁)

 python中一个线程对应于c语言中的一个线程 (cpython)

 gil使得同一个时刻只有一个线程在一个cpu上执行字节码, 无法将多个线程映射到多个cpu上执行

GIL主动释放的情况:

  •  gil会根据执行的字节码行数以及时间片释放gil
  •  gil在遇到io的操作时候会主动释放

2、多线程编程 --threading

 操作系统能调度的最小单元是线程

 对于io操作来说,多线程和多进程性能差别不大

  • setdaemon 方法:守护线程,主线程运行完毕,立刻结束所有线程
  • join 方法:线程阻塞,无论主线程是否运行完毕,都需等待子线程执行完成才能结束

创建多线程两种方式

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(4)
    print("get detail url end")

if  __name__ == "__main__":
    thread1 = threading.Thread(target=get_detail_html)
    thread1 = threading.Thread(target=get_detail_html)
    #thread1.setDaemon(True)
    #thread2.setDaemon(True) # 守护线程

    start_time = time.time()
    thread1.start()
    thread2.start()

    thread1.join()
    thread2.join()   # 线程阻塞

    print ("last time: {}".format(time.time()-start_time))

2)使用thread继承方式:

 推荐这种方式,我们可以在类中做更多需要的处理。

class GetDetailHtml(threading.Thread):
    def __init__(self, name):
        super().__init__(name=name)

    def run(self):   # 重载 threading.Thread 中的方法
        print("get detail html started")
        time.sleep(2)
        print("get detail html end")

class GetDetailUrl(threading.Thread):
    def __init__(self, name):
        super().__init__(name=name)

    def run(self): 
        print("get detail url started")
        time.sleep(4)
        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 ("last time: {}".format(time.time()-start_time))

 

 3、线程间通信:共享变量和 Queue

 1)共享变量的方式,即全局定义一个变量,给多个子线程中调用。安全性不高,不建议使用

 2)Queue 队列的方式,比较安全,推荐使用.Queue常用方法可查看源码

#通过queue的方式进行线程间同步,更安全
from queue import Queue
import time
import threading

def get_detail_html(queue):
    # 爬取文章详情页
    while True:
        url = queue.get()  # 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(4)
        for i in range(20):
            queue.put("http://projectsedu.com/{id}".format(id=i))  # queue put 方法
        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()  # 队列阻塞

    print ("last time: {}".format(time.time()-start_time))

 

 4. 线程同步:Lock、RLock

 1)Lock

from threading import Lock

lock = Lock()

    lock.acquire()   # 获取锁
    lock.acquire()   # 上面已获取锁,再acquire则会阻塞住,即在锁未释放前,两次acquire就会造成死锁
    total += 1
    lock.release()   # 释放锁,在释放锁之前其他线程都会被hold住,等待锁释放
    lock.release()

 注意:1、用锁会影响性能

    2、用锁注意造成死锁问题:

    1)未释放锁前,连续两次acquire 会造成死锁

    2)资源竞争:即线程一要的资源在线程二中,线程二要的资源在线程一中,线程一获取锁需要用到线程二的资源,然而锁在线程一中,线程二锁住(阻塞住)无法将资源给到线程一,因此造成死锁

2)RLock 

 可重入的锁 ,在同一个线程里面,可以连续调用多次acquire, 一定要注意acquire的次数要和release的次数相等

from threading import Lock, RLock, Condition

#RLock可重入的锁,在同一个线程里面,可以连续调用多次acquire, 一定要注意acquire的次数要和release的次数相等

total = 0
lock = RLock()
def add():
    global lock
    global total
    for i in range(1000000):
        lock.acquire()   # 获取锁
        lock.acquire()   # RLock ,不会造成死锁
        total += 1
        lock.release()   # 释放锁
        lock.release()  # release次数需与acquire次数一致

RLock的可用性要远远高于Lock,如果要使用锁,建议使用RLock 

 

from threading import Lock, RLock, Condition


total = 0
lock = RLock()
def add():
    global lock
    global total
    for i in range(1000000):
        lock.acquire()   # 获取锁
        lock.acquire()   
        total += 1
        lock.release()   # 释放锁
        lock.release()

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

import threading
thread1 = threading.Thread(target=add)
thread2 = threading.Thread(target=desc)
thread1.start()
thread2.start()

thread1.join()
thread2.join()
print(total)
demo

 

 5、线程同步 - condition 使用以及源码分析

 condition中实现了魔法函数:__enter__、__exit__ ,是上下文管理器,可以用with处理 

#通过condition完成协同读诗

import threading

class XiaoAi(threading.Thread):
    def __init__(self, cond):
        super().__init__(name="小爱")
        self.cond = cond

    def run(self):
        with self.cond:
            self.cond.wait()
            print("{} : 在 ".format(self.name))
            self.cond.notify()

            self.cond.wait()
            print("{} : 好啊 ".format(self.name))
            self.cond.notify()

            self.cond.wait()
            print("{} : 君住长江尾 ".format(self.name))
            self.cond.notify()

            self.cond.wait()
            print("{} : 共饮长江水 ".format(self.name))
            self.cond.notify()

            self.cond.wait()
            print("{} : 此恨何时已 ".format(self.name))
            self.cond.notify()

            self.cond.wait()
            print("{} : 定不负相思意 ".format(self.name))
            self.cond.notify()

class TianMao(threading.Thread):
    def __init__(self, cond):
        super().__init__(name="天猫精灵")
        self.cond = cond

    def run(self):
        with self.cond:
            print("{} : 小爱同学 ".format(self.name))
            self.cond.notify()
            self.cond.wait()

            print("{} : 我们来对古诗吧 ".format(self.name))
            self.cond.notify()
            self.cond.wait()

            print("{} : 我住长江头 ".format(self.name))
            self.cond.notify()
            self.cond.wait()

            print("{} : 日日思君不见君 ".format(self.name))
            self.cond.notify()
            self.cond.wait()

            print("{} : 此水几时休 ".format(self.name))
            self.cond.notify()
            self.cond.wait()

            print("{} : 只愿君心似我心 ".format(self.name))
            self.cond.notify()
            self.cond.wait()

if __name__ == "__main__":
    from concurrent import futures
    cond = threading.Condition()
    xiaoai = XiaoAi(cond)
    tianmao = TianMao(cond)

    xiaoai.start()
    tianmao.start()
    #启动顺序很重要
    #在调用with cond之后才能调用wait或者notify方法
    #condition有两层锁, 一把底层锁会在线程调用了wait方法的时候释放, 上面的锁会在每次调用wait的时候分配一把并放入到cond的等待队列中,等到notify方法的唤醒

 

 6、线程同步 - Semaphore 使用以及源码分析

  Semaphore:信号量, 是用于控制进入数量的锁。本质上是锁,Lock是单锁,信号量是指定多把锁,也就是说通过信号量指定多个数线程可以访问相同资源,一般情况下读操作可以有多个,但写操作同时只有一个

  Semaphore 管理一个计数器,每调用一次 acquire() 方法,计数器就减一,每调用一次 release() 方法,计数器就加一。计时器的值默认为 1 ,计数器的值不能小于 0,当计数器的值为 0 时,调用 acquire() 的线程就会等待,直到 release() 被调用。 因此,可以利用这个特性来控制线程数量 

 控制爬虫数量,一次最多执行3个线程:

import threading
import time
# 控制爬虫数量,一次执行三次
class HtmlSpider(threading.Thread):
    def __init__(self, url, sem):
        super().__init__()
        self.url = url
        self.sem = sem

    def run(self):
        time.sleep(2)
        print("got html text success")
        self.sem.release()       # Semaphore.release(),释放锁
class UrlProducer(threading.Thread):
    def __init__(self, sem):
        super().__init__()
        self.sem = sem

    def run(self):
        for i in range(20):
            self.sem.acquire()    # Semaphore.acquire(),每运行一个爬虫自动减1,当3个爬虫都运行时,计数器减为零,此时再调用acquire方法则会hold
            html_thread = HtmlSpider("https://baidu.com/{}".format(i), self.sem)
            html_thread.start()

if __name__ == "__main__":
    sem = threading.Semaphore(3)  # 控制爬虫数量为3个
    url_producer = UrlProducer(sem)
    url_producer.start()

 

 7、ThreadPoolExecutor线程池

  线程池使用:不仅仅是数量控制,可以获取线程状态、任务状态、线程返回值等信息; 当一个线程完成的时候我们主线程能立即知道  ; futures可以让多线程和多进程编码接口一致 。

  线程池模块  ThreadPollExecutor

  线程池使用过程:

  1.  实例化线程池
  2.  提交任务,会有个返回对象,submit是不会堵塞,立即返回
  3.  让主线程等待线程执行完成
  4.  关闭线程池

  线程池几个方法:

  • done()  :判断任务是否完成
  • result() :获取任务执行结果,会阻塞
  • cancle():取消任务,任务在执行中或者已经执行完成则无法取消
from concurrent.futures import ThreadPoolExecutor
import time

def get_html(times):
    time.sleep(times)
    print("get page {} success".format(times))
    return times
# 程序执行时,会到线程池执行线程
executor = ThreadPoolExecutor(max_workers=2) # 线程数最大两个
# 通过submit函数提交执行的函数到线程池中, submit不会阻塞,有个返回值,线程状态等可以通过这个返回值查看
task1 = executor.submit(get_html, (3))
task2 = executor.submit(get_html, (2))

# 状态查询:
# done方法用于判定某个任务是否完成
# print(task1.done())     # 返回:False
# print(task2.cancel())   # 可以取消没有执行的
# time.sleep(3)
# print(task1.done())     # 返回:True

# #result方法可以获取task的执行结果
# print(task1.result())   # 返回:3

 

 1)futures下的as_completed()方法 :获取已经执行完成的任务的结果(推荐)

from concurrent.futures import as_completed
urls = [3,2,4] all_task = [executor.submit(get_html, (url)) for url in urls] # wait(all_task, return_when=FIRST_COMPLETED) for future in as_completed(all_task): as_completed是个生成器,执行完成的任务都能获取到 data = future.result() print("get {} page".format(data)) # 打印执行成功的任务

 

 2)线程池自带的map()方法:获取已经执行完成的任务结果

from concurrent.futures import ThreadPoolExecutor, as_completed, wait, FIRST_COMPLETED

import time

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

executor = ThreadPoolExecutor(max_workers=2)

#通过executor的map获取已经完成的task的值
for data in executor.map(get_html, urls):
    print("get {} page".format(data))

 

 3)futures下的wait()方法 :等待线程完成,才执行下面的程序

urls = [3,2,4]
all_task = [executor.submit(get_html, (url)) for url in urls]
wait(all_task, return_when=FIRST_COMPLETED)   # 阻塞,return_when(条件),FIRST_COMPLETED:第一个线程执行完成才能执行下面的,未完成前会阻塞住
print("main")
for future in as_completed(all_task):
    data = future.result()
    print("get {} page".format(data))

 

8、multiprocessing 多进程编程

多进程参考:http://www.cnblogs.com/kaituorensheng/p/4445418.html

1)多进程创建方式

方式一:

import multiprocessing
import time

def worker_1(interval):
    print "worker_1"
    time.sleep(interval)
    print "end worker_1"

def worker_2(interval):
    print "worker_2"
    time.sleep(interval)
    print "end worker_2"

def worker_3(interval):
    print "worker_3"
    time.sleep(interval)
    print "end worker_3"

if __name__ == "__main__":
    p1 = multiprocessing.Process(target = worker_1, args = (2,))
    p2 = multiprocessing.Process(target = worker_2, args = (3,))
    p3 = multiprocessing.Process(target = worker_3, args = (4,))

    p1.start()
    p2.start()
    p3.start()

    print("The number of CPU is:" + str(multiprocessing.cpu_count()))
    for p in multiprocessing.active_children():
        print("child   p.name:" + p.name + "\tp.id" + str(p.pid))
    print "END!!!!!!!!!!!!!!!!!"

 

# 结果:
The number of CPU is:4
child   p.name:Process-3    p.id7992
child   p.name:Process-2    p.id4204
child   p.name:Process-1    p.id6380
END!!!!!!!!!!!!!!!!!
worker_1
worker_3
worker_2
end worker_1
end worker_2
end worker_3

方式二:

import multiprocessing
import time

class ClockProcess(multiprocessing.Process):
    def __init__(self, interval):
        multiprocessing.Process.__init__(self)
        self.interval = interval

    def run(self):
        n = 5
        while n > 0:
            print("the time is {0}".format(time.ctime()))
            time.sleep(self.interval)
            n -= 1

if __name__ == '__main__':
    p = ClockProcess(3)
    p.start()      
# 结果:
the time is Tue Apr 21 20:31:30 2015
the time is Tue Apr 21 20:31:33 2015
the time is Tue Apr 21 20:31:36 2015
the time is Tue Apr 21 20:31:39 2015
the time is Tue Apr 21 20:31:42 2015

2)进程池:

方式一:

from concurrent.futures import ProcessPoolExecutor
import requests
import time

def task(url):
    response = requests.get(url)
    print(url,response)
    # 写正则表达式


pool = ProcessPoolExecutor(7)
url_list = [
    'http://www.cnblogs.com/wupeiqi',
    'http://huaban.com/favorite/beauty/',
    'http://www.bing.com',
    'http://www.zhihu.com',
    'http://www.sina.com',
    'http://www.baidu.com',
    'http://www.autohome.com.cn',
]
for url in url_list:
    pool.submit(task,url)

pool.shutdown(wait=True)

 

方式二:

from concurrent.futures import ProcessPoolExecutor
import requests
import time

def task(url):
    response = requests.get(url)
    return response

def done(future,*args,**kwargs):
    response = future.result()
    print(response.status_code,response.content)

pool = ProcessPoolExecutor(7)
url_list = [
    'http://www.cnblogs.com/wupeiqi',
    'http://huaban.com/favorite/beauty/',
    'http://www.bing.com',
    'http://www.zhihu.com',
    'http://www.sina.com',
    'http://www.baidu.com',
    'http://www.autohome.com.cn',
]
for url in url_list:
    v = pool.submit(task,url)
    v.add_done_callback(done)

pool.shutdown(wait=True)

 

3)进程下的pool

使用进程池(非阻塞):

#coding: utf-8
import multiprocessing
import time

def func(msg):
    print "msg:", msg
    time.sleep(3)
    print "end"

if __name__ == "__main__":
    pool = multiprocessing.Pool(processes = 3)
    for i in xrange(4):
        msg = "hello %d" %(i)
        pool.apply_async(func, (msg, ))   #维持执行的进程总数为processes,当一个进程执行完毕后会添加新的进程进去

    print "Mark~ Mark~ Mark~~~~~~~~~~~~~~~~~~~~~~"
    pool.close()
    pool.join()   #调用join之前,先调用close函数,否则会出错。执行完close后不会有新的进程加入到pool,join函数等待所有子进程结束
    print "Sub-process(es) done."

函数解释:

  • apply_async(func[, args[, kwds[, callback]]]) 它是非阻塞,apply(func[, args[, kwds]])是阻塞的(理解区别,看例1例2结果区别)
  • close()    关闭pool,使其不在接受新的任务。
  • terminate()    结束工作进程,不在处理未完成的任务。
  • join()    主进程阻塞,等待子进程的退出, join方法要在close或terminate之后使用。

执行说明:创建一个进程池pool,并设定进程的数量为3,xrange(4)会相继产生四个对象[0, 1, 2, 4],四个对象被提交到pool中,因pool指定进程数为3,所以0、1、2会直接送到进程中执行,当其中一个执行完事后才空出一个进程处理对象3,所以会出现输出“msg: hello 3”出现在"end"后。因为为非阻塞,主函数会自己执行自个的,不搭理进程的执行,所以运行完for循环后直接输出“mMsg: hark~ Mark~ Mark~~~~~~~~~~~~~~~~~~~~~~”,主程序在pool.join()处等待各个进程的结束。

 

使用进程池(阻塞):

#coding: utf-8
import multiprocessing
import time

def func(msg):
    print "msg:", msg
    time.sleep(3)
    print "end"

if __name__ == "__main__":
    pool = multiprocessing.Pool(processes = 3)
    for i in xrange(4):
        msg = "hello %d" %(i)
        pool.apply(func, (msg, ))   #维持执行的进程总数为processes,当一个进程执行完毕后会添加新的进程进去

    print "Mark~ Mark~ Mark~~~~~~~~~~~~~~~~~~~~~~"
    pool.close()
    pool.join()   #调用join之前,先调用close函数,否则会出错。执行完close后不会有新的进程加入到pool,join函数等待所有子进程结束

 

9、进程间通信 - Queue、Pipe,Manager

1)Queue队列

 是 multiprocessing自带的 Queue 

import time
from multiprocessing import Process, Queue
# from queue import Queue  #不是这个Queue

def producer(queue):
    queue.put("a")
    time.sleep(2)

def consumer(queue):
    time.sleep(2)
    data = queue.get()
    print(data)

if __name__ == "__main__":
    queue = Queue(10)
    my_producer = Process(target=producer, args=(queue,))
    my_consumer = Process(target=consumer, args=(queue,))
    my_producer.start()
    my_consumer.start()
    my_producer.join()
    my_consumer.join()

 

multiprocessing中的Queue不能用于multiprocessing下的pool进程池,pool中的进程间通信需要使用manager中的queu

目前我们已经说到了三个Queue:

  • from queue import Queue :用于线程通信
  • from multiprocessing import Queue :用于进程通信
  • from multiprocessing import Manager → q=Manager().Queue() :用于multiprocessing下的pool进程池的进程通信

2)Pipe 管道

multiprocessing下的Pipe ,Pipe只能用于两个进程间的通信,Pipe的性能高于Queue的

from multiprocessing import Process, Pipe

def producer(pipe):
    pipe.send("MJ")

def consumer(pipe):
    print(pipe.recv())

if __name__ == "__main__":
    recevie_pipe, send_pipe = Pipe()
    #pipe只能适用于两个进程间通信
    my_producer= Process(target=producer, args=(send_pipe, ))
    my_consumer = Process(target=consumer, args=(recevie_pipe,))

    my_producer.start()
    my_consumer.start()
    my_producer.join()
    my_consumer.join()

GIL锁知识点:

  • Python的多线程在多核CPU上,只对于IO密集型计算产生正面效果;而当有至少有一个CPU密集型线程存在,那么多线程效率会由于GIL而大幅下降。
  • 即使在多核上运行多线程,同一时刻也只有一核会运行一个线程。
  • 遇到CPU密集型这种,建议不使用多线程,而改用多进程处理,但多进程更耗成本

 

posted on 2018-10-10 21:04  Eric_nan  阅读(551)  评论(1编辑  收藏  举报