多线程

真正意义上的多线程是由CPU来控制的,例如如果一个CPU密集型的程序,用C语言写,运行在一个四核处理器上,采用多线程的话最多可以获得4倍的效率提升。但是用Python写的话,效率不会提高,甚至会变慢,因为Python中的多线程是由GIL控制的,GIL的全称是Global Interpreter Lock(全局解释器锁),Python最初的设计理念在于,为了解决多线程之间数据完整性和状态同步的问题,设计为在任意时刻只能由一个线程在解释器中运行。因此Python中的多线程是表面上的多线程(同一时刻只有一个线程),不是真正的多线程。除了程序本身执行外,还多了线程切换所花的时间。因此,Python多线程相对更适合写I/O密集型的程序,真正对效率要求高的CPU密集型程序都用C/C++去写。

Threading实现多线程

# encoding:utf-8
import threading

def thread_job():
    print("abc")
added_thread1 = threading.Thread(target=thread_job)
added_thread1.start()

将目标指定执行的功能,就是设定的线程设定名称,如果要建立执行参数的参数,可以设定执行的参数,设定格式一定要Tuple(变量,)
threading.Thread(target=function, name="Thread", args=参数)
开始执行线程,开始前面指定线程的名称
<线程>.start()
将主线程暂停,等待指定的线程结束,join前面要放指定的线程名称
<线程>.join()
- 查看当前有多少个线程
threading.active_count()
查看当前使用线程的信息
threading.enumerate()
查看当前在哪个线程中
threading.current_thread() 

#打印线程名称
t1=threading.currentThread().name;

join将主线程暂停

使用join()将主线程暂停,等待指定的线程结束,主线程才会结束

# encoding:utf-8
import threading
import time
def thread_first_job(x):
    time.sleep(0.1)
    print("This is the first thread ", x)

def thread_second_job(x):
    print("This is the second thread ", x)

first_thread = threading.Thread(target=thread_first_job, args=("Hi",))
second_thread = threading.Thread(target=thread_second_job, args=("Hello",))
ts=[first_thread,second_thread]
for t in ts:
    t.start()
#等待执行线程结束
for t in ts:
    t.join()
print('all done')

setDaemon设置守护线程

若希望在主线程执行完毕后,不管其他的Thread是否已执行完毕,都强制跟主线程一起结束,setDaemon()必须写在start() 之前,预设为 False。

# encoding:utf-8
import threading
import time

def thread_first_job(x):
    time.sleep(5)
    print("This is the first thread ", x)

def thread_second_job(x):
    print("This is the second thread ", x)

first_thread = threading.Thread(target=thread_first_job, args=("Hi",))
second_thread = threading.Thread(target=thread_second_job, args=("Hello",))
first_thread.setDaemon(True)
first_thread.start()
second_thread.start()

# ---- output ---
#主线程结束 first_thread还没有来得及执行就必须结束了
# This is the second thread  Hello

queue获取线程结果

# encoding:utf-8
import threading
from queue import Queue

# 将要传回的值存入Queue
def thread_job(data, q):
    for i in range(len(data)):
        data[i] = data[i] * 2
    q.put(data)

def multithread():
    data = [[1, 2, 3], [4, 5, 6]]
    q = Queue()
    all_thread = []

    # 使用 multi-thread
    for i in range(len(data)):
        thread = threading.Thread(target=thread_job, args=(data[i], q))
        thread.start()
        all_thread.append(thread)

    # 等待全部 Thread执行完毕
    for t in all_thread:
        t.join()
    # 使用 q.get() 取出要传回的值
    result = []
    for _ in range(len(all_thread)):
        result.append(q.get())
    print(result)


multithread()
# ====== output ======
# [[2, 4, 6], [8, 10, 12]]

Lock线程同步

当同时有几个 Thread 要用到同一个数据时,为了不发生 Race Condition 的现象,需要使用 lock.acquire() 以及 lock.release() 来将其锁定住,不让其他Thread 执行

Python 互斥锁 Lock,主要作用是并行访问共享资源时,保护共享资源,防止出现脏数据。

#先看看不加锁的情况

# encoding:utf-8
import threading

def job1():
    global n
    for i in range(50):
        n+=1
        print('job1',n)

def job2():
    global n
    for i in range(50):
        n+=10
        print('job2',n)

