• 进程、线程、协程和GIL(二)


        上一篇博客讲了进程、线程、协程和GIL的基本概念,这篇我们来说说在以下三点:

      1> python中使用threading库来创建线程的两种方式

      2> 使用Event对消来判断线程是否已启动

      3> 使用Semaphore和BoundedSemaphore两个类分别来控制线程的并发数以及二者之间的区别。

      如果想要了解基本概念,请移步我的上一篇博客:https://www.cnblogs.com/ss-py/p/10236125.html

    正文:

      利用threading库来在创建一个线程:

    from threading import Thread
    
    def run(name):
        print('我是: %s' % (name))
        
    if __name__ == '__main__':
        t = Thread(target=run, args=('线程1号',))
        t.start()

      运行结果:  

      

      首先,创建一个Thread线程,并用target=run来指定这个线程需要执行那个run方法,然后将run方法所需的参数以args参数的形式传递过去,

      请注意,args所传的参数是一个元组(tuple)类型,因此即使元组中只有一个参数,也要在这个参数后再加一个逗号,如 args=('线程1号',)

      

      而且,当我们创建一个线程对象后,这个线程并不会立即执行,除非调用它的star()方法,当调用star()方法后,这个线程会调用你使用target传给他的函数,

      并将args中的参数传递给这个函数。

      Python中的线程会在一个单独的系统级线程中执行(如一个POSIX线程或一个Windows线程),这些线程全部由操作系统进行管理,线程一旦启动,

      它将独立运行直至目标函数运行结束。

      我们可以调用is_alive()方法来进行判断该线程是否在运行(is_alive()方法返回True或者False):

      我们知道,进程是依赖于线程来执行的,所以当我们的py文件在执行时,我们可以理解为有一个主线程在执行,当我们创建一个子线程时,就相当于当前程序一共有两个线程在执行。当子线程被创建后,主线程和子线程就独立运行,相互并不影响。

      代码如下:

     1 import time
     2 from threading import Thread
     3 
     4 def run(name):
     5     time.sleep(2)
     6     print('我是: %s' % (name))
     7 
     8 if __name__ == '__main__':
     9     t = Thread(target=run, args=('子线程',))
    10     t.start()
    11     print('我是主线程')

      执行结果:

      

      在代码的第9行,创建了一个线程t,让它来执行run()方法,这时,程序中就有了两个线程同时存在、各自独立运行,默认的,主线程是不会等待子线程的运算结果的,所以主线程继续向下执行,打印L“我是主线程”,而子线程在调用run()方法时sleep了两秒钟,之后才打印出“我是子线程”

      当然,我们可以手动的调用join()方法来让主线程等待子线程运行结束后再向下执行:

     1 import time
     2 from threading import Thread
     3 
     4 def run(name):
     5     time.sleep(2)
     6     print('我是: %s' % (name))
     7 
     8 if __name__ == '__main__':
     9     t = Thread(target=run, args=('子线程',))
    10     t.start()
    11     t.join()
    12     print('我是主线程')

      

      这时,程序在进行到第十行后,主线程就卡住了,它在等待子线程运行结束,档子线程运行结束后,主线程才会继续向下运行,直至程序退出。

      但是,但是,无论主线程等不等待子线程,Python解释器都会等待所有的线程都终止后才会退出。也就是说,无论主线程等不等待子线程,这个程序最终都

      会运行两秒多,因为子线程sleep了两秒。

      所以,当遇到需要长时间运行的线程或者是需要一直在后台运行的线程时,可以将其设置为后台线程(守护线程)daemon=True,如:

    t = Thread(target=run, args=('子线程',), daemon=True)

      守护线程,顾名思义是守护主线程的线程,他们是依赖于主线程而存在的,当主线程执行结束后,守护线程会被立即注销,无论该线程是否执行结束。

      当然,我们也可以利用join()来使主线程等待主线程。

      使用threading库来创建线程还有一种方式:

    from threading import Thread
    
    class CreateThread(Thread):
    
        def __init__(self):
            super().__init__()
    
        def run(self):
            print('我是子线程!')
    
    t = CreateThread()
    t.start()

      在开始t = Thread(target=run, args=('子线程',))这种方式调用方法时子线程调用的run方法的这个方法名是我随便起的,实际上叫什么都行,

      但是以继承Thread类方式实现线程时,线程调用的方法名必须是run() 这个是程序写死的。

       使用Event对象判断线程是否已经启动

       threading库中的Event对象包含一个可由线程来设置的信号标志,它允许线程等待某些事件的发生。

      初始状态时,event对象中的信号标志被设置为假,如果有一个线程等待event对象,且这个event对象的标志为假,那么这个线程就会一直阻塞,直到该标志为真。如果将一个event对象的标志设置为真,他将唤醒所有等待这个标志的线程,如果一个线程等待一个被设置为真得Event对象,那么它将忽略这个事件,继续向下执行。  

      Event (事件) 定义了一个全局的标志Flag,如果Flag为False,当程序执行event.wait()时就会阻塞,当Flag为True时,程序执行event.wait()时便不会阻塞:

      event.set():  将标志Flag设置为True, 并通知所有因等待该标志而处于阻塞状态的线程恢复运行。

      event.clear(): 将标志Flag设置为False

      event.wait():  判断当前标志状态,如果是True则立即返回,否则线程继续阻塞。

      event.isSet(): 获取标志Flag状态: 返回True或者False

     1 from threading import Thread, Event
     2 
     3 def run(num, start_evt):
     4     if int(num) >10:
     5         start_evt.set()
     6     else:
     7         start_evt.clear()
     8 start_evt = Event()
     9 
    10 if __name__ == '__main__':
    11     num = input("请输入数字>>>")
    12     t = Thread(target=run, args=(num, start_evt,))
    13     t.start()
    14     start_evt.wait()  # 主线程获取Event对象的标志状态,若为True,则主线程继续执行,否则,主线程阻塞
    15     print("主线程继续执行!")

      上边这段代码:当输入的数字大于10时,将标志设置为True,主程序继续执行,当小于或者等于10时,将标志设为False(默认为False),主线程阻塞。

         

      值得注意的是:当Event对象的标志被设置为True时,他会唤醒所有等待他的线程,如果只想唤醒某一个线程,最好使用信号量。

      信号量

      信号量,说白了就是一个计数器,用来控制线程的并发数,每次有线程获得信号量的时候(即acquire())计数器-1,释放信号量时候(release())计数器+1,计数器为0的时候其它线程就被阻塞无法获得信号量

      acquire()   # 设置一个信号量

      release()   # 释放一个信号量

      python中有两个类实现了信号量:(Semaphore和BoundedSemaphore)

      Semaphore和BoundedSemaphore的相同之处

        通过: threading.Semaphore(3) 或者 threading.BoundedSemaphore(3) 来设置初始值为3的计数器

        执行acquire() 计数器-1,执行release() 计数器+1,当计数器为0时,其他线程均无法再获得信号量从而阻塞

    import threading
    
    se = threading.BoundedSemaphore(3)
    
    for i in range(5):
        se.acquire()
        print('信号量被设置')
    
    for j in range(10):
        se.release()
        print('信号量被释放了')

      执行结果:   

    import threading
    
    se = threading.Semaphore(3)
    
    for i in range(5):
        se.acquire()
        print('信号量被设置')
    
    for j in range(10):
        se.release()
        print('信号量被释放了')

      执行结果:

      可以看到,无论我们用那个类创建信号量,当计数器被减为0时,其他线程均会阻塞。

      这个功能经常被用来控制线程的并发数

      没有设置信号量:

    import time
    import threading
    
    num = 3
    
    def run():
        time.sleep(2)
        print(time.time())
    
    if __name__ == '__main__':
        t_list = []
        for i in range(20):
            t = threading.Thread(target=run)
            t_list.append(t)
        for i in t_list:
            i.start()

      执行结果:20个线程几乎在同时执行,,如果主机在执行IO密集型任务时执行这种程序时,主机有可能会宕机,

      但是在设置了信号量时,我们可以来控制同一时间同时运行的线程数:

    import time
    import threading
    
    num = 3
    
    def run():
        se.acquire()  # 添加信号量
        time.sleep(2)
        print(time.time())
        se.release()  #  释放一个信号量
    
    
    if __name__ == '__main__':
        t_list = []
        se = threading.Semaphore(5)  # 设置一个大小为5的计数器(同一时间,最多允许5个线程在运行)
       # se = threading.BoundedSemaphore(5) 
        for i in range(20):
            t = threading.Thread(target=run)
            t_list.append(t)
        for i in t_list:
            i.start()

      这时,我们给程序加上信号量,控制它在同一时间内,最多只有5个线程在运行。

      

      两者之间的差异性:

        当计数器达到设定好的上线时,BoundedSemaphore就无法进行release()操作了,Semaphore没有这个限制,它会抛出异常。

      

    import threading
    
    
    se = threading.Semaphore(3)
    
    for i in range(3):  # 将计数器值减为0
        se.acquire(3)
    
    for j in range(5):  # 将计数器值加至5
        se.release()
        print('信号量被释放了')

      运行结果:

      

    import threading
    
    
    se = threading.BoundedSemaphore(3)
    
    for i in range(3):  # 将计数器值减为0
        se.acquire(3)
    
    for j in range(5):  # 将计数器值加至5
        se.release()
        print('信号量被释放了')

      运行结果:

      抛异常了:信号量被释放太多次。。。

       好了,这篇文章的就先写到这里,下一篇文章我会讲解关于线程间通信、线程加锁等问题

    想了解更多Python关于爬虫、数据分析的内容,欢迎大家关注我的微信公众号:悟道Python

      

      

       

     

  • 相关阅读:
    redis连接客户端
    map中使用的细节问题(不同key循坏中被后面的值替换)
    使用异步开启新的线程Spring异步方法注解@Async
    npm init 出现一堆提问(npm init -y)
    小程序的时间日期选择器
    小程序--分类项目跳转页面(同样也适用轮播图跳转)
    小程序样式不管用,解决方法button:not([size='mini']) { width: 184px; margin-left: auto; margin-right: auto; }
    vue-elementui的时间日期选择器( value-format="yyyy-MM-dd HH:mm:ss"),以及时间解析{y}-{m}-{d} {h}:{i}:{s}
    vue.config.js配置详细说明(逐条解释)
    element在el-table-column中如何使用过滤器
  • 原文地址:https://www.cnblogs.com/ss-py/p/10238067.html
Copyright © 2020-2023  润新知