并发编程(2)、多线程 & 进程池线程池

线程理论

特点
1.开启一个进程时,一般会开启多个线程;
2.一个进程下的多个线程共享进程的内存空间;
3.开启线程比开启进程开销更小.

举例:开启一个字处理软件进程,该进程肯定需要办不止一件事情,比如监听键盘输入,处理文字,定时自动将文字保存到硬盘,这三个任务操作的都是同一块数据,因而不能用多进程。只能在一个进程里并发地开启三个线程,如果是单线程,那就只能是,键盘输入时,不能处理文字和自动保存,自动保存时又不能输入和处理文字。

工作方式:使用多个线程和进程时,主机操作系统负给每个进程和线程分配一个小的时间片,并在所有活动任务之间快速循环,给每个任务分配一个可用的cpu周期.

线程共享内存示例:

from threading import Thread

n = 100

def run(m):
    global n
    n = m

if __name__ == '__main__':
    t = Thread(target=run, args=(10,))
    t1 = Thread(target=run, args=(20,))
    t.start()
    t.join()
    t1.start()
    t1.join()
    print('n:',n )  # 20

创建线程开销小:创建一个进程,就是创建一个车间,涉及到申请空间,而且在该空间内建至少一条流水线,但是创建线程,就只是在一个车间内造一条流水线,无需申请空间,所以创建开销小.

# 线程开启的要比进程的快,从主进程打印顺序的前后就可以看出
from threading import Thread
from multiprocessing import Process

def run():
    print('run>>>>>>')

if __name__ == '__main__':
    # t = Thread(target=run)
    # t.start()
    '''
    run>>>>>>
    主
    '''
    p = Process(target=run)
    p.start()
    print('主')
    '''
    主
    run>>>>>>
    '''

Thread模块

方法
isAlive(): 返回线程是否活动的;
getName(): 返回线程名;
setName(): 设置线程名;
threading.currentThread(): 返回当前的线程变量;
threading.enumerate():返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程;
threading.activeCount():返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。

from threading import (Thread, activeCount, enumerate, current_thread)
import os
import time

def run():
    print('子线程<%s> start' % current_thread().name)
    time.sleep(2)
    print('子线程<%s> finish' % current_thread().name)

if __name__ == '__main__':
    t = Thread(target=run)
    t.start()

    # 返回当前活跃的线程数量
    # print(activeCount())

    # 返回当前进程活跃的线程对象列表
    print(' 当前活跃线程列表: %s' % enumerate())
    t.join()
    print('子线程是否存活' % t.isAlive())
    print('主线程<%s> 结束' % current_thread().name)

守护线程

无论是线程还是进程, 都遵循守护xx会在主xx运行完毕后被销毁;

线程 & 进程运行完毕的区别

主进程在其代码结束后就已经算运行完毕了,直接回收守护子进程,然后等非守护的子进程都运行完毕后回收资源.
主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收).

from threading import Thread
from multiprocessing import Process
import time

def foo():
    print(123)
    time.sleep(1)
    print("end123")

def bar():
    print(456)
    time.sleep(2)
    print("end456")
    # t2运行完毕, 此时切换到主线程,直接回收守护线程thread<t1>

if __name__ == '__main__':
    # t1=Thread(target=foo)
    t1=Process(target=foo)
    # t2=Thread(target=bar)
    t2=Process(target=bar)

    t1.daemon=True
    t1.start()
    t2.start()
    print("main-------")
    # 等待所有非守护子线程运行完毕, 即thread<t2>

线程互斥锁

功能同进程互斥锁一样.

from threading import Thread, Lock
import time

n = 100

def run(mutex):
    global n
    time.sleep(0.1)
    mutex.acquire()
    # 此处若修改涉及io操作就很明显了
    n -= 1
    print(n)
    mutex.release()

if __name__ == '__main__':
    mutex = Lock()
    t_lst = []
    for i in range(100):
        t = Thread(target=run, args=(mutex, ))
        # 加入线程列表中
        t_lst.append(t)
        # 启动每个线程
        t.start()

    for i in t_lst:
        i.join()

    print('主线程: n=%s' %n)

全局解释器锁GIL

代码执行过程:

  1. 子线程先拿到用户代码执行权限;
  2. 再去解释器拿解释器代码的执行权限;
  3. 拿到解释器权限后将用户代码作为参数传递给解释器,由解释器执行用户代码.

下图所示,保证python解释器同一时间只能执行一个任务的代码
GIL解释器锁.png

扫描二维码关注公众号,回复: 2782782 查看本文章

