TOC

多线程Deamon

    在使用Python的threading类库时,可以设置线程是否为daemon线程,但是需要注意的是这里的deamon线程并非Linux中的守护进程,它并非是在后台运行的守护进程,这块的概念稍微有点出入,具体如下;
    通过threading模块的源码可以看到,所有子线程的daemon属性都是取自于当前线程的deamon属性,因为创建子线程就是主线程负责的,所以这个当前线程就是主线程,又因为主线程的daemon属性都为non-deamon,所以默认情况下所有的线程均为non-daemon线程;
    那么对于non-daemon线程而言,主线程结束时,会检查所有non-daemon的子线程是否结束,如果还未结束,则主线程等待所有non-daemon结束后再退出,即使主线程显示给定了sys.exit()或者主线程抛出异常也不会退出,都会等待non-daemon线程结束后主线程再结束;
    而对于deamon线程而言,这里都daemon线程可能和我们平常理解都daemon线程有点不一样,我们平常理解的daemon线程是守护进程,主线程结束,子线程依然在后台运行,那么对于Python中的threading模块而言,它不是这样的,一旦主线程结束,会将所有的daemon线程全部杀死,然后主线程再退出程序;
daemon:返回线程是否是daemon线程,如果要修改这个值,必须在线程start之前执行,否则引发RuntimeError异常;
isDaemon:返回一个布尔值,表示该线程是否是一个daemon线程;
setDaemon:设定线程是否为daemon线程,必须在start之前设置;

daemon线程

    针对deamon线程而言,主线程一旦结束,将杀死所有的daemon线程,然后退出程序,主线程不会等待daemon的子线程结束之后再结束,而是暴力的将daemon线程杀死,如下示例;
def worker():
    time.sleep(1)
    print(threading.current_thread().daemon)

thread1=threading.Thread(target=worker,daemon=True)
thread1.start()
print("---------end---------")
# ---------end---------
    可以看到最后的打印结果,仅看到了主线程打印出的"---------end---------"信息,而thread1线程却无任何输出信息,需要注意的是,它并非在后台执行,而是当主线程结束之后,直接将这个thread1给杀死了;

non-daemon线程

    对于non-daemon而言,就很好理解了,threading模块创建的子线程默认也就是non-daemon,non-daemon和daemon线程正好相反,主线程结束时,如果还存在其他的non-daemon线程,主线程将一直等待,直到所有non-daemon线程退出,主线程才会退出;
def worker():
    time.sleep(1)
    print(threading.current_thread().daemon)

thread1=threading.Thread(target=worker,daemon=False)
thread1.start()
print("---------end---------")
# ---------end---------
# False
主线程异常
    在non-daemon情况下,主线程是需要等待所有non-daemon线程结束才会结束的,即使主线程抛出异常,或显示sys.exit(),主线程都不会退出,它仅仅影响整个程序的退出码;
def worker():
    time.sleep(1)
    print(threading.current_thread().daemon)

thread1=threading.Thread(target=worker,daemon=False)
thread1.start()
print("---------end---------")
sys.exit(11)
# ---------end---------
# False
# Process finished with exit code 11

多线程Join

    Join方法是线程使用的标准方法,它主要是用来阻塞线程继续往下执行的一个属性,假设现在有两个线程,一个主线程一个子线程,如果在主线程中join了子线程,那么主线程自join之后的指令,将需要等待子线程结束之后,才会执行,如下示例;
def worker():
    time.sleep(3)
    print(threading.current_thread().name)

thread1=threading.Thread(target=worker,name='Thread-1')
thread1.start()
print("---------join---------")
thread1.join()
print("---------end---------")
# ---------join---------
# Thread-1
# ---------end---------
    可以看到上述的输出信息,一般来讲,主线程和子线程都是并行执行的,而针对于上述的例子中,子线程睡眠了3秒,按常理来讲,主线程的"---------end---------"信息应该是首先打印的,但是它却最后才打印出来;
    这主要就是join的功效之一,因为我们在主线程中join了"Thread-1"这个子线程,所以主线程中的join之后的指令,将需要等待"Thread-1"这个子线程执行结束之后才会执行;

超时时间

    因为join是一种阻塞行为,一旦A进程join了B进行,如果B进行一直都无法结束,那么A进程也将一直都处于阻塞状态,所以对于join方法,也提供了一个timeout属性,用来解决这种异常的行为;
def worker():
    time.sleep(3)
    print(threading.current_thread().name)

thread1=threading.Thread(target=worker,name='Thread-1')
thread1.start()
print("---------join---------")
thread1.join(1) # 阻塞1秒
print("---------end---------")
# ---------join---------
# ---------end---------
# Thread-1
    可以看到输出的结果,仅为两条,子线程中的输出信息并没有打印出来,因为子线程sleep了3秒,而主线程中的join只阻塞1秒,所以主线程阻塞1秒之后,发现子线程还未结束,那么就不会继续再等待了,主线程继续往下执行,然后因为我们的线程是一个non-deamon线程,所以主线程执行结束之后,并未直接退出,而是等待子线程结束之后,主线程再退出;

