• 大聊Python----进程和线程


    什么是线程

    线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

    什么是进程

    程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称之为进程。程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念。

    在多道编程中,我们允许多个程序同时加载到内存中,在操作系统的调度下,可以实现并发地执行。这是这样的设计,大大提高了CPU的利用率。进程的出现让每个用户感觉到自己独享CPU,因此,进程就是为了在CPU上实现多道编程而提出的。

    有了进程为什么还要线程

    进程有很多优点,它提供了多道编程,让我们感觉我们每个人都拥有自己的CPU和其他资源,可以提高计算机的利用率。很多人就不理解了,既然进程这么优秀,为什么还要线程呢?其实,仔细观察就会发现进程还是有很多缺陷的,主要体现在两点上:

      1、进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。

      2、进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行。

    例如,我们在使用qq聊天, qq做为一个独立进程如果同一时间只能干一件事,那他如何实现在同一时刻 即能监听键盘输入、又能监听其它人给你发的消息、同时还能把别人发的消息显示在屏幕上呢?你会说,操作系统不是有分时么?但我的亲,分时是指在不同进程间的分时呀, 即操作系统处理一会你的qq任务,又切换到word文档任务上了,每个cpu时间片分给你的qq程序时,你的qq还是只能同时干一件事呀。

    再直白一点, 一个操作系统就像是一个工厂,工厂里面有很多个生产车间,不同的车间生产不同的产品,每个车间就相当于一个进程,且你的工厂又穷,供电不足,同一时间只能给一个车间供电,为了能让所有车间都能同时生产,你的工厂的电工只能给不同的车间分时供电,但是轮到你的qq车间时,发现只有一个干活的工人,结果生产效率极低,为了解决这个问题,应该怎么办呢?。。。。没错,你肯定想到了,就是多加几个工人,让几个人工人并行工作,这每个工人,就是线程!

    Python threading模块

    最简单的多线程程序:

    import threading  # 多线程模块
    import time
    
    def run(n):
        print("test name is",n)
        time.sleep(2)
    
    test1 = threading.Thread(target=run,args=("test1",))
    test2 = threading.Thread(target=run,args=("test2",))
    
    test1.start() # 开始多线程 test1
    test2.start() # 开始多线程 test2

    显示结果:

    用类的方式实现最简单的多线程程序:

    import threading
    import time
    
    class MyThread(threading.Thread):
        def __init__(self,n):
            super(MyThread,self).__init__()
            self.n = n
    
        def run(self):
            print("thr tast name is",self.n)
            time.sleep(2)
    
    test1 = MyThread("test1")
    test2 = MyThread("test2")
    
    test1.start()
    test2.start()

    程序的执行结果为:

     

    Join(等待线程)

    使用Join时,程序会等子线程结束完毕,不会等主线程结束完毕。

    像下面的程序,想知道多线程运算的时间,所以需要join模块,否则无法计算多线程运算的时间!

    import threading
    import time
    
    def run(n):  # 子线程
        print("test name is",n)
        time.sleep(2)
    
    start_time = time.time()
    str_objects = []
    for i in range(50):  # 主线程
        t= threading.Thread(target=run,args=("test-%s"%i,))
        t.start()
        str_objects.append(t)
    
    for i in str_objects:
        i.join()  # 等待 相当wait()
    
    
    
    print("The coast times is ",time.time() - start_time) # 主线程

    程序执行的结果为:

    test name is test-0
    test name is test-1
    test name is test-2
    test name is test-3
    test name is test-4

    ·······················

    ·······················

    test name is test-46
    test name is test-47
    test name is test-48
    test name is test-49
    The coast times is 2.009114980697632

    Daemon (守护线程)

    使用Daemon时,程序会等主线程结束完毕,不会等守护线程(子线程)结束完毕。

    可以看下面的程序:

    import threading
    import time
    
    def run(n):  # 子线程
        print("test name is",n)
        time.sleep(2)
    
    start_time = time.time()
    str_objects = []
    for i in range(50):  # 主线程
        t= threading.Thread(target=run,args=("test-%s"%i,))
        t.setDaemon(True) # 把当前线程设置为守护线程
        t.start()
        str_objects.append(t)
    
    # for i in str_objects:
    #     i.join()  # 等待 相当wait()
    
    print("The coast times is ",time.time() - start_time) # 主线程

    程序的执行结果为:

    test name is test-0
    test name is test-1
    test name is test-2
    test name is test-3
    test name is test-4

    ·······················

    ·······················

    test name is test-46
    test name is test-47
    test name is test-48
    test name is test-49

    The coast times is  0.020000934600830078

    那么实际的应用场景是:

    当你写一个SocketServer,而SocketServer的实际情况是每一个链接过来,SocketServer就会为这个链接分配一个新的线程,启动一个新线程之后,那么如果手动的把SocketServer停掉,那么这种情况下手能停服务,那它就要宕了,那么这种情况还需等线程结束吗?那就是不等线程结束了,它就结束了,像上面的这种场景就可以把每一个Socket线程都可以设置为守护线程,主线程一宕也就是主线程一退出全部都退出。

    上面的线程相互之间无沟通,无数据共享,而线程是可以相互沟通和共享数据的,所以下面的程序是线程之间相互沟通和共享数据的!若想实现这个功能,则需要使用线程锁!

    那么什么是线程锁(互斥锁)呢?

    一个进程下可以启动多个线程,多个线程共享父进程的内存空间,也就意味着每个线程可以访问同一份数据,此时,如果2个线程同时要修改同一份数据,会出现什么状况?

    import threading
    import time
    
    def run(n):  # 子线程
        lock.acquire()  # 开锁
        global num
        num += 1
        time.sleep(0.1)
        lock.release() # 关锁
    
    lock = threading.Lock()
    num = 0  # 主线程
    str_objects = []
    for i in range(50):  # 主线程
        t= threading.Thread(target=run,args=("test-%s"%i,))
        # t.setDaemon(True) # 把当前线程设置为守护线程
        t.start()
        str_objects.append(t)
    
    for i in str_objects:
        i.join()  # 等待 相当wait()
    
    print("--------------all threads has finished ...",threading.current_thread(),threading.active_count())
    print("num:",num)

    程序运行后的结果为:

    正常来讲,这个num结果应该是50, 但在python 2.7上多运行几次,会发现,最后打印出来的num结果不总是50,为什么每次运行的结果不一样呢? 哈,很简单,假设你有A,B两个线程,此时都 要对num 进行减1操作, 由于2个线程是并发同时运行的,所以2个线程很有可能同时拿走了num=0这个初始变量交给cpu去运算,当A线程去处完的结果是1,但此时B线程运算完的结果也是1,两个线程同时CPU运算的结果再赋值给num变量后,结果就都是1。那怎么办呢? 很简单,每个线程在要修改公共数据时,为了避免自己在还没改完的时候别人也来修改此数据,可以给这个数据加一把锁, 这样其它线程想修改此数据时就必须等待你修改完毕并把锁释放掉后才能再访问此数据。

    为什么会运行5s呢?

    因为它要执行50次,每次0.1s,想当于是个串行,所以会出现这样的效果,其实像上面的程序里的线程锁适用于Python2.6/2.7,因为在Python3里已经对线程锁进行优化了,在代码里不需要在对线程锁进行编写。

    GIL VS Lock

    机智的同学可能会问到这个问题,就是既然你之前说过了,Python已经有一个GIL来保证同一时间只能有一个线程来执行了,为什么这里还需要lock? 注意啦,这里的lock是用户级的lock,跟那个GIL没关系 ,具体我们通过下图来看一下+配合我现场讲给大家,就明白了。

    那你又问了, 既然用户程序已经自己有锁了,那为什么C python还需要GIL呢?加入GIL主要的原因是为了降低程序的开发的复杂度,比如现在的你写python不需要关心内存回收的问题,因为Python解释器帮你自动定期进行内存回收,你可以理解为python解释器里有一个独立的线程,每过一段时间它起wake up做一次全局轮询看看哪些内存数据是可以被清空的,此时你自己的程序 里的线程和 py解释器自己的线程是并发运行的,假设你的线程删除了一个变量,py解释器的垃圾回收线程在清空这个变量的过程中的clearing时刻,可能一个其它线程正好又重新给这个还没来及得清空的内存空间赋值了,结果就有可能新赋值的数据被删除了,为了解决类似的问题,python解释器简单粗暴的加了锁,即当一个线程运行时,其它人都不能动,这样就解决了上述的问题,这可以说是Python早期版本的遗留问题。

    RLock(递归锁)

    说白了就是在一个大锁中还要再包含子锁

    import threading,time
    
    def run1():
        print("grab the first part data")
        lock.acquire()
        global num
        num +=1
        lock.release()
        return num
    def run2():
        print("grab the second part data")
        lock.acquire()
        global  num2
        num2+=1
        lock.release()
        return num2
    def run3():
        lock.acquire()
        res = run1()
        print('--------between run1 and run2-----')
        res2 = run2()
        lock.release()
        print(res,res2)
    if __name__ == '__main__':
        num,num2 = 0,0
        lock = threading.RLock()
        for i in range(10):
            t = threading.Thread(target=run3)
            t.start()
    while threading.active_count() != 1:
        print(threading.active_count())
    else:
        print('----all threads done---')
        print(num,num2)

    程序执行后的结果为:

    ······························································

     ·····························································

     

    Semaphore(信号量)

     互斥锁 同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据 ,比如厕所有3个坑,那最多只允许3个人上厕所,后面的人只能等里面有人出来了才能再进去。

    import threading,time
    
    def run(n):
        semaphore.acquire()
        time.sleep(1)
        print("run the thread: %s
    " %n)
        semaphore.release()
    
    
    if __name__ == '__main__':
        num= 0
        semaphore = threading.BoundedSemaphore(5) #最多允许5个线程同时运行
        for i in range(20):
            t = threading.Thread(target=run,args=(i,))
            t.start()
    while threading.active_count() != 1:
        pass #print threading.active_count()
    else:
        print('----all threads done---')
        print(num)

    程序运行后的结果:

    run the thread: 2
    run the thread: 4
    run the thread: 3
    run the thread: 1
    run the thread: 0

    run the thread: 7
    run the thread: 8
    run the thread: 6
    run the thread: 5
    run the thread: 9

    run the thread: 12
    run the thread: 11
    run the thread: 10
    run the thread: 14
    run the thread: 13

    run the thread: 15
    run the thread: 19
    run the thread: 17
    run the thread: 16
    run the thread: 18

    ----all threads done---
    0

    Events

    事件是一个简单的同步对象;
    该事件代表一个内部标志和线程
    可以等待设置标志,或者自己设置清除标志。

    event = threading.Event()
    event.wait()#客户端线程可以等待设置标志
    event.set()#一个服务器线程可以设置或重置它
    event.clear()
    如果设置了标志,则wait方法不会执行任何操作。
    如果该标志被清除,则等待将被阻塞,直到它再次被设置为止。
    任意数量的线程都可以等待同一事件。

    通过Event来实现两个或多个线程间的交互,下面是一个红绿灯的例子,即起动一个线程做交通指挥灯,生成几个线程做车辆,车辆行驶按红灯停,绿灯行的规则。

    import time
    import threading
    
    event = threading.Event()
    
    def lighter():
        count = 0
        event.set()
        while True:
            if count > 5 and count <= 10:
                event.clear()
                print("33[41;1m 红灯亮 33[0m")
            elif count > 10:
                event.set()
                count = 0
            else:
                print("33[42;1m 绿灯亮 33[0m")
            time.sleep(1)
            count += 1
    
    def car(name):
        while True:
            if event.is_set():
                print("[%s] running..."% name)
                time.sleep(1)
            else:
                print("[%s] sees red light,waiting..." %name)
                event.wait()
                print("33[34;1m[%s] green light is on,start going...33[0m"%name)
    
    
    
    
    light = threading.Thread(target=lighter,)
    light.start()
    
    cars = threading.Thread(target=car,args=("Tesla",))
    cars.start()

    程序执行后的结果为:

    绿灯亮
    [Tesla] running....

    绿灯亮 
    [Tesla] running....

    绿灯亮 
    [Tesla] running....

    绿灯亮 
    [Tesla] running....

    绿灯亮 
    [Tesla] running....

    绿灯亮 
    [Tesla] running....

    红灯亮

    [Tesla] sees red light,waiting...

    红灯亮

    [Tesla] sees red light,waiting...

    红灯亮

    [Tesla] sees red light,waiting...

    红灯亮

    [Tesla] sees red light,waiting...

    红灯亮

    [Tesla] sees red light,waiting...

    [Tesla] green light is on,start going...

    绿灯亮 
    [Tesla] running....

  • 相关阅读:
    AtCoder Beginner Contest 167
    AtCoder Beginner Contest 165
    Codeforces Round #732 (Div. 2)
    【贪心 + 模拟】UVA10382 Watering Grass
    【BCC】冗余路径(做法 + 证明)
    数据库作业[定时执行任务]的创建(转)
    SQL数据类型大全 《转自网络》
    C#的OpenFileDialog和SaveFileDialog的常见用法(转)
    团队展示(团队)
    高级软件工程第一次作业--准备
  • 原文地址:https://www.cnblogs.com/zhuifeng-mayi/p/9273705.html
Copyright © 2020-2023  润新知