GILlock是两把锁,保护的数据不一样.前者是解释器级别的,保护的是解释器级别的数据,比如垃圾回收,后者是保护用户自己开发的应用程序的数据.


多线程 & 多进程 选择

多线程用于IO密集型,例如socket/爬虫/web;
多进程用户计算密集型,例如金融分析.

# i/o密集型
from multiprocessing import Process
from threading import Thread
import threading
import os, time

def work():
    print('子进程<%s> 开始'% os.getpid())
    time.sleep(3)
    print('子进程<%s> ================='% os.getpid())

if __name__ == '__main__':
    l = []
    print(os.cpu_count())  # 本机为4核
    start = time.time()
    for i in range(100):
        # 多进程: 8.8s
        # 1. 创建进程耗费时间;
        # 2. 四核并行, 遇到io阻塞切换较慢
        p = Process(target=work)

        # 多线程: 3.05s
        # 1. 所有线程并发启动程序
        # 2. 遇到阻塞切换速度快
        # p=Thread(target=work)
        l.append(p)
        p.start()
    for p in l:
        p.join()
    stop = time.time()
    print('run time is %s' % (stop - start))


# 计算密集型
from multiprocessing import Process
from threading import Thread
import os,time

def work():
    print('子进程<%s>' % os.getpid())
    res=0
    for i in range(100000000):
        res += i
    print('子进程<%s> %s' % (os.getpid(), res))


if __name__ == '__main__':
    l=[]
    print(os.cpu_count()) #本机为4核
    start=time.time()
    for i in range(4):
        # 耗时14.3s多,开始四个进程,运用四个cpu同时运算
        p=Process(target=work)

        # 耗时26.8s多,因为一个进程内,同一时刻,
        # 只能有一个线程使用cpu计算,没有发挥多核优势
        # p=Thread(target=work)
        l.append(p)
        p.start()

    for p in l:
        p.join()
    stop=time.time()
    print('run time is %s' %(stop-start))

死锁Lock

两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象.若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁, 这些永远在互相等待的进程称为死锁进程.
互斥锁只能aquire()一次,想要再次拿到必须先释放掉.

from threading import Thread, Lock
import time

mutexA = Lock()
mutexB = Lock()


class MyThread(Thread):
    def run(self):
        self.func1()
        self.func2()

    def func1(self):
        mutexA.acquire()
        print('\033[41m%s 拿到A锁\033[0m' % self.name)

        mutexB.acquire()
        print('\033[42m%s 拿到B锁\033[0m' % self.name)
        mutexB.release()

        mutexA.release()

    def func2(self):
        '''
        1. thread1先执行,会优先拿到B锁,
        2. thread2执行func1,拿到A锁, 等待thread1释放B锁
        3. thread1继续运行,准备到A锁, 等待thread2释放A锁
        4. 两个线程执手相看泪眼, 就这么一直耗着!
         '''
        mutexB.acquire()
        print('\033[43m%s 拿到B锁\033[0m' % self.name)
        time.sleep(2)

        mutexA.acquire()
        print('\033[44m%s 拿到A锁\033[0m' % self.name)
        mutexA.release()

        mutexB.release()


if __name__ == '__main__':
    for i in range(3):
        t = MyThread()
        t.start()

递归锁Rlock

可以被同一个线程获取多次,每被获取一次内置计数器加1,在此期间其他线程不能获取,直到计数器为0.

from threading import Thread, RLock
import time

mutexA = mutexB = RLock()

class MyThread(Thread):
    def run(self):
        self.func1()
        self.func2()

    def func1(self):
        """一个线程拿到锁,counter加1,该线程内又碰到加锁的情况,则counter继续加1,
        这期间所有其他线程都只能等待,等待该线程释放所有锁,即counter递减到0为止
        """
        print('<%s>f1' % self.name)
        mutexA.acquire()
        print('<%s>f1 拿到A锁' %self.name)

        mutexB.acquire()
        print('<%s>f1 拿到B锁' % self.name)
        mutexB.release()
        print('<%s>f1 释放B锁' %self.name)

        mutexA.release()
        print('<%s>f1 释放A锁' %self.name)

    def func2(self):
        '''一旦释放完后,所有的线程都公竞争抢,谁先抢到谁使用'''
        print('<%s>f2' % self.name)
        mutexB.acquire()
        print('<%s>f2 拿到B锁' % self.name)
        # 抢到 B锁 的线程已经绑定R锁,
        # 其他的线程必须等你释放所有的acquire才能继续抢
        print('<%s>f2 阻塞' % self.name)
        time.sleep(2)
        mutexA.acquire()
        print('<%s>f2 拿到A锁' %self.name)
        mutexA.release()
        print('<%s>f2 释放A锁' % self.name)
        mutexB.release()
        print('<%s>f2 释放B锁' %self.name)