n=0
t1=threading.Thread(target=job1)
t2=threading.Thread(target=job2)
t1.start()
t2.start()

 看执行结果,是乱的

python多进程和多线程的区别 python多进程和多线程协程_python多进程和多线程的区别

 再看看加锁的情况

def job1():
    global n, lock
    # 获取锁
    lock.acquire()
    for i in range(10):
        n += 1
        print('job1', n)
    lock.release()


def job2():
    global n, lock
    # 获取锁
    lock.acquire()
    for i in range(10):
        n += 10
        print('job2', n)
    lock.release()

n = 0
# 生成锁对象
lock = threading.Lock()

t1 = threading.Thread(target=job1)
t2 = threading.Thread(target=job2)
t1.start()
t2.start()

由于job1的线程,率先拿到了锁,所以在for循环中,没有人有权限对n进行操作。当job1执行完毕释放锁后,job2这才拿到了锁,开始自己的for循环。
看看执行结果,真如我们预想的那样。

python多进程和多线程的区别 python多进程和多线程协程_python_02

 信号量(Semaphore)

Semaphore 跟 Lock 类似,但 Semaphore 多了计数器的功能,可以指定允许个数的线程同时执行。

# encoding:utf-8
# -*- coding:utf-8 -*-
import threading
import time

sem = threading.Semaphore(3)

class DemoThread(threading.Thread):

    def run(self):
        print('{0} is waiting semaphore.'.format(self.name))
        sem.acquire()
        print('{0} acquired semaphore({1}).'.format(self.name, time.ctime()))
        time.sleep(5)
        print('{0} release semaphore.'.format(self.name))
        sem.release()


if __name__ == '__main__':
    threads = []
    for i in range(4):
        threads.append(DemoThread(name='Thread-' + str(i)))

    for t in threads:
        t.start()

    for t in threads:
        t.join()

运行结果:可以看到Thread-3是在Thread-0释放后才获得信号对象。

Thread-0 is waiting semaphore.
Thread-0 acquired semaphore(Thu Oct 25 20:33:18 2018).
Thread-1 is waiting semaphore.
Thread-1 acquired semaphore(Thu Oct 25 20:33:18 2018).
Thread-2 is waiting semaphore.
Thread-2 acquired semaphore(Thu Oct 25 20:33:18 2018).
Thread-3 is waiting semaphore.
Thread-0 release semaphore.
Thread-3 acquired semaphore(Thu Oct 25 20:33:23 2018).
Thread-1 release semaphore.
Thread-2 release semaphore.
Thread-3 release semaphore.

 Rlock可重入锁

RLock 跟 Lock 类似,但 RLock 可允许同一个线程重复取得锁定的使用权。

使用 acquire 取得 rlock,release 释放rlock

普通的锁是threading.Lock,这个锁在同一线程下 未释放的情况下再次获取会造成死


常规锁和Python中的Rlock之间的一个区别是,常规锁可以由不同的线程释放,而重入锁必须由获取它的同一个线程释放同时要求解锁次数应与加锁次数相同,才能用于另一个线程。另外,需要注意的是一定要避免在多个线程之间拆分锁定操作,如果一个线程试图释放一个尚未获取的锁,Python将引发错误并导致程序崩溃。

可重入锁一般是在面向对象中使用
调用顺序不同,而且都需要同步的时候,如果不用可重入锁,会死锁。如果f1或f2不加锁,数据不同步,报错。

# encoding:utf-8
import threading
class A:
   def f1(self):
       mutex.acquire()
       try:
            print('do something')
       finally:
           mutex.release()

   def f2(self):
       mutex.acquire()
       try:
           print('do something')
       finally:
           mutex.release()
           
def run1(obj):
    obj.f1()
    obj.f2()

def run2(obj):
    obj.f2()
    obj.f1()

obj1 = A()
mutex = threading.RLock()
t1 = threading.Thread(target=run1, args=(obj1, ))
t2 = threading.Thread(target=run2, args=(obj1, ))
t1.start()
t2.start()

条件(Condition)

可以这么理解,Condition 提供了一种多线程通信机制,假如线程 1 需要数据,那么线程 1 就阻塞等待,这时线程 2 就去制造数据,线程 2 制造好数据后,通知线程 1 可以去取数据了,然后线程 1 去获取数据。

