09-2 | 多线程

逸兴
逸兴
逸兴
57
文章
25
评论
2020-06-0321:53:12
评论
4270字阅读14分14秒

多线程基本概念

一个进程中,有多个工作线程,是为多线程。

主线程一般是做资源管理,协调,监视等功能,而工作线程是真正运行工作函数,真正干活的。

每个线程都有自己的stack栈,栈是一种后进先出内存数据结构。每次执行函数,都要将函数压到栈中。所以多线程中,如果调用同一个函数,这个函数会被分别压到各个线程的栈中,互不干扰。

每个线程调用函数,都是在自己的栈中调用的,栈是线程私有的。

线程的数据也是保存在线程栈中的。

注意:

如果主线程创建了工作线程,而工作线程中有创建了子子工作线程,此时的子线程是不需要对子子工作线程负责的。也就是子线程运行完成后,可直接退出,而不关心其子线程是否还在运行。

线程安全

通过多线程执行一段代码,如果不会产生不确定结果,那么这段代码就是线程安全的。

多线程在执行过程中,由于共享同一进程中的数据,多个线程使用同一个数据,那么就有可能被相互修改,从而导致某时刻无法确定这个数据的值,最终的运行结果将不可预期,这就是线程不安全。

daemon线程

Python中,构造线程的时候,可以设置daemon属性,这个属性必须在start方法前设置好。
daemon是一个布尔值,在线程源码中,使用self._daemonic 记录daemon值。

当前线程默认是False,如果工作线程没有设置daemon,则使用当前线程的 False。

源码如下:

在初始化函数中,传入的daemon默认是None

09-2 | 多线程

继续向下看,如果没有设置 daemon 则使用 current_thread() 的daemon

09-2 | 多线程

如果线程的daemon是True,则这个线程是daemon线程,否则是non-daemon线程。

定义MainThread也就是主线程时就已经写死了 daemon是False

09-2 | 多线程

总结:

  • 线程具有一个daemon属性,可以手动设置True 或 False
  • 如果不设置,则使用 主线程的daemon值,默认为False
名称含义
daemon属
表示线程是否是daemon线程,这个值必须在start()之前设置,否则引发
RuntimeError异常
isDaemon()是否是daemon线程
setDaemon设置为daemon线程,必须在start方法之前设置

daemon属性的作用:

多线程环境中,在主线程执行完成后,如果剩余的工作线程是daemon线程,则主线程不会关心daemon线程是否执行完毕,都会结束掉daemon线程。

如果剩余线程是non-daemon,则主线程会等待non-daemon线程执行完成后,再退出。

import threading
import time


def worker(timeout):
    print('in the', threading.current_thread().name, threading.current_thread().ident)
    time.sleep(timeout)
    print('{}: finished ~~~~~~~~~~~~~~~~'.format(threading.current_thread().name))


t1 = threading.Thread(target=worker, name='worker1', args=(10, ))   # 不设置daemon,则使用 主线程的daemon,是False
t2 = threading.Thread(target=worker, name='worker2', daemon=True, args=(10, ))
t3 = threading.Thread(target=worker, name='worker3', daemon=False, args=(5, ))

t1.setDaemon(True)      # 可通过setDaemon()方法设置daemon值,必须在线程执行前设置。

t1.start()
t2.start()
t3.start()
print('*' * 30)
09-2 | 多线程

主线程退出时,并不会关心 daemon 线程是否执行完,都会将其结束。

主线程要等待所有的non-daemon线程执行完成后,才能退出。

简单来说, 就是默认情况下, 主线程执行完毕, 但是其子线程还没有执行完, 这时候主线程必须等待子线程运行结束, 才能结束。

如果这个子线程无关紧要就将其daemon=True, 这样主线程执行完, 它就会被强制结束。

daemon的应用场景

主要应用场景有:
1. 后台任务。如发送心跳包、监控,这种场景最多
2. 主线程工作才有用的线程。如主线程中维护这公共的资源,主线程已经清理了,准备退出,而工作线程使用这些资源工作也没有意义了,一起退出最合适
3. 随时可以被终止的线程

如果主线程退出,想所有其它工作线程一起退出,就使用daemon=True来创建工作线程。
比如,开启一个线程定时判断WEB服务是否正常工作,主线程退出,工作线程也没有必须存在了,应该
随着主线程退出一起退出。这种daemon线程一旦创建,就可以忘记它了,只用关心主线程什么时候退
出就行了。
daemon线程,简化了程序员手动关闭线程的工作。

join() 方法-父线程阻塞

线程使用join方法后,主线程会阻塞一定时间,不会向下执行。

这段时间内,如果工作线程执行完成后,主线程继续运行。如果时间到了, 工作线程还没有执行完,主线程也会继续执行。