信号量Semaphore

也是一把锁,但是可以允许设定最大值.
例如s_lock = Semaphore(5) 可以允许最多同时被5个线程抢到并执行.
Semaphore管理一个内置的计数器,每当调用acquire()时内置计数器-1
调用release()时内置计数器+1;计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其他线程调用release()

from threading import Thread, Semaphore, Lock
import threading
import time

def func():
    print('准备抢锁')
    sm.acquire()
    print('<%s> 拿到锁' % threading.current_thread().getName())
    time.sleep(2)
    # 待阻塞过后,随机释放内部线程的锁,空出位置后,
    # 同时, 其他的线程又可以抢锁,进入sm中;
    sm.release()
    print('<%s>      释放锁' % threading.current_thread().getName())

if __name__ == '__main__':
    sm = Semaphore(2)
    # sm = Lock()
    for i in range(5):
        t = Thread(target=func)
        t.start()

信号Event

线程之间根据event对象的状态来执行,其他线程也可以设置event对象的状态;
Event对象中的信号标志默认为False
如果有线程等待一个Event对象,而这个Event对象的标志为False,那么这个线程将会被一直阻塞直至该标志为True;
一个线程如果将一个Event对象的信号标志设置为True,它将唤醒所有等待这个Event对象的线程;
如果一个线程等待一个已经被设置为True的Event对象,那么它将忽略这个事件,继续执行;

方法
event = Event(): 设置信号对象event
event.is_set() = False: 默认为False
event.set(): 设置eventTrue

from threading import Thread, Event
import threading
import time

def conn_mysql():
    count = 1
    # 若event的状态值为False则执行;
    while not event.is_set():
        if count > 3:
            raise TimeoutError('链接超时')

        print('<%s>第%s次尝试链接'%(threading.current_thread().getName(), count))

        # 如果event.is_set()==False将阻塞线程, 若期间event被设置为True,则立即结束等待,往下执行
        event.wait(3)
        count += 1

    print('<%s>链接成功' % threading.current_thread().getName())


def check_mysql():
    print('\033[45m[%s]正在检查mysql\033[0m' % threading.current_thread().getName())
    time.sleep(5)
    event.set() # 设置event状态为True,所有阻塞池的线程激活进入就绪状态,等待操作系统调用


if __name__ == '__main__':
    event = Event()
    conn1 = Thread(target=conn_mysql)
    conn2 = Thread(target=conn_mysql)
    check = Thread(target=check_mysql)

    conn1.start()
    conn2.start()
    check.start()

定时器

指定n秒之后执行某个动作,实质上是开启了子线程取执行动作.
格式: t = Timer(interval, func)

from threading import Timer, current_thread

def say_hi():
    print('<%s> hello world!' % current_thread().name)

# 指定1秒之后执行sayhi这个函数
t = Timer(2, say_hi)
t.start()
print('<%s> 主线程' % current_thread().name)

'''
<MainThread> 主线程
<Thread-1> hello world!
'''

随机验证码定时刷新

import random
import string
from threading import Timer


class Code:
    def __init__(self):
        self.make_cache()

    def make_cache(self, interval=6):
        """6s后会自动生成随机生成验证码"""
        self.cache = self.make_code()
        self.t = Timer(interval, self.make_cache)
        self.t.start()

    def make_code(self, n=4):
        res = ''
        for i in range(n):
            s = str(random.choice(string.hexdigits))
            res += s
        print('\n'+res)
        return res

    def check(self):
        while True:
            code = input('请输入您的验证码>>> ').strip()
            if code == self.cache:
                print('验证码输入正确!')
                # 取消已设定的定时器
                self.t.cancel()
                break

if __name__ == '__main__':
    c = Code()
    c.check()

线程Queue