典型案例是如下的生产者消费者模式

# encoding:utf-8

import threading
import time
con = threading.Condition()
meat_num = 0
def thread_consumers():  # 条件变量 condition 线程上锁
    con.acquire()
    # 全局变量声明关键字 global
    global meat_num
    # 等待肉片下锅煮熟
    con.wait()
    while True:
        print("我来一块肉片...")
        meat_num -= 1
        print("剩余肉片数量:%d" % meat_num)
        time.sleep(0.5)
        if meat_num == 0:
            #  肉片吃光了,通知老板添加肉片
            print("老板,再来一份老肉片...")
            con.notify()
            #  肉片吃光了,等待肉片
            con.wait()

        # 条件变量 condition 线程释放锁
    con.release()


def thread_producer():  # 条件变量 condition 线程上锁
    con.acquire()  # 全局变量声明关键字 global
    global meat_num
    # 肉片熟了,可以开始吃了
    
    meat_num = 10
    print("肉片熟了,可以开始吃了...")
    con.notify()
    while True:
        #  阻塞函数,等待肉片吃完的通知
        con.wait()
        meat_num = 10
        #  添加肉片完成,可以继续开吃
        print("添加肉片成功!当前肉片数量:%d" % meat_num)
        time.sleep(1)
        con.notify()

    con.release()

if __name__ == '__main__':
    t1 = threading.Thread(target=thread_producer)
    t2 = threading.Thread(target=thread_consumers)
    # 启动线程 -- 注意线程启动顺序,启动顺序很重要
    t2.start()
    t1.start()
    # 阻塞主线程,等待子线程结束
    t1.join()
    t2.join()

print("程序结束!")

'''
输出结果:

肉片熟了,可以开始吃了...
我来一块肉片...
剩余肉片数量:9
我来一块肉片...
剩余肉片数量:8
我来一块肉片...
剩余肉片数量:7
我来一块肉片...
剩余肉片数量:6
我来一块肉片...
剩余肉片数量:5
我来一块肉片...
剩余肉片数量:4
我来一块肉片...
剩余肉片数量:3
我来一块肉片...
剩余肉片数量:2
我来一块肉片...
剩余肉片数量:1
我来一块肉片...
剩余肉片数量:0
老板,再来一份老肉片...
添加肉片成功!当前肉片数量:10
我来一块肉片...
剩余肉片数量:9
我来一块肉片...
剩余肉片数量:8
我来一块肉片...
剩余肉片数量:7
.............
'''

事件(Event)

用于线程间的通信,藉由发送线程设置的信号,若信号为True,其他等待的线程接受到信好后会被唤醒。提供 設置信号 event.set(), 等待信号event.wait(), 清除信号 event.clear() 。

# encoding:utf-8
import threading
import time
def thread_first_job():
    global a
    # 线程进入等待状态
    print("Wait…")
    event.wait()

    for _ in range(3):
        a += 1
        print("This is the first thread ", a)
a = 0
# 创建event对象
event = threading.Event()
first_thread = threading.Thread(target=thread_first_job)
first_thread.start()
time.sleep(3)
# 唤醒处于等待状态的线程
print("Wake up the thread…")
event.set()
first_thread.join()
# ====== output ======
# Wait...
# Wake up the thread...
# This is the first thread  1
# This is the first thread  2
# This is the first thread  3

线程池

线程池使用场景
当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池。

线程池原理

线程池在系统启动时即创建大量空闲的线程,程序只要将一个函数提交给线程池,线程池就会启动一个空闲的线程来执行它。当该函数执行结束后,该线程并不会死亡,而是再次返回到线程池中变成空闲状态,等待执行下一个函数。

作用

可以控制并发多线程的数量,不会导致系统崩溃

线程池创建

concurrent.futures.Executor 提供了两个子类
ThreadPoolExecutor :用于创建线程池
ProcessPoolExecutor :用于创建进程池

pool方法

  • submit(fn, *args, **kwargs):将 fn 函数提交给线程池。*args 代表传给 fn 函数的参数,*kwargs 代表以关键字参数的形式为 fn 函数传入参数。
  • map(func, *iterables, timeout=None, chunksize=1):该函数类似于全局函数 map(func, *iterables),只是该函数将会启动多个线程,以异步方式立即对 iterables 执行 map 处理。
  • shutdown(wait=True):关闭线程池。加了pool.shutdown(),则会等待所有线程都运行结束后,再运行后面语句。

 pool.submit

