菜鸟初学python入门进阶第十节:面向对象,多线程与多进程

1.python中的GIL

python中的一个线程对应于c语言的一个线程(基于cpython)
python前期为了简单,在解释过程中加了一把所,使得一次只能有一个线程运行在1个cpu上
GIL锁使得同一时刻只有一个线程在一个cpu上执行字节码
无法将多个线程映射到多个cpu上

import dis
def foo():
    pass
print(dis.dis(foo))

dis模块可以查看函数的字节码
当把多个函数挂在线程上,函数的执行不是完整而独立的
比如函数有1000行字节码,则有可能先执行100行,再切换到别的线程的函数上再执行100行
但每个线程中的函数的字节码的执行是按照顺序的
依此达到某种程度上的同时进行
其实还是在一条主线程上

当遇到io操作,GIL会将执行权给别的线程(io操作慢)

2.多线程编程

一开始只有进程,进程对系统资源消耗大,所以有了线程
线程依赖于进程,线程间切换开销小于进程间切换

class Thread:
    """A class that represents a thread of control.

    This class can be safely subclassed in a limited fashion. There are two ways
    to specify the activity: by passing a callable object to the constructor, or
    by overriding the run() method in a subclass.

    """
'''1.通过Thread类实例化'''

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, args=('', ))
    thread2 = threading.Thread(target=get_detail_url, args=('', ))
    start_time = time.time()
    thread1.start()
    thread2.start()
     print ("last time: {}".format(time.time()-start_time))


'''2. 通过继承Thread来实现多线程'''
import time
import threading
class GetDetailHtml(threading.Thread):
    def __init__(self, name):
        super().__init__(name=name)

    def run(self):
        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()
    #当主线程退出的时候, 子线程kill掉
    print ("last time: {}".format(time.time()-start_time))

3.线程间通信

每个函数的执行涉及到一些外部变量
如果各个函数字节码胡乱执行,可能导致外部变量的值混乱出错
函数的参数之间要实现某种通信
python的参数传递是引用

Queue是线程安全的queue.get()可以阻塞线程
实际上用了deque实现,deque是字节码级别上的线程安全

#通过queue的方式进行线程间同步
from queue import Queue


import time
import threading


def get_detail_html(queue):
    #爬取文章详情页
    while True:
        url = queue.get()
        # for url in detail_url_list:
        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))
        print("get detail url end")


#1. 线程通信方式- 共享变量

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()
    # # thread2 = GetDetailUrl("get_detail_url")
    start_time = time.time()
    # thread_detail_url.start()
    # thread_detail_url1.start()
    #
    # thread1.join()
    # thread2.join()
    detail_url_queue.task_done()'''发出这个信号才会退出queue的join'''
    detail_url_queue.join()'''从queue的角度阻塞主线程'''

    #当主线程退出的时候, 子线程kill掉
    print ("last time: {}".format(time.time()-start_time))

4.线程同步lock、Rlock

a = 0
def add(a):
    a += 1
def desc(a):
    a -= 1
import dis
print(dis.dis(add))
print(dis.dis(desc))

add(a)
desc(a)

字节码
可见a+=1和a-=1大致分别有4步操作

  5           0 LOAD_FAST                0 (a)   加载a
              2 LOAD_CONST               1 (1)    加载1
              4 INPLACE_ADD                         做加法
              6 STORE_FAST               0 (a)      赋给a
              8 LOAD_CONST               0 (None)
             10 RETURN_VALUE
None
  7           0 LOAD_FAST                0 (a)
              2 LOAD_CONST               1 (1)
              4 INPLACE_SUBTRACT
              6 STORE_FAST               0 (a)
              8 LOAD_CONST               0 (None)
             10 RETURN_VALUE
None

如果线程在函数间随意切换,a极有可能出现问题,比如两个函数通过切换线程先分别先加载了值为0的a,之后又把两个a一个加1一个减1,然后从1和-1中选一个赋给最终的a

在函数中加锁可以只执行在函数被锁住的代码片段的字节码,其间不切换线程

但是加锁和开锁都需要时间,会降低性能
且可能有死锁
1.lock不能嵌套,嵌套会有死锁
2.果两个函数分别需要正在对方函数里的数据,都停住了,结果互相等待,资源竞争,出现死锁

Rlock可以嵌套

from threading import Lock, RLock, Condition #可重入的锁

#在同一个线程里面,可以连续调用多次acquire, 一定要注意acquire的次数要和release的次数相等
total = 0
lock = RLock()
def add():
    #1. dosomething1
    #2. io操作
    # 1. dosomething3
    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()

5.线程同步,condition使用

condition条件变量
用于复杂的线程间同步
如果两个函数的代码要你一段我一段地按顺序执行
锁可以锁住第一段,但不知道下一个获得锁的是哪个函数的代码段