子线程join

    上述是一个主线程去join一个子线程,那么对于子线程与子线程之间进行join也是一样的,假设现在有两个子线程A和B,当B线程中join了A线程时,那么B线程中,自join之后的指令,将需要等待A线程执行完成之后才能继续往下执行,如下示例;
def worker1():
    time.sleep(3)
    print(threading.current_thread().name)
def worker2(thread):
    thread.join()
    print(threading.current_thread().name)

thread1=threading.Thread(target=worker1)
thread1.start()
thread2=threading.Thread(target=worker2,args=(thread1,))
thread2.start()
print("---------end---------")
# ---------end---------
# Thread-1
# Thread-2
    同时,通过这个案例也可以看出来,当主线程没有join任何子线程时,那么主线程将不会阻塞,也正因此,可以看出来信息"---------end---------"作为第一条输出;

结合non-deamon

    默认情况下,线程都是non-deamon的,即主线程会等待所有non-deamon线程结束之后,再退出,那么non-deamon结合join也是有很多应用场景使用的,join我们可以理解为join方是否继续往下执行;
    如下示例,主线程join了子线程,那么因为子线程是一个non-deamon线程,所以主线程并不会在执行完之后直接退出主线程,而是等待non-deamon的子线程执行完成,但是使用了join方法之后它们的区别在于,主线程需要等待non-deamon的子线程执行完,或者执行多少秒之后,才会继续往下执行其他的指令;
def worker():
    time.sleep(2)
    print(threading.current_thread().name)

thread1=threading.Thread(target=worker,name='Thread-1',daemon=False)
thread1.start()
print("---------主线程join---------")
thread1.join() # 阻塞1秒
print("---------主线程end---------")
# ---------主线程join---------
# Thread-1
# ---------主线程end---------

结合deamon

    结合daemon线程效果就比较鲜明了,因为在针对于一个daemon线程而言,主线程是不会等待这个daemon线程执行结束之后再退出的,而是主线程指令执行完成之后,不管子线程是否执行完成,直接暴力的将其杀死,然后结束整个程序;
    那么在daemon线程中加入了join方法,问题就有点不一样了,如下示例,主线程中join了一个daemon的子线程,可以看到输出结果,即使整个daemon的子线程虽然sleep了5秒,主线程还是一直都在等待它执行完成,才执行后面的代码;
def worker():
    time.sleep(5)
    print(threading.current_thread().name)

thread1=threading.Thread(target=worker,name='Thread-1',daemon=True)
thread1.start()
print("---------主线程join---------")
thread1.join() # 阻塞1秒
print("---------主线程end---------")
# ---------主线程join---------
# Thread-1
# ---------主线程end---------
    所以由此可以看出,即使主线程退出会杀死所有未结束的daemon子线程,但是加入了join方法之后,主线程就需要等待daemon的子线程结束之后,才能继续执行自join之后的指令;

daemon线程的应用场景

    实际上,本来就没有daemon线程,只不过Python是为了简化程序的工作而提供的,使程序员不用去记录和管理那些后台进程,它的主要思想是,当把一个子线程设置为daemon线程,它会随主线程退出而退出,它的主要场景有如下几种;
    第一,后台任务,如发送心跳包、监控等场景,比如监控,工作线程在监控着很多的服务,那么一旦主线程退出,子线程也没必要再继续监控了,所以,此时我们就可以使用daemon线程,针这种只有主线程在工作才有意义的程序,主线程退出,子线程就没意义存在了这种场景,从而达到释放资源的效果;
    第二,随时可以被终止的进程,这种线程也可以使用,比如开启一个线程定时判断WEB服务是否正常工作,这种程序,那么如果主线程退出了,子线程就没必要存在了,这种daemon线程一旦创建,就可以忘记它了,简化程序的工作;

注意

    如果non-deamon线程A中,对另一个daemon线程B使用了join方法,那么这个线程B设置成deamon就没什么意义了,因为non-deamon线程A必须要等待线程B结束之后,才能执行;
    如果一个daemon线程C中,对另一个daemon线程D使用了join方法,只能说明线程C需要等待线程D,主线程退出,不管C和D是否结束,也不管他俩要等谁,都会被暴力杀死;
def worker2():
    while True:
        time.sleep(1)
        print(threading.current_thread().name)
def worker1():
    print(threading.current_thread().name)
    thread2=threading.Thread(target=worker2,name='worker-2')
    thread2.start()