# encoding:utf-8
from concurrent.futures import ThreadPoolExecutor
import threading
import time
# 定义一个准备作为线程任务的函数
def action(max):
    my_sum = 0
    for i in range(max):
        print(threading.current_thread().name + '  ' + str(i))
        my_sum += i
    return my_sum
# 创建一个包含2条线程的线程池
pool = ThreadPoolExecutor(max_workers=2)
# 向线程池提交一个task, 50会作为action()函数的参数
future1 = pool.submit(action, 50)
# 向线程池再提交一个task, 100会作为action()函数的参数
future2 = pool.submit(action, 100)
# 判断future1代表的任务是否结束
print(future1.done())
time.sleep(3)
# 判断future2代表的任务是否结束
print(future2.done())
# 查看future1代表的任务返回的结果
print(future1.result())
# 查看future2代表的任务返回的结果
print(future2.result())
# 关闭线程池
pool.shutdown()

pool.map

map可以保证输出的顺序, submit输出的顺序是乱的.

如果你要提交的任务的函数是一样的,就可以简化成map.

该方法的功能类似于全局函数 map(),区别在于线程池的 map() 方法会为 iterables 的每个元素启动一个线程,以并发方式来执行 func 函数。这种方式相当于启动 len(iterables) 个线程,井收集每个线程的执行结果。

# encoding:utf-8
import datetime
import threading
from concurrent.futures import ThreadPoolExecutor
import time
def spider(page):
    time.sleep(page)
    print(f"crawl task{page} finished  tread_name"+threading.currentThread().name)
    return page

pool = ThreadPoolExecutor(max_workers=4)
print ("start processes...")
#for i in range(10):
#    t = pool.submit(spider, i)
t1=datetime.datetime.now()
pool.map(spider, [0,1,2,3])
pool.shutdown()
t2=datetime.datetime.now()
#time.sleep(20)
print("time cost==="+str(t2-t1))
print ("is ok?")
print ("all is ok!")

Future方法 

submit 方法会返回一个 Future 对象,Future 类主要用于获取线程任务函数的返回值。

Future 提供了如下方法

cancel():取消该 Future 代表的线程任务。如果该任务正在执行,不可取消,则该方法返回 False;否则,程序会取消该任务,并返回 True。
cancelled():返回 Future 代表的线程任务是否被成功取消。
running():如果该 Future 代表的线程任务正在执行、不可被取消,该方法返回 True。
done():如果该 Funture 代表的线程任务被成功取消或执行完成,则该方法返回 True。
result(timeout=None):获取该 Future 代表的线程任务最后返回的结果。如果 Future 代表的线程任务还未完成,该方法将会阻塞当前线程,其中 timeout 参数指定最多阻塞多少秒。
exception(timeout=None):获取该 Future 代表的线程任务所引发的异常。如果该任务成功完成,没有异常,则该方法返回 None。
add_done_callback(fn):为该 Future 代表的线程任务注册一个“回调函数”,当该任务成功完成时,程序会自动触发该 fn 函数。
 

Future回调

如果程序不希望直接调用 result() 方法阻塞线程,则可通过 Future 的 add_done_callback() 方法来添加回调函数,该回调函数形如 fn(future)。当线程任务完成后,程序会自动触发该回调函数,并将对应的 Future 对象作为参数传给该回调函数。

# encoding:utf-8
from concurrent.futures import ThreadPoolExecutor
import threading
import time

# 定义一个准备作为线程任务的函数
def action(max):
    my_sum = 0
    for i in range(max):
        # print(threading.current_thread().name + '  ' + str(i))
        my_sum += i
    return my_sum
# 创建一个包含2条线程的线程池
with ThreadPoolExecutor(max_workers=2) as pool:
    # 向线程池提交一个task, 50会作为action()函数的参数
    future1 = pool.submit(action, 50)
    # 向线程池再提交一个task, 100会作为action()函数的参数
    future2 = pool.submit(action, 100)
    def get_result(future):
        print(future.result())
    # 为future1添加线程完成的回调函数
    future1.add_done_callback(get_result)
    # 为future2添加线程完成的回调函数
    future2.add_done_callback(get_result)
    print('--------------')