join(timeout), 指定阻塞时间,缺省是None,代表永久阻塞。

import threading
import time


def worker(timeout):
    print('in the', threading.current_thread().name, threading.current_thread().ident)
    time.sleep(timeout)
    print('finished ~~~~~~~~~~~~~~')


t1 = threading.Thread(target=worker, name='worker1', daemon=True, args=(10,))

t1.start()
t1.join()   # 使用join方法,将t1线程加入到主线程中去,进行阻塞。哪个线程启动它,就加入到哪里去

print('*' * 30)
09-2 | 多线程

主线程要等到 工作线程执行完成,即使工作线程是daemon线程。

可以向 join() 函数中传入阻塞时间单位是s,如果在阻塞时间内,工作线程依然没有执行完,主线程继续执行,主线程执行完后,如果还有工作线程,python进程会检查工作线程是否是daemon,daemon线程则会将其结束,退出进程。如果是non-daemon线程,则需要等待其执行完毕才可退出进程。

threading.local类

# encoding = utf-8
__author__ = "mcabana.com"


import threading


class A:
    def __init__(self):
        self.x = 100


a = A()
y = 10
print(a.x)


def worker():
    print('in the', threading.current_thread().name, threading.current_thread().ident, y, a.x)
    a.x += 1
    print(a.x)  # 在线程内,可以修改类属性


t1 = threading.Thread(target=worker, name='worker1')

t1.start()
t1.join()

print('*' * 30)
print(a.x)
09-2 | 多线程

通过上面的代码可以发现,在线程内是可以调用并修改过线程外实例的属性的,但是在多线程环境下这种修改是极不安全的,如下:

# encoding = utf-8
__author__ = "mcabana.com"


import threading


class A:
    def __init__(self):
        self.x = 100


a = A()
y = 10
print(a.x)


def worker():
    global y

    for c in range(1000000):
        a.x += 1
    print('in the', threading.current_thread().name, a.x)
    # a.x += 1
    # print(a.x)  # 在线程内,可以修改类属性


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

print('*' * 30, a.x)
09-2 | 多线程

可以看到在计算量大的情况下,a.x 的值是无法预测的,并且每次都是不一样的,这就是前面说的,多个线程使用同一个数据,无法保证线程安全。同样的使用全局变量也会出现这个问题。

只有使用函数变量,才可以说是绝对安全的,因为函数变量是函数内有效,每个线程都会压栈,每个栈都是独立的。

关于threading.local()

# encoding = utf-8
__author__ = "mcabana.com"


import threading


a = threading.local()
print(a)

a.x = 100


def worker():
    print(a)
    print(hasattr(a, 'x'))
    print(threading.current_thread().name, a.x)


t1 = threading.Thread(target=worker, name='worker1')
t1.start()

print('*' * 30, a.x)
09-2 | 多线程

通过threading.local() 实例化的对象,在线程内是无法访问其属性的。

在当前线程中,为threading.local()实例的属性创建的新值,只对当前线程有关,如果不是therading.local的实例,则不影响。

# encoding = utf-8
__author__ = "mcabana.com"


import threading


a = threading.local()
print(a)

a.x = 100


def worker():
    a.y = 10
    print('in the worker', hasattr(a, 'x'), hasattr(a, 'y'))    # False, True
    print('in the worker', threading.current_thread().name, a.y)


t1 = threading.Thread(target=worker, name='worker1')

t1.start()
t1.join()

print('*', a.x)     # 100
print('*', hasattr(a, 'y'))     # False
09-2 | 多线程

threading.local 的属性访问与赋值,只与当前线程有关。




https://www.hugbg.com/archives/2777.html
逸兴
  • 本文由 发表于 2020-06-0321:53:12
  • 除非特殊声明,本站文章均为原创,转载请务必保留本文链接
01-1 数据类型 基础语法

01-1 数据类型

第一章 数据类型 使用type() 函数可以查看数据类型 1.1 字符串 str 字符串是使用单引号或双引号括起来的任意文本。 比如'abc', '123'等 字符串类型 字符串类型用str表示 st...
09-5 | asyncio基本使用 并发编程

09-5 | asyncio基本使用

第一节 关于asyncio asyncio 在3.4 版本中加入到标准库, asyncio基于selector实现, 看似库, 其实是个框架, 包含异步IO, 事件循环, 协程, 任务等内容。 通过a...
09-4 | 全局解释器锁 & 多进程 & 池 并发编程

09-4 | 全局解释器锁 & 多进程 & 池

GIL CPython 在解释器进程级别有一把锁,叫做GIL,即全局解释器锁。 GIL 保证CPython进程中,只有一个线程执行字节码。甚至是在多核CPU的情况下,也只允许同时只能 有一个CPU核心...
匿名

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: