zoukankan      html  css  js  c++  java
  • day033线程介绍,锁,递归锁,信号量,及与进程的效率对比

     
     

    本节内容:

    1.线程的创建
    2.线程join
    3.线程的其他方法
    4.线程和进程的效率对比
    5.查看子线程与主线程是否在同一个进程
    6.线程之间是数据共享的
    7.验证多线程共享数据资源造成数据不安全
    8.加锁解决共享数据不安全的问题
    9.死锁现象
    10.递归锁,解决死锁现象
    11.守护线程
    12.信号量(也是一把锁,可以同时执行多个线程)
    13.事件

    一、背景知识

    1、进程

    之前我们已经了解了操作系统中进程的概念,程序并不能单独运行,
    只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称之为进程。
     
    程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念。
     
    在多道编程中,我们允许多个程序同时加载到内存中,在操作系统的调度下,可以实现并发地执行。
    这是这样的设计,大大提高了CPU的利用率。
    进程的出现让每个用户感觉到自己独享CPU,因此,进程就是为了在CPU上实现多道编程而提出的。

    2、有了进程,为什么还要线程

    线程介绍及vs进程
    #什么是线程:#指的是一条流水线的工作过程,关键的一句话:一个进程内最少自带一个线程,其实进程根本不能执行,进程不是执行单位,是资源的单位,分配资源的单位#线程才是执行单位#进程:做手机屏幕的工作过程,刚才讲的#我们的py文件在执行的时候,如果你站在资源单位的角度来看,我们称为一个主进程,如果站在代码执行的角度来看,它叫做主线程,
    只是一种形象的说法,其实整个代码的执行过程成为线程,也就是干这个活儿的本身称为线程,
    但是我们后面学习的时候,我们就称为线程去执行某个任务,其实那某个任务的执行过程称为一个线程,一条流水线的执行过程为线程
     
    #进程vs线程#1 同一个进程内的多个线程是共享该进程的资源的,不同进程内的线程资源肯定是隔离的#2 创建线程的开销比创建进程的开销要小的多
     
     
    #并发三个任务:1启动三个进程:因为每个进程中有一个线程,但是我一个进程中开启三个线程就够了#同一个程序中的三个任务需要执行,你是用三个进程好 ,还是三个线程好?#例子:
    # pycharm 三个任务:键盘输入 屏幕输出 自动保存到硬盘
    #如果三个任务是同步的话,你键盘输入的时候,屏幕看不到
    #咱们的pycharm是不是一边输入你边看啊,就是将串行变为了三个并发的任务
    #解决方案:三个进程或者三个线程,哪个方案可行。
    如果是三个进程,进程的资源是不是隔离的并且开销大,最致命的就是资源隔离,但是用户输入的数据还要给另外一个进程发送过去,进程之间能直接给数据吗?
    你是不是copy一份给他或者通信啊,但是数据是同一份,我们有必要搞多个进程吗,线程是不是共享资源的,
    我们是不是可以使用多线程来搞,你线程1输入的数据,线程2能不能看到,你以后的场景还是应用多线程多,
    而且起线程我们说是不是很快啊,占用资源也小,还能共享同一个进程的资源,不需要将数据来回的copy!
    PythonCopy

    3、进程的两个缺陷

    1.进程在一个时间内,只能做一件事,如果想做两件事或多件事,进程就无能为力了
    2.进程在执行的时候如果遇到阻塞,例如等待输入,整个进程就会挂起,
    即使后面有些工作不依赖于输入的数据,也将无法执行
    举个现实的例子也许你就清楚了:
    如果把我们上课的过程看成一个进程的话,那么我们要做的是耳朵听老师讲课,手上还要记笔记,脑子还要思考问题,这样才能高效的完成听课的任务。
    而如果只提供进程这个机制的话,上面这三件事将不能同时执行,同一时间只能做一件事,
    听的时候就不能记笔记,也不能用脑子思考,这是其一;如果老师在黑板上写演算过程,我们开始记笔记,
    而老师突然有一步推不下去了,阻塞住了,他在那边思考着,
    而我们呢,也不能干其他事,即使你想趁此时思考一下刚才没听懂的一个问题都不行,这是其二。
     
    现在你应该明白了进程的缺陷了,而解决的办法很简单,
    我们完全可以让听、写、思三个独立的过程,并行起来,这样很明显可以提高听课的效率。
    而实际的操作系统中,也同样引入了这种类似的机制——线程。

    4、线程的出现

    60年代,在OS中能拥有资源和独立运行的基本单位是进程,然而随着计算机技术的发展,进程出现了很多弊端,
    一是由于进程是资源拥有者,创建、撤消与切换存在较大的时空开销,因此需要引入轻型进程;
    二是由于对称多处理机(SMP)出现,可以满足多个运行单位,而多个进程并行开销过大。
    线程及相关解释
    因此在80年代,出现了能独立运行的基本单位——线程(Threads)。
     
    注意:进程是资源分配的最小单位,线程是CPU调度的最小单位.
        每一个进程中至少有一个线程。 进程里的线程共享改进程中的资源,每个线程有自己的id号,不同于端口号,
        
        在传统操作系统中,每个进程有一个地址空间,而且默认就有一个控制线程
     
        线程顾名思义,就是一条流水线工作的过程,一条流水线必须属于一个车间,一个车间的工作过程是一个进程
     
        车间负责把资源整合到一起,是一个资源单位,而一个车间内至少有一个流水线
     
        流水线的工作需要电源,电源就相当于cpu
     
        所以,进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),而线程才是cpu上的执行单位。
     
     
     多线程(即多个控制线程)的概念是,在一个进程中存在多个控制线程,多个控制线程共享该进程的地址空间,
    相当于一个车间内有多条流水线,都共用一个车间的资源。
     
     例如,北京地铁与上海地铁是不同的进程,而北京地铁里的13号线是一个线程,
    北京地铁所有的线路共享北京地铁所有的资源,比如所有的乘客可以被所有线路拉。
    PythonCopy

    二、线程与进程的关系

    1、线程与进程的区别可以归纳为以下4点:

    1)地址空间和其它资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
     
    2)通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——
    需要进程同步和互斥手段的辅助,以保证数据的一致性。(就类似进程中的锁的作用)
     
    3)调度和切换:线程上下文切换比进程上下文切换要快得多。线程的开启速度非常快,没有进程那么多的开销,开辟内存地址,回收等等,
     
    4)在多线程操作系统中(现在咱们用的系统基本都是多线程的操作系统),
    进程不是一个可执行的实体,真正去执行程序的不是进程,是线程,
    你可以理解进程就是一个线程的容器。
    PythonCopy

    三、线程的特点

    先简单了解一下线程有哪些特点,里面的堆栈啊主存区啊什么的后面会讲,大家先大概了解一下就好啦。
    在多线程的操作系统中,通常是在一个进程中包括多个线程,每个线程都是作为利用CPU的基本单位,是花费最小开销的实体。

    1、线程具有以下属性。

    1)轻型实体
    线程中的实体基本上不拥有系统资源,只是有一些必不可少的、能保证独立运行的资源。
    线程的实体包括程序、数据和TCB。线程是动态概念,它的动态特性由线程控制块TCB(Thread Control Block)描述。
    TCB包括以下信息:
    (1)线程状态。
    (2)当线程不运行时,被保存的现场资源。
    (3)一组执行堆栈。
    (4)存放每个线程的局部变量主存区。
    (5)访问同一个进程中的主存和其它资源。
    用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。
     
     
    2)独立调度和分派的基本单位。
    在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。
    由于线程很“轻”,故线程的切换非常迅速且开销小(在同一进程中的)。
     
    3)共享进程资源。
    线程在同一进程中的各个线程,都可以共享该进程所拥有的资源,
    这首先表现在:所有线程都具有相同的进程id,这意味着,线程可以访问该进程的每一个内存资源;
    此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。
    由于同一个进程内的线程共享内存和文件,所以线程之间互相通信不必调用内核。
     
    4)可并发执行。
    在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行;
    同样,不同进程中的线程也能并发执行,充分利用和发挥了处理机与外围设备并行工作的能力。
    PythonCopy

    四、threading模块,线程的模块

    multiprocess模块的完全模仿了threading模块的接口,二者在使用层面,有很大的相似性,因而不再详细介绍(官方链接)

    1、(多)线程的创建

    1.我们先简单应用一下threading模块来看看并发效果:

    多线程简单实现
    import time
    from threading import Thread
    #多线程并发,是不是看着和多进程很类似def func(n):
    time.sleep(1)
    print(n)
     
    #并发效果,1秒打印出了所有的数字for i in range(10):
    t = Thread(target=func,args=(i,))
    t.start()
    PythonCopy

    2.线程的两种创建方法

    两种创建方法
    from threading import Thread
     
    # 第一种, 调用Thread模块,
     
    def func(n):
    print("你好呀")
    print(n)
     
     
    if __name__ == '__main__':
    t = Thread(target=func, args=(2,))
    t.start()
    print("主线程结束") # 实际上是没有主线程之分的,共享进程资源
     
     
    # 第二种 继承类
     
    class MyThread(Thread):
    def __init__(self, ni):
    super().__init__() # 记得不要覆盖父类的__init__方法,
    self.ni = ni
     
     
    def run(self):
    print(self.ni)
    print("你好啊")
     
    if __name__ == '__main__':
    t = MyThread(11)
    t.start()
    PythonCopy

    2、线程的join方法,等同于进程的join方法

    在join的地方等待子线程执行完成后,再往下执行主线程的代码
    import time
    from threading import Thread
     
    def func():
    time.sleep(1) # 模拟代码执行时间,显示join的效果
    print('我是子线程')
     
    if __name__ == '__main__':
     
    t = Thread(target=func,) # 创建一个线程
    t.start() # 发出信号,告诉操作系统,可以运行这个线程了,
     
    print('开始等待子线程了')
    t.join() # 等待子线程完成后,再往下执行
    print('主线程结束')
    PythonCopy

    3、线程的其他方法

    1.其他方法的解释

    方法解释
    Thread实例对象的方法
    # isAlive(): 返回线程是否活动的。
    # getName(): 返回线程名。
    # setName(): 设置线程名。
     
    threading模块提供的一些方法:
    # threading.currentThread(): 返回当前的线程变量。
    # threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
    # threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果
     
    其他方法
    PythonCopy

    2.具体代码示例

    代码示例
    import time
    from threading import Thread
    from threading import current_thread
    import threading
     
     
    def func():
    time.sleep(3)
     
    # current_thread().ident
    print('我是子线程,名字是',current_thread().getName()) # 返回线程名
    print('我是子线程,id是',current_thread().ident) # 子线程的id
     
    if __name__ == '__main__':
     
    for i in range(10):
    t = Thread(target=func,)
    # t = Thread(target=func, name="线程名%s" % i) # 也可直接在这里输入线程名
    t.setName("线程名%s" % i) # 设置线程名
    t.start()
     
    # print(t.isAlive()) # 正常来说这里是True;有可能是False,取决于速度,也许线程还没创建,这里就判断了,
    print(threading.enumerate()) # 返回正在运行的线程的list,正在运行指线程启动后、结束前,
    print(threading.activeCount()) # 返回正在运行的线程数量(一般还需包含一个主线程),与len(threading.enumerate())有相同的结果
     
    # print(threading.current_thread())#主线程对象
    # print(threading.current_thread().getName()) #主线程名称
    # print(threading.current_thread().ident) #主线程ID
    # print(threading.get_ident()) #主线程ID
    #
    print('主线程结束')
    PythonCopy

    4、线程和进程的效率对比

    1.查看线程的pid

    首先来看看pid(进程id)
    from threading import Thread
    from multiprocessing import Process
    import os
     
    def work():
    print('hello',os.getpid())
    print("子线程的pid",os.getpid()) # 查看子进程的pid
     
    if __name__ == '__main__':
    #part1:在主进程下开启多个线程,每个线程都跟主进程的pid一样
    t1=Thread(target=work)
    t2=Thread(target=work)
    t1.start()
    t2.start()
    print('主线程/主进程pid',os.getpid())
     
    #part2:开多个进程,每个进程都有不同的pid
    p1=Process(target=work)
    p2=Process(target=work)
    p1.start()
    p2.start()
    print('主线程/主进程pid',os.getpid())
    PythonCopy
    那么哪些东西存在进程里,那些东西存在线程里呢?
     
    进程:导入的模块、执行的python文件的文件所在位置、内置的函数、文件里面的这些代码、全局变量等等,
     
    然后线程里面有自己的堆栈(类似于一个列表,后进先出)和寄存器,里面存着自己线程的变量,操作(add)等等,占用的空间很小。

    2.线程与进程效率对比的具体代码示例

    进程与线程开启效率比较,具体代码示例
    import time
    from threading import Thread
    from multiprocessing import Process
     
    def func():
    # time.sleep(3)
    print('xxxx')
     
    if __name__ == '__main__':
     
    t_list = []
    t_s_t = time.time() # 所有线程的开始时间
    for i in range(100): # 创建100个线程
    t = Thread(target=func,) # 创建线程
    t_list.append(t)
    t.start()
    [tt.join() for tt in t_list] # join等待所有线程执行
    t_e_t = time.time() # 所有线程的结束时间
    t_dif_t = t_e_t - t_s_t # 所有线程的执行时间
     
    p_list = []
    p_s_t = time.time()
    for i in range(100):
    p = Process(target=func,)
    p_list.append(p)
    p.start()
    [pp.join() for pp in p_list] # 等待所有进程执行
    p_e_t = time.time()
    p_dif_t = p_e_t - p_s_t # 所有进程的执行时间
    print('多线程的时间>>>',t_dif_t) # 时间对比,体现效率
    print('多进程的时间>>>',p_dif_t)
     
    print('主线程结束')
    PythonCopy

    5、线程之间是数据共享的

    import time
    from threading import Thread
     
    num = 100
     
    def func():
    # time.sleep(3)
    global num # 这里可以拿到本进程中的全局变量,子线程可以对他进行改变,体现了线程共享同进程的数据
    num -= 1
     
    if __name__ == '__main__':
     
    # for i in range()
     
    t = Thread(target=func,)
    t.start()
    t.join() # 等待线程执行完,才能看到效果
    print('主线程的num',num) # 99 这里输出,是子线程改变后的全局变量
    PythonCopy

    6、验证多线程共享数据资源造成数据不安全

    import time
    from threading import Thread
     
     
    num = 100
     
    def func():
    global num
    tep = num
    time.sleep(0.001) # 模拟延迟,这就会有多个子线程,拿到同一个数值,而不是按顺序修改,说明了线程间会导致数据不安全
    tep = tep - 1 # 需要通过加锁来,确保安全,加锁牺牲了效率,保证了安全
    num = tep
    # num -= 1
     
     
    if __name__ == '__main__':
    t_list = []
    for i in range(100):
    t = Thread(target=func,) # 创建一个子线程
    t_list.append(t)
    t.start()
     
    [tt.join() for tt in t_list] # 等待所有的子线程结束
    print("主线程的num", num)
    PythonCopy

    7、加锁解决共享数据不安全的问题

    import time
    from threading import Thread,Lock
     
    num = 100def func(tl):
    # time.sleep(3)
    # print('xxxxx')
    time.sleep(1)
    global num # 这里global,一开始所有的子线程拿到的都是100,随着加锁里面的代码执行,这个num一直在改变
    print(num)
    tl.acquire() # 加锁保证只有一个子线程进入,然后执行,这里所有的子线程会变成串行,牺牲了效率,保证了安全
    tep = num
    time.sleep(0.001)
    tep = tep - 1
    num = tep
    tl.release()
    # num -= 1
     
    if __name__ == '__main__':
    tl = Lock()
    t_list = []
    for i in range(10):
    t = Thread(target=func,args=(tl,)) # 异步提交100个子线程
    t_list.append(t)
    t.start()
    [tt.join() for tt in t_list] # 这样join等待所有的子线程
    # t.join()
    print('主线程的num',num)
    PythonCopy

    8、死锁现象

    双方互相等待对方释放对方手里拿到的那个锁
    import time
    from threading import Thread,Lock,RLock
     
    def func1(lock_A,lock_B):
    lock_A.acquire()
    time.sleep(0.5)
    print('alex拿到了A锁')
    lock_B.acquire() # 子线程t2此时的lock_B锁还没释放,t1阻塞在这里,等待其释放
    print('alex拿到了B锁')
    lock_B.release()
    lock_A.release()
     
    def func2(lock_A,lock_B):
    lock_B.acquire()
    print('taibai拿到了B锁')
    lock_A.acquire() # 子线程t1此时的lock_A锁还没释放,t2阻塞在这里,等待其释放
    print('taibai 拿到了A锁')
    lock_A.release()
    lock_B.release()
     
    if __name__ == '__main__':
    lock_A = Lock()
    lock_B = Lock()
    t1 = Thread(target=func1,args=(lock_A,lock_B))
    t2 = Thread(target=func2,args=(lock_A,lock_B))
    t1.start()
    t2.start()
    PythonCopy

    9、递归锁,解决死锁现象

    RLock 同样是互斥的.里面存着个计数器,拿到一个锁,加1,释放一个锁,减1,
    同一时间,只有一个线程进入了锁里面,外面的线程,
    只有等这个线程释放了所有的锁,才可以进行争抢这个锁的,进行执行
    注意:
    锁必须这样创建,
    lock_A = lock_B = RLock()
    必须要同时指向同一个递归锁,才能解决问题
    import time
    from threading import Thread, Lock, RLock
     
     
     
    def func1(lock_A, lock_B):
    lock_A.acquire()
    time.sleep(0.5)
    print("alex拿到了A锁")
    lock_B.acquire()
    print("alex拿到了B锁")
    lock_B.release()
    lock_A.release()
     
    # 同一时间,只有一个子线程抢到了递归锁,其他子线程就都抢不到了,须等到这个子线程执行完毕,全部释放了,才能重新抢锁def func2(lock_A, lock_B):
    lock_B.acquire()
    print("taibai拿到了B锁")
    lock_A.acquire()
    print("taibai拿到了A锁")
    lock_A.release()
    lock_B.release()
     
     
    if __name__ == '__main__':
    # lock_A = RLock() # 这样不行
    # lock_B = RLock() #
    lock_A = lock_B = RLock() # 必须同时指向一个递归锁,才能解决死锁
    # lock_A = lock_B = Lock() # 这样也不行
    t1 = Thread(target=func1, args= (lock_A,lock_B))
    t2 = Thread(target=func2, args=(lock_A,lock_B))
     
    t1.start()
    t2.start()
    PythonCopy

    10、守护线程

    守护线程:主线程等着进程中所有非守护线程的结束,才算结束
     
    守护进程:主进程代码结束,守护进程跟着结束
    import time
    from threading import Thread
    from multiprocessing import Process
     
     
    def func1():
    time.sleep(3)
    print("任务1结束")
     
    def func2():
    time.sleep(2)
    print("任务2结束")
     
     
    if __name__ == '__main__':
    t1 = Thread(target=func1,)
    t2 = Thread(target=func2,)
    # t1.daemon = True # 线程1不会执行,只有线程2执行,因为守护线程的代码执行时间比其他的子线程的代码执行时间长,
    t2.setDaemon(True) # t2设置为守护线程,但是t2的执行时间,比其他子线程的时间短,所以会打印结果
    t1.start()
    t2.start()
     
    print("主线程结束") # 主线程代码虽然结束,但是还在等待非守护子线程的结束才结束
    # # 守护线程会跟着主线程的结束而结束,不是跟着主线程的代码结束而结束,
     
    # # 对比来看进程# p1 = Process(target=func1,)# p2 = Process(target=func2,)# p1.daemon = True # 守护进程跟着主进程的代码结束,而结束,# p1.start()# p2.start()## print("主进程结束")
    PythonCopy

    fe:在这里我们简单总结一下:

    进程是最小的内存分配单位
     
    线程是操作系统调度的最小党委
     
    线程被CPU执行了
     
    进程内至少含有一个线程
     
    进程中可以开启多个线程 
     
    开启一个线程所需要的时间要远小于开启一个进程
     
    多个线程内部有自己的数据栈,数据不共享
     
    全局变量在多个线程之间是共享的
    PythonCopy

    11、信号量(也是一把锁,可以同时执行多个线程)

    实际上也是一把锁,就是同一时间可以进入多个子线程,
    可以自己设定,同一时间可以执行多少个子线程,默认为cup_count()的数量(cup的核数)
    import time
    import random
    from threading import Thread,Semaphore
     
    def func1(i,s):
    s.acquire()
    # time.sleep(1)
    print('客官%s里边请~~'%i) # 一开始可以同时进入四个子线程,因为一开始4个信号量都为空,
    # 然后就是根据每个子线程的执行时间的不同,释放一个,进入一个,或者释放多个,进入多个
    time.sleep(random.randint(1, 3)) # 模拟每个子进程的执行时间的不同
    s.release()
     
     
    if __name__ == '__main__':
    s = Semaphore(4) # 信号量
    for i in range(10):
    t = Thread(target=func1,args=(i,s))
    t.start()
    PythonCopy

    12、事件(同进程的事件)

    from threading import Thread,Event
     
    e = Event() #默认是False,
     
    # print(e.isSet())print('开始等啦')
    e.set() #将事件对象的状态改为Trueprint(e.isSet())# e.clear() #将e的状态改为False
     
    e.wait() #如果e的状态为False,就在这里阻塞print('大哥,还没完事儿,你行')
     
  • 相关阅读:
    C#调用Halcon
    C#跨窗体程序调用方法的具体操作
    C#调用DLL报错:试图加载格式不正确的程序
    C#窗体程序设置禁用关闭按钮
    C#窗体程序设置禁用关闭按钮
    C#在字符串中查询指定字符串是否存在
    poj1654
    poj1873
    poj2451
    poj1113
  • 原文地址:https://www.cnblogs.com/yipianshuying/p/10045329.html
Copyright © 2011-2022 走看看