多进程

Python的threading包主要运用多线程的开发,但由于GIL的存在,Python中的多线程其实并不是真正的多线程,如果想要充分地使用多核CPU的资源,大部分情况需要使用多进程。在Python 2.6版本的时候引入了multiprocessing包,它完整的复制了一套threading所提供的接口方便迁移。唯一的不同就是它使用了多进程而不是多线程。每个进程有自己的独立的GIL,因此也不会出现进程之间的GIL争抢。

Process创建进程

注意:在windows中Process()必须放到if __name__=='__main__':下

# encoding:utf-8
from multiprocessing import Process
import os
def run_proc(name):
    print('Run child process %s (%s)...' % (name, os.getpid()))
if __name__=='__main__':
    print('Parent process %s.' % os.getpid())
    p = Process(target=run_proc, args=('test',))
    print('Child process will start.')
    p.start()
    p.join()
print('Child process end.')

multiprocessing模块

multiprocessing模块就是跨平台版本的多进程模块。multiprocessing模块封装了fork()调用,使我们不需要关注fork()的细节。由于Windows没有fork调用,因此,multiprocessing需要“模拟”出fork的效果。

Pool类(进程池)

Pool类用于需要执行的目标很多,而手动限制进程数量又太繁琐时,如果目标少且不用控制进程数量则可以用Process类。Pool可以提供指定数量的进程,供用户调用,用于创建管理进程池。

pool.map

# encoding:utf-8
# -*- coding:utf-8 -*-
# Pool+map
import datetime
import os
from multiprocessing import Pool
from time import sleep
def test(i):
    print(i)
    sleep(1*5)
    print('Run child process  (%s)...' % (  os.getpid()))
if __name__ == "__main__":
    lists = range(30)
    pool = Pool(8)
    t1=datetime.datetime.now()
    pool.map(test, lists)
    pool.close()
    pool.join()
    t2 = datetime.datetime.now()
    print(f'cost==={t2-t1}')

pool.apply_async

# encoding:utf-8
# -*- coding:utf-8 -*-
# 异步进程池(非阻塞)
from multiprocessing import Pool
def test(i):
    print(i)
if __name__ == "__main__":
    pool = Pool(8)
    for i in range(100):
        pool.apply_async(test, args=(i,))
    #子进程和父进程是异步的
    print("test")
    pool.close()
    pool.join()

pool.apply

# encoding:utf-8
# -*- coding:utf-8 -*-
# 异步进程池(非阻塞)
from multiprocessing import Pool
def test(i):
    print(i)
if __name__ == "__main__":
    pool = Pool(8)
    for i in range(100):
        pool.apply(test, args=(i,))
    #在线程池里的任务执行完毕后才执行下面的打印
    print("test")
    pool.close()
    pool.join()

Queue类(进程资源共享)

用于进程通信,资源共享
在使用多进程的过程中,最好不要使用共享资源。普通的全局变量是不能被子进程所共享的,只有通过Multiprocessing组件构造的数据结构可以被共享。

Queue是用来创建进程间资源共享的队列的类,使用Queue可以达到多进程间数据传递的功能(缺点:只适用Process类,不能在Pool进程池中使用)。

Queue

# encoding:utf-8
from multiprocessing import Process, Queue
import os, time, random
def write(q):
    print('Process to write: %s' % os.getpid())
    for value in ['A', 'B', 'C']:
        print('Put %s to queue...' % value)
        q.put(value)
        time.sleep(random.random())

def read(q):
    print('Process to read: %s' % os.getpid())
    while True:
        value = q.get(True)
        print('Get %s from queue.' % value)

if __name__ == "__main__":
    q = Queue()
    pw = Process(target=write, args=(q,))
    pr = Process(target=read, args=(q,))
    pw.start()
    pr.start()
    pw.join()  # 等待pw结束
    pr.terminate()  # pr进程里是死循环,无法等待其结束,只能强行终止

JoinableQueue 

JoinableQueue就像是一个Queue对象,但队列允许项目的使用者通知生成者项目已经被成功处理。通知进程是使用共享的信号和条件变量来实现的。