同进程的Queue类似,可以实现线程之间安全通讯.
常用方法
q.put: 用以插入数据到队列中,put方法还有两个可选参数:blockedtimeout.如果blocked=True(默认值),并且timeout为正值,该方法会阻塞timeout指定的时间,直到该队列有剩余的空间,如果超时,会抛出Queue.Full异常;如果blocked=False,但该Queue已满,会立即抛出Queue.Full异常.
q.get: 可以从队列读取并且删除一个元素。同样,get方法有两个可选参数:blockedtimeout。如果blocked=True(默认值),并且timeout为正值,那么在等待时间内没有取到任何元素,会抛出Queue.Empty异常。如果blocked=False,有两种情况存在,如果Queue有一个值可用,则立即返回该值,否则,如果队列为空,则立即抛出Queue.Empty异常;
q.get_nowait():同q.get(False)
q.put_nowait():同q.put(False)
q.empty(): 调用此方法时q为空则返回True,该结果不可靠,比如在返回True的过程中,如果队列中又加入了项目。
q.full(): 调用此方法时q已满则返回True,该结果不可靠,比如在返回True的过程中,如果队列中的项目被取走。
q.qsize(): 返回队列中目前项目的正确数量,结果也不可靠,理由同q.empty()q.full()一样.

队列queue.Queue(),先进先出:

import queue

q=queue.Queue()
q.put(1)
q.put(2)
q.put(3)

print(q.get())
print(q.get())
print(q.get())

堆栈: class queue.LifoQueue(maxsize=0),先进后出:

import queue

q=queue.LifoQueue()
q.put('1')
q.put('2')
q.put('3')

print(q.get())
print(q.get())
print(q.get())

优先级队列: class queue.PriorityQueue(maxsize=0): 存储数据时可设置优先级的队列.

import queue

q=queue.PriorityQueue()
# put进入一个元组,元组的第一个元素是优先级(通常是数字,也可以是非数字之间的比较),
# 数字越小优先级越高
q.put((20,'22'))
q.put((1,'100'))
q.put((100,'2'))

print(q.get())
print(q.get())
print(q.get())

'''
结果(数字越小优先级越高,优先级高的优先出队):
(1, '100')
(20, '22')
(100, '2')
'''

进程池和线程池

限定同时运行的最大线程和进程个数,不是传统意义上的开启线程和进程.
concurrent.futures 模块提供了高度封装的异步调用接口;
ThreadPoolExecutor 线程池模块;
ProcessPoolExecutor 进程池模块;

常用方法
submit(fn, *args, **kwargs):异步提交任务;
map(func, *iterables, timeout=None, chunksize=1)
shutdown(wait=True): 相当于进程池的pool.close()+pool.join()操作;
result(timeout=None): 取得结果;
add_done_callback(fn): 回调函数;

创建线程池

# 线程池创建
from concurrent.futures import ThreadPoolExecutor
pool = ThreadPoolExecutor(10)       
pool.submit(func, *args)  # 此时会立即创建并启动线程

进程池实例化

from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import os, time
from multiprocessing import Process, current_process

def task(n):
    time.sleep(1)
    print('线程<%s><%s>正在运行' %(current_process().name, os.getpid()))
    return n**2


if __name__ == '__main__':
    # 生成进程池对象,最大同时运行进程数目为4
    pool = ProcessPoolExecutor(4)
    futures = []
    for i in range(5):
        # 1. 将任务及其参数扔进进程池
        # 2. 进程池自行启动进程
        # 3. 返回执行结果
        future = pool.submit(task, i)
        futures.append(future)

    # 1. 等待进程池内的进程执行完
    # 2. 关闭关闭进程池
    pool.shutdown(wait=True)
    print('进程池内部进程运行完毕')
    for f in futures:
        # 取得返回结果
        print(type(f), f.result())

    print('主')

改进版本:可以运用map(func, *iterables,timeout=None,chunksize=1)取代for循环submit的操作.

from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import os, time


def task(n):
    time.sleep(1)
    print('进程<%s>正在运行'  % os.getpid())
    return n**2


if __name__ == '__main__':
    # 生成进程池子
    process_pool = ProcessPoolExecutor(max_workers=4)
    futures = []

    # 取代for循环,加入4个进程任务到进程池子
    process_pool.map(task, range(1, 5))

    # 等待池内所有任务执行完毕回收完资源后才继续
    process_pool.shutdown(wait=True)
    print('主进程<%s>' % os.getpid())

线程池同步调用示例:串行执行线程池内的线程

import time
import random
from concurrent.futures import ThreadPoolExecutor, thread

def get_ticket(name):
    '''购票'''
    print('%s 购票中' % name)
    time.sleep(random.randint(2,4))
    price = random.randint(1000, 2000)
    return {'name':name, 'price':price}


def board(ticket):
    '''选座位'''
    name = ticket['name']
    seat_num = random.randint(1, 100)
    time.sleep(1)
    print('%s, 您的座位是%s' % (name, seat_num))


