• 并发编程(线程)——验证GIL锁,GIL与普通互斥锁的区别,io密集型和计算密集型,死锁现象(解决方式:递归锁),Semaphore信号量,Event事件,线程queue,多进程实现tcp服务端并发,线程池&进程池


    一、验证GIL锁的存在方式

    GIL锁:全局解释器锁

    GIL锁作用:因为垃圾回收线程不是线程安全的,所有线程必须拿到这把锁,才能执行

    from threading import Thread
    from multiprocessing import Process
    
    
    def task():
        while True:
            pass
    
    if __name__ == '__main__':
        for i in range(6):
            # t=Thread(target=task)  # 因为有GIL锁,同一时刻,只有一条线程执行,所以cpu不会满
            t=Process(target=task)   # 由于是多进程,进程中的线程会被cpu调度执行,6个cpu全在工作,就会跑满
            t.start()

    二、GIL与普通互斥锁的区别

    区别:GIL锁是不能保证数据的安全,普通互斥锁来保证数据安全
    from threading import Thread, Lock
    import time
    
    mutex = Lock()
    money = 100
    
    
    def task():
        global money
        mutex.acquire()
        temp = money
        time.sleep(1)
        money = temp - 1
        mutex.release()
    
    
    if __name__ == '__main__':
        ll=[]
        for i in range(10):
            t = Thread(target=task)
            t.start()
            # t.join()  # 会怎么样?变成了串行,不能这么做
            ll.append(t)
        for t in ll:
            t.join()
        print(money)

    三、io密集型和计算密集型

    -----以下只针对于cpython解释器
    -在单核情况下:
    -开多线程还是开多进程?不管干什么都是开线程
    -在多核情况下:
    -如果是计算密集型,需要开进程,能被多个cpu调度执行
    -如果是io密集型,需要开线程,cpu遇到io会切换到其他线程执行
    from threading import Thread
    from multiprocessing import Process
    import time
    
    
    # 计算密集型
    def task():
        count = 0
        for i in range(100000000):
            count += i
    
    
    if __name__ == '__main__':
        ctime = time.time()
        ll = []
        for i in range(10):
            t = Thread(target=task)  # 开线程:42.68658709526062
            # t = Process(target=task)   # 开进程:9.04949426651001
            t.start()
            ll.append(t)
    
        for t in ll:
            t.join()
        print(time.time()-ctime)
    
    
    ## io密集型
    def task():
        time.sleep(2)
    
    
    if __name__ == '__main__':
        ctime = time.time()
        ll = []
        for i in range(400):
            t = Thread(target=task)  # 开线程:2.0559656620025635
            # t = Process(target=task)   # 开进程:9.506720781326294
            t.start()
            ll.append(t)
    
        for t in ll:
            t.join()
        print(time.time()-ctime)

    四、死锁现象(哲学家就餐问题)

    是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程,如下就是死锁
    死锁现象:
    (1)A线程拿到了A锁,等B锁,B线程拿到了B锁,等A锁,相互等待,永远等下去;
    (2)A线程拿到了A锁,再去拿A锁
    from threading import Thread, Lock
    import time
    
    mutexA = Lock()
    mutexB = Lock()
    
    
    def eat_apple(name):
        mutexA.acquire()
        print('%s 获取到了a锁' % name)
        mutexB.acquire()
        print('%s 获取到了b锁' % name)
        print('开始吃苹果,并且吃完了')
        mutexB.release()
        print('%s 释放了b锁' % name)
        mutexA.release()
        print('%s 释放了a锁' % name)
    
    
    def eat_egg(name):
        mutexB.acquire()
        print('%s 获取到了b锁' % name)
        time.sleep(2)
        mutexA.acquire()
        print('%s 获取到了a锁' % name)
        print('开始吃鸡蛋,并且吃完了')
        mutexA.release()
        print('%s 释放了a锁' % name)
        mutexB.release()
        print('%s 释放了b锁' % name)
    
    
    if __name__ == '__main__':
        ll = ['egon', 'alex', '铁蛋']
        for name in ll:
            t1 = Thread(target=eat_apple, args=(name,))
            t2 = Thread(target=eat_egg, args=(name,))
            t1.start()
            t2.start()

    解决方式:

    递归锁

    递归锁(可重入锁),当前线程可以多次acquire锁,每acquire一次,内部计数器加1,每relaese一次,内部计数器减一
    只有计数器不为0,其他人都不获得这把锁
    from threading import Thread, Lock,RLock
    import time
    
    # 同一把锁
    # mutexA = Lock()
    # mutexB = mutexA
    
    # 使用可重入锁解决(同一把锁)
    # mutexA = RLock()
    # mutexB = mutexA
    mutexA = mutexB =RLock()
    
    def eat_apple(name):
        mutexA.acquire()
        print('%s 获取到了a锁' % name)
        mutexB.acquire()
        print('%s 获取到了b锁' % name)
        print('开始吃苹果,并且吃完了')
        mutexB.release()
        print('%s 释放了b锁' % name)
        mutexA.release()
        print('%s 释放了a锁' % name)
    
    
    def eat_egg(name):
        mutexB.acquire()
        print('%s 获取到了b锁' % name)
        time.sleep(2)
        mutexA.acquire()
        print('%s 获取到了a锁' % name)
        print('开始吃鸡蛋,并且吃完了')
        mutexA.release()
        print('%s 释放了a锁' % name)
        mutexB.release()
        print('%s 释放了b锁' % name)
    
    
    if __name__ == '__main__':
        ll = ['egon', 'alex', '铁蛋']
        for name in ll:
            t1 = Thread(target=eat_apple, args=(name,))
            t2 = Thread(target=eat_egg, args=(name,))
            t1.start()
            t2.start()

    五、Semaphore信号量

    Semaphore:信号量可以理解为多把锁,允许多条线程同时修改数据
    from  threading import Thread,Semaphore
    import time
    import random
    sm=Semaphore(3) # 数字表示可以同时有多少个线程操作
    
    
    def task(name):
        sm.acquire()
        print('%s 正在蹲坑'%name)
        time.sleep(random.randint(1,5))
        sm.release()
    
    
    
    if __name__ == '__main__':
        for i in range(20):
            t=Thread(target=task,args=('屌丝男%s号'%i,))
            t.start()

    六、Event事件

    一些线程需要等到其他线程执行完成之后才能执行,类似于发射信号
    比如一个线程等待另一个线程执行结束再继续执行
    event.set() 发信号
    event.wait()阻塞等待信号,只要收到set信号,就会继续往下执行

    from threading import Thread, Event
    import time
    
    event = Event()
    
    
    def girl(name):
        print('%s 现在不单身,正在谈恋爱'%name)
        time.sleep(10)
        print('%s 分手了,给屌丝男发了信号'%name)
        event.set()#发信号
    
    
    def boy(name):
        print('%s 在等着女孩分手'%name)
        event.wait()  # 阻塞等信号,只要收到set信号,就会继续往下执行。只要没来信号,就卡在者
        print('女孩分手了,机会来了,冲啊')
    
    
    if __name__ == '__main__':
        lyf = Thread(target=girl, args=('刘亦菲',))
        lyf.start()
    
        for i in range(10):
            b = Thread(target=boy, args=('屌丝男%s号' % i,))
            b.start()

    案例:

    起两个线程,第一个线程读文件的前半部分,读完发一个信号,另一个进程读后半部分,并打印

    from threading import Thread, Event
    import time
    import os
    
    event = Event()
    # 获取文件总大小
    size = os.path.getsize('a.txt')
    
    
    def read_first():
        with open('a.txt', 'r', encoding='utf-8') as f:
            n = size // 2  # 取文件一半,整除
            data = f.read(n)
            print(data)
            print('我一半读完了,发了个信号')
            event.set()
    
    
    def read_last():
        event.wait()  # 等着发信号
        with open('a.txt', 'r', encoding='utf-8') as f:
            n = size // 2  # 取文件一半,整除
            # 光标从文件开头开始,移动了n个字节,移动到文件一半
            f.seek(n, 0)
            data = f.read()
            print(data)
    
    
    if __name__ == '__main__':
        t1=Thread(target=read_first)
        t1.start()
        t2=Thread(target=read_last)
        t2.start()

    七、线程queue

    1、进程queue和线程不是一个
      进程queue:
      from multiprocessing import Queue

      线程queue:
      from queue import Queue,LifoQueue,PriorityQueue

    2、不同线程数据交互(线程间通信)的两种方式:
    (1)共享变量:因为会出现数据不安全问题,所以不同线程修改同一份数据要加锁(互斥锁)
    (2)用线程queue通信:不需要考虑数据安全问题(queue是线程安全的)不需要加锁,内部自带
    3、三种线程Queue
    -Queue:队列,先进先出
    -PriorityQueue:优先级队列,谁小谁先出
    -LifoQueue:栈,后进后出
    #1 Quenue使用
    
    q=Queue(5)
    #放值
    q.put("lqz")
    q.put("egon")
    q.put("铁蛋")
    q.put("钢弹")
    q.put("金蛋")
    
    # q.put("银蛋")
    # q.put_nowait("银蛋")
    # 取值
    print(q.get())
    print(q.get())
    print(q.get())
    print(q.get())
    print(q.get())
    # 卡住
    # print(q.get())
    # q.get_nowait()
    # 是否满,是否空
    print(q.full())
    print(q.empty())
    
    #2 LifoQueue使用
    
    q=LifoQueue(5)
    q.put("lqz")
    q.put("egon")
    q.put("铁蛋")
    q.put("钢弹")
    q.put("金蛋")
    
    # q.put("ddd蛋")
    print(q.get())
    
    #3 PriorityQueue使用
    #PriorityQueue:数字越小,级别越高
    
    q=PriorityQueue(3)
    q.put((-10,'金蛋'))
    q.put((100,'银蛋'))
    q.put((101,'铁蛋'))
    # q.put((1010,'铁dd蛋'))  # 不能再放了
    
    print(q.get())
    print(q.get())
    print(q.get())

    八、通过多进程,实现TCP服务端支持多个客户端连接(并发)

    #服务端
    from multiprocessing import Process
    
    import socket
    
    
    
    
    def task(conn):
        while True:
            try:
                data = conn.recv(1024)
                if len(data) == 0: break
                print(data)
                conn.send(data.upper())
            except Exception as e:
                print(e)
                break
        conn.close()
    
    
    
    if __name__ == '__main__':
        server = socket.socket()
    
        server.bind(('127.0.0.1', 8081))
        server.listen(5)
    
        # 多线程,或者多进程
        while True:  # 连接循环
            conn, addr = server.accept()
            # 多用户的服务端
            t=Process(target=task,args=(conn,))
            t.start()
    
            ### 单用户的服务端
            # while True:
            #     try:
            #         data = conn.recv(1024)
            #         if len(data) == 0: break
            #         print(data)
            #         conn.send(data.upper())
            #     except Exception as e:
            #         print(e)
            #         break
            # conn.close()
    #客户端
    import socket
    import time
    
    
    cli=socket.socket()
    cli.connect(('127.0.0.1',8081))
    
    while True:
        cli.send(b'hello world')
        time.sleep(0.1)
        data=cli.recv(1024)
        print(data)

     

    九、线程池&进程池

    池:池子,用来做缓冲
    1、为什么会出现池?不管是开进程还是开线程,不能无限制开,通过池,假设池子里就有10个,不管再怎么开,永远是这10个

    2、使用(需要记住)
    from concurrent.futures import ThreadPoolExecutor
    pool = ThreadPoolExecutor(2)
    pool.submit(函数名,参数1,参数2...).add_done_callback(call_back回调函数)
    #函数执行完的数据如何给回调函数?
      回调函数会接收一个f对象,对象中有要的数据(函数的return结果),f.result()
    from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
    from threading import Thread
    import time
    import random
    
    pool = ThreadPoolExecutor(5)  # 数字是池的大小
    # pool = ProcessPoolExecutor(5)  # 数字是池的大小
    
    
    def task(name):
        print('%s任务开始' % name)
    
        time.sleep(random.randint(1, 4))
        print('任务结束')
        return '%s 返回了'%name
    
    
    
    def call_back(f):
        # print(type(f))
        print(f.result())
    if __name__ == '__main__':
    
        # ll=[]
        # for i in range(10):  # 起了100个线程
        #     # t=Thread(target=task)
        #     # t.start()
        #     res = pool.submit(task, '屌丝男%s号' % i)  # 不需要再写在args中了
        #     # res是Future对象
        #     # from  concurrent.futures._base import Future
        #     # print(type(res))
        #     # print(res.result())  # 像join,只要执行result,就会等着结果回来,就变成串行了
        #     ll.append(res)
        #
        # for res in ll:
        #     print(res.result())
    
        # 终极使用
        for i in range(10):  # 起了100个线程
            # 向线程池中提交一个任务,等任务执行完成,自动回到到call_back函数执行
            pool.submit(task,'屌丝男%s号' % i).add_done_callback(call_back)

    线程池和进程池的shutdown(二者用法一样)

    #主线程等待所有任务执行完成

    from concurrent.futures import ThreadPoolExecutor
    import time
    
    pool = ThreadPoolExecutor(3)
    
    
    def task(name):
        print('%s 开始'%name)
        time.sleep(1)
        print('%s 结束'%name)
    
    
    if __name__ == '__main__':
        for i in range(20):
            pool.submit(task, '屌丝%s' % i)
    
        # 放到for外面,等待所有任务执行完成,主线程再继续走
        pool.shutdown(wait=True)  # 等待所有任务完成,并且把池关闭
        #  问题,关了还能提交任务吗?不能再提交了
        # pool.submit(task,'sdddd')#如果关闭后提交数据会报错
        print('') # 立马执行,20个线程都执行完了,再执行

     线程池案例:

      爬网站

    from concurrent.futures import ThreadPoolExecutor
    
    import requests  # 爬虫会学到的模块
    
    pool = ThreadPoolExecutor(2)
    
    
    def get_pages(url):
        # https://www.baidu.com
        res = requests.get(url)  # 向这个地址发送请求
    
        name = url.rsplit('/')[-1] + '.html'
        print(name)  # www.baidu.com.html
        # res.content拿到页面的二进制
        return {'name': name, 'text': res.content}
    
    
    def call_back(f):
        dic = f.result()
        with open(dic['name'], 'wb') as f:
            f.write(dic['text'])
    
    
    if __name__ == '__main__':
        ll = ['https://www.baidu.com', 'https://www.mzitu.com', 'https://www.cnblogs.com']
        for url in ll:
            pool.submit(get_pages, url).add_done_callback(call_back)

    十、

    ---39----

  • 相关阅读:
    c++ 与 c 的区别
    c++ 查看程序运行时间
    串口阻塞与非阻塞
    串口缓冲区
    马拉车算法
    printf 自加自减
    stack
    长度问题
    PCIE的内存地址空间、I/O地址空间和配置地址空间
    数组和指针
  • 原文地址:https://www.cnblogs.com/guojieying/p/13565776.html
Copyright © 2020-2023  润新知