# encoding:utf-8
from multiprocessing import Process, JoinableQueue
import time, random
def consumer(q):
    while True:
        res = q.get()
        print('消费者拿到了 %s' % res)
        q.task_done()
def producer(seq, q):
    for item in seq:
        time.sleep(random.randrange(1,2))
        q.put(item)
        print('生产者做好了 %s' % item)
    q.join()
if __name__ == "__main__":
    q = JoinableQueue()
    seq = ('产品%s' % i for i in range(5))
    p = Process(target=consumer, args=(q,))
    p.daemon = True  # 设置为守护进程,在主线程停止时p也停止,但是不用担心,producer内调用q.join保证了consumer已经处理完队列中的所有元素
    p.start()
    producer(seq, q)
    print('主线程')

Value,Array(进程资源共享)

用于进程通信,资源共享,注意:Value和Array只适用于Process类
multiprocessing 中Value和Array的实现原理都是在共享内存中创建ctypes()对象来达到共享数据的目的,两者实现方法大同小异,只是选用不同的ctypes数据类型而已。

# encoding:utf-8
import multiprocessing
def f(n, a):
    n.value = 3.14
    a[0] = 5
if __name__ == '__main__':
    num = multiprocessing.Value('d', 0.0)
    arr = multiprocessing.Array('i', range(10))
    p = multiprocessing.Process(target=f, args=(num, arr))
    p.start()
    p.join()
    print(num.value)
    print(arr[:])

Pipe(进程资源共享)

用于管道通信
多进程还有一种数据传递方式叫管道原理和Queue相同。
Pipe可以在进程之间创建一条管道,并返回元组(conn1,conn2),其中conn1,conn2表示管道两端的连接对象,强调一点:必须在产生Process对象之前产生管道。

# encoding:utf-8
from multiprocessing import Process, Pipe
import time
# 子进程执行方法
def f(Subconn):
    time.sleep(1)
    Subconn.send("吃了吗")
    print("来自父亲的问候:", Subconn.recv())
    Subconn.close()

if __name__ == "__main__":
    parent_conn, child_conn = Pipe()  # 创建管道两端
    p = Process(target=f, args=(child_conn,))  # 创建子进程
    p.start()
    print("来自儿子的问候:", parent_conn.recv())
    parent_conn.send("嗯")

 Manager(进程资源共享)

用于资源共享
Manager()返回的manager对象控制了一个server进程,此进程包含的python对象可以被其他的进程通过proxies来访问。从而达到多进程间数据通信且安全。Manager模块常与Pool模块一起使用

SyncManager,以下类型均不是进程安全的,需要加锁.

Array(self,*args,**kwds)
BoundedSemaphore(self,*args,**kwds)
Condition(self,*args,**kwds)
Event(self,*args,**kwds)
JoinableQueue(self,*args,**kwds)
Lock(self,*args,**kwds)
Namespace(self,*args,**kwds)
Pool(self,*args,**kwds)
Queue(self,*args,**kwds)
RLock(self,*args,**kwds)
Semaphore(self,*args,**kwds)
Value(self,*args,**kwds)
dict(self,*args,**kwds)
list(self,*args,**kwds)

# encoding:utf-8
import multiprocessing
def f(x, arr, l, d, n):
    x.value = 3.14
    arr[0] = 5
    l.append('Hello')
    d[1] = 2
    n.a = 10

if __name__ == '__main__':
    server = multiprocessing.Manager()
    x = server.Value('d', 0.0)
    arr = server.Array('i', range(10))
    l = server.list()
    d = server.dict()
    n = server.Namespace()
    proc = multiprocessing.Process(target=f, args=(x, arr, l, d, n))
    proc.start()
    proc.join()
    print(x.value)
    print(arr)
    print(l)
    print(d)
    print(n)

同步子进程模块

用法都和多线程中的用法类似,只是用的是multiprocessing中的类

