zoukankan      html  css  js  c++  java
  • 27.并发编程之多线程:线程相关概念、开启线程的两种方式、线程对象方法、守护线程、互斥锁

    • 线程相关概念

    • 什么是线程

    • 线程的创建开销小

    • 进程与线程的区别

    • 为和要用多线程

    • 开启线程的两种方式

    • 线程对象的方法

    • 守护线程

    • 互斥锁


    • 线程相关概念

    • 什么是线程?

    • 线程:一个流水线的运行过程

      ​ 进程内代码的运行过程

      进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),
      线程线程是一个执行单位,cpu执行的就是线程

      ​ 多线程(即多个控制线程)的概念是,在一个进程中存在多个控制线程,多个控

      制线程共享该进程的地址空间,相当于一个车间内有多条流水线,都共用一个车间的资源。

    • 线程的创建开销小

    • 创建进程的开销远大于线程

      创建子进程要开一个内存空间,把父进程数据拷一份给子进程,该空间里至少要有一条流水线,这样开销很大

      创建子线程资源不用申请,就用当前进程就可以,只是提交的时候有一段代码要并发运行,创建线程的开销要远远小于进程,几乎是这个请求发出的同时,这个线程立马就创建好了

    • 进程之间是竞争关系,线程之间是协作关系?

    • 进程与线程的区别

      1.线程共享创建它的进程的地址空间;进程有自己的地址空间

      2.线程可以直接访问其进程的数据段;进程有自己的父进程的数据段副本

      3.线程可以直接与其进程中的其他线程通信;进程必须使用进程间通信来与同级进程通信

      4.新线程很容易创建;新的进程需要父进程的复制。

      5.线程可以对同一进程的线程进行相当大的控制;进程只能对子进程进行控制。

      6.主线程的改变(取消,优先级的改变等)可能会影响进程中其他线程的行为;父进程的更改不会影响子进程

      • 总结:

        1、同一进程下的多个线程共享该进程的内存资源

        2、开启子线程的开销要远远小于开启子进程

    • 为何要用多线程

    • 多线程指的是,在一个进程中开启多个线程,简单的讲:如果多个任务共用一块地址空间,那么必须在一个进程内开启多个线程。详细的讲分为4点:

      1. 多线程共享一个进程的地址空间
      2. 线程比进程更轻量级,线程比进程更容易创建可撤销,在许多操作系统中,创建一个线程比创建一个进程要快10-100倍,在有大量线程需要动态和快速修改时,这一特性很有用
      3. 若多个线程都是cpu密集型的,那么并不能获得性能上的增强,但是如果存在大量的计算和大量的I/O处理,拥有多个线程允许些活动彼此重叠运行,从而会加快程序执行的速度。
      4. 在多cpu系统中,为了最大限度的利用多核,可以开启多个线程,比开进程开销要小的多。(这一条并不适用于python)
    • 开启线程的两种方式

    • threading模块介绍

      开启线程的threading模块提供了一个比thread模块更高层的API来提供线程的并发性。这些线程并发运行并共享内存。

      Thread类的使用 :目标函数可以实例化一个Thread对象,每个Thread对象代表着一个线程,可以通过start()方法,开始运行。

    • 开启线程的方式一:每创建一个进程,默认有一个线程,就是主线程。进程要想执行,要先创建一个主线程,然后由这个进程内的线程去运行代码

    # 1.证明开启子线程的开销小
    from threading import Thread,current_thread
    import os
    
    def task():
        # current_thread()会得到当前线程的线程对象.name能获取它的名字
        print("%s is running" %current_thread().name)
    
    if __name__ == '__main__':  # Windows规定开启线程的代码必须放在它下面
        t = Thread(target=task)
        t.start()  # 通知操作系统开启子线程 
        print('主线程',current_thread().name) #.name默认名,可以更改.name=名字
    
    
    # 2.证明同一进程下的多个线程共享该进程的内存资源
    from threading import Thread, current_thread
    
    n = 100
    
    def task():
        global n   # 声明n是全局变量
        n = 0
    
    if __name__ == '__main__':
        t = Thread(target=task)
        t.start()  # 因为线程的运行太快了,有可能看到100
        t.join()  # 加join等线程运行完,打印永远看不到100的值
        print("主线程",n)
    
    • 开启线程的方式二:自己写一个子类去继承它
    from threading import Thread
    
    class Mythread(Thread): # 自定义的类必须继承Thread类
        # 重写了init方法 父类就被覆盖掉
        def __init__(self, name):
            super().__init__() # 重用父类 因为父类还有很多有用功能,继承父类
            self.name = name
    
        def run(self) -> None:  # 方法一定要写run
            print("%s is running" % self.name)
    
    if __name__ == '__main__':
        # 实例化直接用自己自定义的类开子线程
        t = Mythread("线程1")  # args 为函数传参数 没有就不传
        t.start()  # 通知系统开启子线程
    
    • 线程对象相关的方法

      # Thread实例对象的方法
      isAlive(): 返回线程是否活动的。
      getName(): 返回线程名。
      setName(): 设置线程名。
      
      # threading模块提供的一些方法:
      threading.currentThread(): 返回当前的线程变量。
          
      threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、
          			       结束前,不包括启动前和终止后的线程。
          
      threading.activeCount(): 返回正在运行的线程数量,len(threading.enumerate())
          					 有相同的结果。
              
      group:线程组,目前还没有 实现
      
      target: 要执行的方法,就是要执行的程序
               是将被run()方法调用的可调用对象。默认为None,表示不调用任何东西
          
      name:线程的名字,默认情况下以Thread-N的形式构造一个唯一的名字,N是一个小的十进制整数
      
      args:给调用程序传入的值()
      
      kwargs:给程序传入的值,默认为{}
      
    from threading import Thread,current_thread,active_count,enumerate
    import os
    import time
    
    def task():
        print("%s is running" %current_thread().name)
        time.sleep(3)
    
    if __name__ == '__main__':
        t = Thread(target=task)
        t.start()
        # print(active_count()) # 查看活跃的线程数
        print(enumerate()) # 显示列表里面放的是每个线程的对象,一个主线程的,一个线程1的
        print("主")        # 可以取出线程对象,去调对象下面的方法
        # print(t.is_alive()) # 判断是否存活
        # print('主线程',current_thread().name)
    
    
    • 守护线程

      守护进程是守护主进程的代码

      守护线程是守护主线程的生命周期

      无论是进程还是线程,都遵循:守护xxx会等待主xxx运行完毕后被销毁

      需要强调的是:运行完毕并非终止运行

    1.对主进程来说,运行完毕指的是主进程代码运行完毕
    
    2.对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,
      主线程才算运行完毕
    

    详细解释:

    1. 主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直
       等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束,
    
    2. 主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束
       意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束。
    
    from threading import Thread,current_thread,active_count,enumerate
    import os
    import time
    
    def task(n):
        print("%s is running" %current_thread().name)
        time.sleep(n)
        print("%s is end" %current_thread().name)
    
    
    if __name__ == '__main__':
        t1 = Thread(target=task,args=(3,))
        t2 = Thread(target=task,args=(5,))
        t3 = Thread(target=task,args=(100,))
        t3.daemon = True  # 守护线程,守护主线程的生命周期
    
        t1.start()
        t2.start()
        t3.start()
        print("主") # 主线程5秒钟结束
        
    """
    # 打印结果
    Thread-1 is running
    Thread-2 is running
    Thread-3 is running
    主
    Thread-1 is end
    Thread-2 is end
    
    过程分析:
    t1、t2、t3三个线程在背后都开始运行了,t1、t2都是正常的子线程,
    t3则是守护线程,守护着主线程的生命周期 t3按正常来说是100秒结束,
    但主线程在5秒后就结束了,一旦结束就直接带走t3了,主线程应该很快就
    运行完了,但是要在原地等t1、t2运行完主进程才能结束,也就5秒钟,可
    t3还没有运行完还是被一起带走了,所以无法看到Thread-3 is end
    
    """
    
    • 互斥锁

      在多任务操作系统中,同时运行的多个任务可能都需要使用同一种资源。这个过程有点类似于,公司部门里,我在使用着打印机打印东西的同时(还没有打印完),别人刚好也在此刻使用打印机打印东西,如果不做任何处理的话,打印出来的东西肯定是错乱的。

      互斥锁是一种简单的加锁的方法来控制对共享资源的访问,对共享数据进行锁定,保证同一时刻只能有一个线程去操作。

      【互斥锁的特点】:

      1. 原子性:把一个互斥量锁定为一个原子操作,这意味着操作系统保证了如果一个线程锁定了一个互斥量,没有其他线程在同一时间可以成功锁定这个互斥量;
      2. 唯一性:如果一个线程锁定了一个互斥量,在它解除锁定之前,没有其他线程可以锁定这个互斥量;
      3. 非繁忙等待:如果一个线程已经锁定了一个互斥量,第二个线程又试图去锁定这个互斥量,则第二个线程将被挂起(不占用任何cpu资源),直到第一个线程解除对这个互斥量的锁定为止,第二个线程则被唤醒并继续执行,同时锁定这个互斥量。

      【互斥锁的操作流程如下】:

      1. 在访问共享资源后临界区域前,对互斥锁进行加锁;
      2. 在访问完成后释放互斥锁导上的锁。在访问完成后释放互斥锁导上的锁;
      3. 对互斥锁进行加锁后,任何其他试图再次对互斥锁加锁的线程将会被阻塞,直到锁被释放。对互斥锁进行加锁后,任何其他试图再次对互斥锁加锁的线程将会被阻塞,直到锁被释放。
    from threading import Thread
    import time
    
    n = 100
    def task():
        global n
        temp = n
        time.sleep(0.1)  # 设置延迟
        n = temp - 1
    
    
    if __name__ == "__main__":
        n = 100
        thread_l = []
        for i in range(100):
            t=Thread(target=task)
            l.append(p)
            t.start()
        for obj in thread_l:
            obj.join()
    
        print("主",n,time.time()-start_time)
        
    '''
    代码运行完成结果大概率为99,小概率为98,运行时间则是略大于0.1秒。并发执行,速度快,数据不安全:
    '''    
        
    
    需要注意:
    join是等待所有,即整体串行,而锁只是锁住修改共享数据的部分,即部分串行,要想保证数据安全的根本原理在于让并发变成串行,join与互斥锁都可以实现,毫无疑问,互斥锁的部分串行效率要更高
    
    from threading import Thread,Lock
    import os
    import time
    
    n = 100
    mutex = Lock()
    
    def task():
        global n
        with mutex:
            temp = n
            time.sleep(0.1)
            n = temp - 1
    
    if __name__ == '__main__':
        thread_l = []
        start_time = time.time()
        for i in range(100):
            t = Thread(target=task)
            thread_l.append(t)
            t.start()
    
        for obj in thread_l:
            obj.join()
    
        print("主",n,time.time()-start_time)
        
    '''
    代码运行结果则一定是0,但是这样也有一个缺点就是运行速度降低
    了运行时间在10秒多一点,但是为了数据安全这些时间是必须的!
    
    '''
        
        
    

    提示:加上互斥锁,哪个线程抢到这个锁我们决定不了,哪个先抢到锁的那么就是哪个线程先执行,没有抢到的线程需要等待

    加上互斥锁多任务瞬间变成单任务,性能会下降,也就是说同一时刻只能有一个线程去执行

  • 相关阅读:
    C#使用结构体,输入5个人的学号,姓名,分数,按照成绩高低排列打印出来
    数据库考试试题
    数据库存储系统应用,超市小票系统
    数据库变量与语句
    练习:C#---类(身份证号截取生日、验证邮箱、DateTime)
    Chapter 3. 类---String类、Math类、Datetime类
    练习:C#---for循环(整数和、阶乘、楼梯)
    Chapter 2 C#语句---异常语句
    Chapter 2. C#语句---循环语句
    Chapter 2 C#语句---选择语句
  • 原文地址:https://www.cnblogs.com/gfeng/p/14310874.html
Copyright © 2011-2022 走看看