通过condition完成协同读诗
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)

    启动顺序很重要
    在调用with cond之后才能调用wait或者notify方法
    用了with上下文管理器就不需要acquire和release这个cond了
    condition有两层锁, notify之后会找当前唤醒wait的下一个wait,
    并把下一个wait加入等候队列、加锁(下一层),然后把当前的wait上的锁(上一层)打开
    
    xiaoai.start()
    tianmao.start()

condition内部还是调用了lock、Rlock实现的
notify会开启正在wait的线程

6.Semaphore使用

semaphore是用于控制进入数量的锁
比如爬虫控制并发线程的数量,防止被ban

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()

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

    def run(self):
        for i in range(20):
            self.sem.acquire()
            html_thread = HtmlSpider("https://baidu.com/{}".format(i), self.sem)
            html_thread.start()

if __name__ == "__main__":
    sem = threading.Semaphore(3)
    url_producer = UrlProducer(sem)
    url_producer.start()

实际上semaphore还是调用了condition来实现的
判断是否已经满足最大并发量,满足了就wait
Queue里边也调用了condition

7.线程池编程

调用from concurrent import futures
线程池并不止提供并发数量限制,还可以获取某一个线程的状态或某一个任务的状态,以及返回值
当一个线程完成时主线程能立即知道
futures可以让多线程和多进程编码接口一致

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())
print(task2.cancel())'''取消任务,注意一定要在任务开始前取消 不然取消失败'''
time.sleep(3)
print(task1.done())

'''result方法可以获取task的执行结果   阻塞'''
print(task1.result())

如果要阻塞所有线程并了解已经成功的线程,可以

from concurrent.futures import ThreadPoolExecutor, as_completed
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))

'''1.用as_complete'''
urls = [3,2,4]
all_task = [executor.submit(get_html, (url)) for url in urls]
for future in as_completed(all_task):
    data = future.result()
    print("get {} page".format(data))

'''2.通过executor的map获取已经完成的task的值 
     注意这样输出的顺序是按照列表的url顺序来的'''
for data in executor.map(get_html, urls):
    print("get {} page".format(data))

关于wait函数
可以让主线程阻塞,可以指定主线程必须在某一个线程执行完之后再继续

from concurrent.futures import wait, FIRST_COMPLETED

wait(all_task, return_when=FIRST_COMPLETED)
print("main")

8.多进程编程

对于某一些耗cpu的操作,用多进程好
对io操作,用多线程好
进程操作代价大于线程间切换

耗cpu的操作:数学运算、机器学习、挖矿

from concurrent.futures import ProcessPoolExecutor
def fib(n):
    if n<=2:
        return 1
    return fib(n-1)+fib(n-2)

'''在windows下要写入口'''
if __name__ == "__main__":
    with ProcessPoolExecutor(3) as executor:
        all_task = [executor.submit(fib, (num)) for num in range(25,40)]
        start_time = time.time()
        for future in as_completed(all_task):
            data = future.result()
            print("exe result: {}".format(data))

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

进程会把代码全部拷贝一遍,再从开始进程的地方执行
在这里插入图片描述
更加底层的方式:

import multiprocessing

#多进程编程
import time
def get_html(n):
    time.sleep(n)
    print("sub_progress success")
    return n


if __name__ == "__main__":
    progress = multiprocessing.Process(target=get_html, args=(2,))
    print(progress.pid)
    progress.start()
    print(progress.pid)
    progress.join()
    print("main progress end")

使用池:

if __name__ == "__main__":
    #使用进程池
    pool = multiprocessing.Pool(multiprocessing.cpu_count())
    result = pool.apply_async(get_html, args=(3,))
    
    #等待所有任务完成
    pool.close()
    pool.join()
    
    print(result.get())

imap方法:
加_unordered无序输出,不加按列表序输出

if __name__ == "__main__":
    for result in pool.imap_unordered(get_html, [1,5,3]):
        print("{} sleep success".format(result))

9.进程间通信

不能用普通的Queue,要用multiprocessing的Queue
共享全局变量不能适用于多进程
multiprocessing 中的 queue 不能用于 pool进程池
要用 multiprocessing 中的 manager 中的 queue

import time
from multiprocessing import Process, Queue, Pool, Manager, Pipe

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

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

if __name__ == "__main__":
    queue = Manager().Queue(10)
    pool = Pool(2)

    pool.apply_async(producer, args=(queue,))
    pool.apply_async(consumer, args=(queue,))

    pool.close()
    pool.join()

通过pipe管道进行通信
pipe只能适用于两个进程
pipe的性能高于queue

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

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()

使用内存共享,多个子进程修改主进程间的变量

def add_data(p_dict, key, value):
    p_dict[key] = value

if __name__ == "__main__":
    progress_dict = Manager().dict()
    from queue import PriorityQueue

    first_progress = Process(target=add_data, args=(progress_dict, "bobby1", 22))
    second_progress = Process(target=add_data, args=(progress_dict, "bobby2", 23))

    first_progress.start()
    second_progress.start()
    first_progress.join()
    second_progress.join()

    print(progress_dict)
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值