if __name__ == '__main__':
    # 生成线程池
    pool = ThreadPoolExecutor(10)
    # 同步调用,提交任务后,由于要获取结果,所以在原地阻塞,一旦拿到结果,立即执行下一行代码
    ticket1 = pool.submit(get_ticket, 'alex').result()
    ticket2 = pool.submit(get_ticket, 'jim').result()
    ticket3 = pool.submit(get_ticket, 'bob').result()
    board(ticket1)
    board(ticket2)
    board(ticket3)
    print('购票完成!')

异步调用加回调机制

import time
import random
from concurrent.futures import ThreadPoolExecutor
import threading

def get_ticket(name):
    print('%s 购票中 --<%s>' %
          (name, threading.current_thread().getName()))

    time.sleep(random.randint(2, 4))
    price = random.randint(1000, 2000)
    return {'name':name, 'price':price}

def board(ticket):
    # 传入的是对象,还要拿取返回结果
    ticket = ticket.result()
    name = ticket['name']
    seat_num = random.randint(1, 100)
    time.sleep(1)
    print('%s, 您的座位是%s' % (name, seat_num))

if __name__ == '__main__':
    pool = ThreadPoolExecutor(10)
    # 将自身作为对象传给回调函数`add_done_callback()`作为参数
    pool.submit(get_ticket, 'alex').add_done_callback(board)
    pool.submit(get_ticket, 'jim').add_done_callback(board)
    pool.submit(get_ticket, 'kate').add_done_callback(board)
    pool.submit(get_ticket, 'bob').add_done_callback(board)

进程池线程池小练习:多线程异步抓取网络数据

import requests
import time
from concurrent.futures import ThreadPoolExecutor
from threading import current_thread

def get(url):
    print('<%s> 正在获取%s的信息' %(current_thread().getName(), url))
    time.sleep(2)
    response = requests.get(url)
    return {'url': url, 'context': response.text}

def parse(future):
    context = future.result()['context']
    url = future.result()['url']
    print('The result of %s is %s'%(url, len(context)))


if __name__ == '__main__':
    urls = [
        'https://www.cnblogs.com/fqh202',
        'http://www.cnblogs.com/fqh202/p/8416062.html',
        'http://www.cnblogs.com/fqh202/p/8409933.html',
        'http://www.cnblogs.com/fqh202/p/8350463.html',
        'http://www.cnblogs.com/fqh202/p/8301777.html',
    ]
    # 生成线程池
    pool = ThreadPoolExecutor(3)
    for url in urls:
        # 1. 往线程池增加线程任务
        # 2. 将线程调用程序返回的结果作为参数传入parse()函数
        pool.submit(get, url).add_done_callback(parse)

模拟socket线程池异步

# 服务端==========================================================
from socket import *
from threading import Thread, current_thread
from concurrent.futures import ThreadPoolExecutor
import os

def run(conn):
    """进入新线程,开始接收数据"""
    while True:
        try:
            print('线程<%s>准备接收数据--------' % os.getpid())
            data = conn.recv(1024)
            if not data:
                # 若没收到数据,有可能另一端管道已经断开连接,
                # 所以这个线程应该断开,否则会无限循环接收数据
                print('线程<%s>断开' % os.getpid())
                break
            send_data = '发送自<多线程><%s>: %s' % (current_thread().name,
                                            data.decode('utf-8'))

            conn.send(send_data.encode('utf-8'))
        except ConnectionResetError:
            break
    # 若客户端那边的连接断开,那么这边的conn也应该断开
    conn.close()

def server(ip, port, pool):
    server = socket(AF_INET, SOCK_STREAM)
    server.bind((ip, port))
    server.listen(5)
    while True:
        # 生成套接字对象, 等待客户上门
        print("进入待连接状态>>> ")
        # 主线程停留于此,一直等待
        conn, addr = server.accept()
        pool.submit(run, conn)
    server.close()

if __name__ == '__main__':
    # 创建线程池,最大运行线程数量为2
    pool = ThreadPoolExecutor(2)
    server('127.0.0.1', 8810, pool)


# 客户端==========================================================
from socket import *

client = socket(AF_INET, SOCK_STREAM)
client.connect(('127.0.0.1', 8810))
while True:
    msg = input('>>> ').strip()
    if not msg: continue
    client.send(msg.encode('utf-8'))

    back_data = client.recv(1024)
    print(back_data.decode('utf-8'))

猜你喜欢

转载自www.cnblogs.com/fqh202/p/9483761.html