thread1=threading.Thread(target=worker1,name='worker-1',daemon=True)
thread1.start()
time.sleep(4)
print("Main Exit")
# worker-1
# worker-2
# worker-2
# worker-2
# Main Exit
    可以看到上述的示例,有几个点,我们需要注意,第一,thread2为一个daemon线程,即,主线程会将其直接杀死然后退出,它是daemon的线程的主要原因可以通过源码来查看,默认开启子线程时,这个子线程的daemon值是当前线程的daemon值,在此案例中就是thread1的daemon值,因为thread2是在thread1中创建的,所以当前线程就是thread1。所以在此种场景之下,除了主线程是non-daemon,其他线程都是daemon线程。
    第二,可以看到在thread1里面创建了一个thread2,这thread2是受操作系统来管理的,它不受thread1管理,thread1执行完成之后thread2这个标识符就消失了,虽然说有引用计数器,但是线程特殊,线程不会退出,它会在后台执行。
    那么如果我们不希望这个thread2线程结束,就有很多方法了,比如将thread1的daemon值改为None,通过源码可以得出当子线程的daemon值为None时,会取当前线程,此中案例的创建thread1的线程就是主线程,因为主线程默认就是non-daemon的所以,如果thread1的值为None,则thread1为non-daemon。
    或者,我们也可以将thread1的daemon值改为False,这样的话thread2就会继承thread1的daemon值,或者我们也可以采用下面的方式来保证thread2线程不会结束。
def worker2():
    while True:
        time.sleep(1)
        print(threading.current_thread().name)
def worker1():
    print(threading.current_thread().name)
    thread2=threading.Thread(target=worker2,name='worker-2')
    thread2.start()
    thread2.join()

thread1=threading.Thread(target=worker1,name='worker-1',daemon=True)
thread1.start()
time.sleep(4)
thread1.join()
print("Main Exit")

threading.local

    前面讲过,当多线程操作同一公有资源时,如果涉及到修改该资源的操作,为了避免数据不同步可能导致的错误,需要使用互斥锁机制;
    其实,除非必须将多线程使用的资源设置为公共资源,Python threading模块还提供了一种可彻底避免数据不同步问题的方法,即本节要介绍的local()函数;
    使用local()函数创建的变量,可以被各个线程调用,但和公共资源不同,各个线程在使用local()函数创建的变量时,都会在该线程自己的内存空间中拷贝一份。这意味着,local()函数创建的变量看似全局变量(可以被各个线程调用),但各线程调用的都是该变量的副本(各调用各的,之间并无关系);
    可以这么理解,使用threading模块中的local()函数,可以为各个线程创建完全属于它们自己的变量(又称线程局部变量),正是由于各个线程操作的是属于自己的变量,该资源属于各个线程的私有资源,因此可以从根本上杜绝发生数据同步问题;

线程局部作用域

    可以看到下面的结果,五个线程的结果均为100,这是正常的结果,也是我们要的结果,这说明了一个问题,五个线程中的num变量他们是不一样的,同时也就说明,在多线程的应用场景下,也是存在作用域的概念的,不同线程的作用域是不同的,每个线程都有独立的栈,每个现在在调工作函数时,都是压线程自己的栈,各自线程的栈各自独立,互相没有任何影响;
    线程和线程之间,都是独立的,他们共享进程的资源,但是他们之间是不会互通有无的,栈都的独立的,所以,在多线程的场景下,如果使用的都是局部变量,那这线程就的安全的;
def worker():
    num=0
    for _ in range(100):
        time.sleep(0.001)
        num+=1
    print(threading.current_thread(),num)

for x in range(5):
    threading.Thread(target=worker).start()

# <Thread(Thread-3, started 123145343873024)> 100
# <Thread(Thread-1, started 123145333362688)> 100
# <Thread(Thread-2, started 123145338617856)> 100
# <Thread(Thread-5, started 123145354383360)> 100
# <Thread(Thread-4, started 123145349128192)> 100

线程全局作用域

    像上面这样,线程中的局部变量,的结恶果结果是可预期的,因为线程见变量是互不影响的,但是如果我们将局部变量改为全局变量时,这个结果就不一定了,可能在线程较小的情况下,我们无法看到效果,但是当线程数量加大,线程内的逻辑过于复杂,这样的问题就会频繁产生,因为这样就属于是所有线程去同时修改一个全局对象了,这是多线程最需要注意的地方,如下;
num=0
def worker():
    global num
    num=0
    for _ in range(100):
        time.sleep(0.001)
        num+=1
    print(threading.current_thread(),num)

for x in range(100):
    threading.Thread(target=worker).start()

# ...
# <Thread(Thread-88, started 123145969053696)> 9404
# <Thread(Thread-94, started 123146000584704)> 9405
    可以看到,最终的并不是我们最期望的结果10000,这就产生了线程安全的问题,多个线程同时去修改一个非局部作用域的数据就会产生这样的问题;

threading.local的实现

    threading.local它构建了一个与线程相关的特殊对象,我们可以理解为每个threading.local对象下面的属性,只和当前线程相关,如下,同样的例子,每个线程的num属性都只与当前线程相关,互相不干扰,如下示例;
global_data=threading.local()
def worker():
    global_data.num=0
    for _ in range(100):
        time.sleep(0.001)
        global_data.num+=1
    print(threading.current_thread(),global_data.num)

for x in range(100):
    threading.Thread(target=worker).start()

# <Thread(Thread-100, started 123145927733248)> 100
# <Thread(Thread-99, started 123145922478080)> 100
# <Thread(Thread-93, started 123145890947072)> 100
# <Thread(Thread-89, started 123145869926400)> 100

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注