Lock(互斥锁)
#from multiprocessing import Process, Lock
RLock(可重入的互斥锁(同一个进程可以多次获得它,同时不会造成阻塞)
#from multiprocessing import Process, RLock
Semaphore(信号量)
#from multiprocessing import Process, Semaphore
Condition(条件变量)
#import multiprocessing
Event(事件)
#import multiprocessing

# encoding:utf-8
from multiprocessing import Process, Lock
def l(lock, num):
    lock.acquire()
    print("Hello Num: %s" % (num))
    lock.release()
if __name__ == '__main__':
    lock = Lock()  # 这个一定要定义为全局
    for num in range(20):
        Process(target=l, args=(lock, num)).start()

Python并发之concurrent.futures

Python标准库为我们提供了threading和multiprocessing模块编写相应的多线程/多进程代码。从Python3.2开始,标准库为我们提供了concurrent.futures模块,它提供了ThreadPoolExecutor和ProcessPoolExecutor两个类,实现了对threading和multiprocessing的更高级的抽象,对编写线程池/进程池提供了直接的支持。

ThreadPoolExecutor对象
ThreadPoolExecutor类是Executor子类,使用线程池执行异步调用。

class concurrent.futures.ThreadPoolExecutor(max_workers)

ProcessPoolExecutor对象
ThreadPoolExecutor类是Executor子类,使用进程池执行异步调用。

class concurrent.futures.ProcessPoolExecutor(max_workers=None)

使用方法

以下方法都和多线程的用法一致,只是用concurrent.futures中的类

submit()方法
map()方法
shutdown()方法
Future

# encoding:utf-8
from concurrent import futures
def test():
    import time
    return time.ctime()

if __name__ == '__main__':
    with futures.ProcessPoolExecutor(max_workers=1) as executor:
        future = executor.submit(test)
        print(future.result())

协程

协程:也称微线程,执行效率高,子程序切换不是线程切换,没有线程切换的消耗,线程数量越多协程的性能优势越明显;协程中共享资源不需要锁,协程是一个线程,可通过多进程 + 协程充分利用多核 CPU

优点

1.无需线程上下文切换的开销
2.无需原子操作锁定及同步的开销
3.方便切换控制流,简化编程模型
4.高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。

所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。
原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序是不可以被打乱,或者切割掉只执行部分。视作整体是原子性的核心。

缺点

1.无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
2.进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序

协程基础

Python对协程的支持是通过generator实现的。generator也叫生成器。

通过gevent实现协程

基于greenlet

gevent它是一个并发网络库。它的协程是基于greenlet的,并基于libev实现快速事件循环

其基本思想是:
当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。

spawn构建新协程

# encoding:utf-8
import gevent
def foo():
    print('running in foo')
    gevent.sleep(2)#模拟io
    print('com back from bar in to foo')
    return 'foo'

def bar():
    print('running in bar')
    gevent.sleep(1)#模拟io
    print('com back from foo in to bar')
    return 'bar'

def func():
    print('in func of no io')
    return 'func'

def fund():
    print('in fund of no io')
    return 'fund'

jobs=[gevent.spawn(foo),gevent.spawn(bar),gevent.spawn(func),gevent.spawn(fund)]
gevent.joinall(jobs)
for job in jobs:
    print(job.value) #能够保证返回的顺序

monkey.pach_all

将第三方库标记为IO非阻塞

由于切换是在IO操作时自动完成,所以gevent需要修改Python自带的一些标准库,这一过程在启动时通过monkey patch完成。

协程之间切换的条件:
gevent.sleep():耗时等待的情况下才会切换。

gevent的程序补丁:
gevent.monkey.patch_all()  # 导入这个后,就可以不需要用gevent.sleep()了,只要是耗时,就能切换任务。

# encoding:utf-8
from gevent import monkey; monkey.patch_socket()
import gevent
def f(n):
    for i in range(n):
        print (gevent.getcurrent(), i)
        gevent.sleep(0)

g1 = gevent.spawn(f, 5)
g2 = gevent.spawn(f, 5)
g3 = gevent.spawn(f, 5)
g1.join()
g2.join()
g3.join()

多协程下载图片

# encoding:utf-8
import gevent
from gevent import monkey
import urllib.request
# 有IO才做时需要这一句
monkey.patch_all()  # 将程序中用到的耗时操作的代码,换为gevent中自己实现的模块
def my_download(file_name, url):
    """
    网上获取数据,并保存本地
    :param file_name: 保存的文件名
    :param url: 网址
    :return:
    """
    print("Get : %s " % url)
    resp = urllib.request.urlopen(url)
    data = resp.read()
    with open(file_name, "wb") as f:
        f.write(data)
    print(file_name, "save ok, %d bytes received from %s." % (len(data), url))

def main():
    # 添加所有协程任务,并等待各个协程任务结束
    gevent.joinall([
        gevent.spawn(my_download, "1.jpg",
                     "https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=617100489,3848171650&fm=26&gp=0.jpg"),
        gevent.spawn(my_download, "2.jpg",
                     "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fdik.img.kttpdq.com%2Fpic%2F3%2F1812%2F15cdcfba19bd15b4_1680x1050.jpg&refer=http%3A%2F%2Fdik.img.kttpdq.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1615523263&t=8ef1c78524053592f5f8148b83def2c4"),
        gevent.spawn(my_download, "3.jpg",
                     "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fdik.img.kttpdq.com%2Fpic%2F3%2F1630%2Fe5428d952b120906.jpg&refer=http%3A%2F%2Fdik.img.kttpdq.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1615523263&t=0f8853c8f464a7f3641aadf3423bf75d")

    ])
if __name__ == '__main__':
    main()

协程池

通过协程池控制协程数目

上面的图片下载通过谢程程控制,代码如下

# encoding:utf-8
import gevent
from gevent import monkey
import urllib.request
# 有IO才做时需要这一句
from gevent.pool import Pool

monkey.patch_all()  # 将程序中用到的耗时操作的代码,换为gevent中自己实现的模块
def my_download(file_name, url):
    """
    网上获取数据,并保存本地
    :param file_name: 保存的文件名
    :param url: 网址
    :return:
    """
    print("Get : %s " % url)
    resp = urllib.request.urlopen(url)
    data = resp.read()
    with open(file_name, "wb") as f:
        f.write(data)
    print(file_name, "save ok, %d bytes received from %s." % (len(data), url))

def main():
    # 添加所有协程任务,并等待各个协程任务结束
    pool = Pool(5)
    threads = [
        pool.spawn(my_download, "1.jpg",
                     "https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=617100489,3848171650&fm=26&gp=0.jpg"),
        pool.spawn(my_download, "2.jpg",
                     "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fdik.img.kttpdq.com%2Fpic%2F3%2F1812%2F15cdcfba19bd15b4_1680x1050.jpg&refer=http%3A%2F%2Fdik.img.kttpdq.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1615523263&t=8ef1c78524053592f5f8148b83def2c4"),
        pool.spawn(my_download, "3.jpg",
                     "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fdik.img.kttpdq.com%2Fpic%2F3%2F1630%2Fe5428d952b120906.jpg&refer=http%3A%2F%2Fdik.img.kttpdq.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1615523263&t=0f8853c8f464a7f3641aadf3423bf75d")
    ]
    gevent.joinall(threads)

if __name__ == '__main__':
    main()

多进程+协程

多进程+协程下,避开了CPU切换的开销,又能把多个CPU充分利用起来,这种方式对于数据量较大的爬虫还有文件读写之类的效率提升是巨大的。

# encoding:utf-8
# -*- coding=utf-8 -*-
import requests
from multiprocessing import Process
import gevent
from gevent import monkey;
monkey.patch_all()
def fetch(url):
    try:
        s = requests.Session()
        r = s.get(url, timeout=1)  # 在这里抓取页面
    except Exception as e:
        print(str(e))
    return ''


def process_start(url_list):
    tasks = []
    for url in url_list:
        tasks.append(gevent.spawn(fetch, url))
    gevent.joinall(tasks)  # 使用协程来执行


def task_start(filepath, flag=100000):  # 每10W条url启动一个进程
    with open(filepath, 'r') as reader:  # 从给定的文件中读取url
        url = reader.readline().strip()
        url_list = []  # 这个list用于存放协程任务
        i = 0  # 计数器,记录添加了多少个url到协程队列
        while url != '':
            i += 1
            url_list.append(url)  # 每次读取出url,将url添加到队列
            if i == flag:  # 一定数量的url就启动一个进程并执行
                p = Process(target=process_start, args=(url_list,))
                p.start()
                url_list = []  # 重置url队列
                i = 0  # 重置计数器
            url = reader.readline().strip()
        if url_list :  # 若退出循环后任务队列里还有url剩余
            p = Process(target=process_start, args=(url_list,))  # 把剩余的url全都放到最后这个进程来执行
            p.start()


if __name__ == '__main__':
    task_start('./testData.txt')  # 读取指定文件