并发和并行、进程和线程
线程:Thread类、线程的启动、线程的传参
threading的属性和方法、Thread实例的属性和方法
start()和run()的区别、多线程
daemon线程和join方法

并发和并行


  • 并发(concurrent)
    处理多个任务的能力,强调一个时间段
  • 并行(parallel)
    同时处理多个任务的能力,强调同一时刻互不干扰做

进一步理解可参看全局解释器锁GIL

并发的解决方案

  • 队列、缓冲区
  • 争抢
  • 预处理,缓存cache
  • 并行
  • 提速
  • 消息中间件:如RabbitMQ、ActiveMQ(Apache)、RocketMQ(阿里Apache)、kafka(Apache)等。

进程和线程


  • 在实现了线程的操作系统中,线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一个程序的执行实例就是一个进程
  • 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础
  • 程序是源代码编译后的文件,而这些文件存放在磁盘上。当程序被操作系统加载到内存中,就是进程,进程中存放着指令和数据(资源),它也是线程的容器

线程的状态

  • 就绪(Ready)
  • 运行(Running)
  • 阻塞(Blocked)
  • 终止(Terminated)

python中的进程和线程

  • 进程会启动一个解释器进程,线程共享一个解释器进程
  • 进程靠线程执行代码,至少有一个主线程,其它线程是工作线程
  • 主线程是第一个启动的线程
  • 父线程:如果线程A中启动了一个线程B,A就是B的父线程
  • 子线程:B就是A的子线程

线程


threading.Thread类

def __init__(self, group=None, target=None, name=None, args=(), kwargs=None, *, daemon=None):

先熟悉如下几个参数

  • target:线程调用的对象,就是目标函数
  • name:为线程起的名字
  • args:为目标函数传递实参,元组
  • kwargs:为目标函数关键字传参,字典

线程启动start()

import threading
import time

def work():
    print("i'm working~~~~~")
    time.sleep(1)
    print('finished')

t = threading.Thread(target=work, name="work") # 线程对象
t.start() # 启动,可注释掉本行查看效果发现如果不start,线程并不能启动

线程退出

  • Python没有提供线程退出的方法
  • 线程退出
    • 线程函数内语句执行完毕
    • 线程函数中抛出未处理的异常
import threading
import time

def work():
    count = 0
    while True:
        count += 1
        time.sleep(1)
        print("i'm working~~~~~")
        if count > 5:
            # raise RuntimeError(count)
            break
    print('finished')

t = threading.Thread(target=work, name="work")
t.start()
# 1/0
# raise RuntimeError('error')
# import sys
# sys.exit(100)

Python的线程没有优先级、没有线程组的概念,也不能被销毁、停止、挂起,那也就没有恢复、中断了
主线程退出了,工作线程也能继续工作

线程传参及线程相关属性和方法

  • args和kwargs传参:args元组,kwargs字典,本质就是函数传参
  • threading的属性和方法
    • current_thread():返回当前线程对象
    • main_thread():返回主线程对象
    • active_count():当前处于alive状态的线程个数
    • enumerate():返回所有alive的线程的列表,不包括已经终止的线程和未开始的线程
    • get_ident():返回当前线程的ID,非0整数
  • threading.Thread实例的属性和方法
    • name:只是个标识,名称可以重名,也可用getName()获取、setName()设置
    • ident:线程ID,它是非0整数。线程启动后才会有ID,否则为None。线程退出,此ID依旧可以访问,此ID可以重复使用
    • is_alive():返回线程是否活着
import threading
import time


def show_thread_info():
    print('current thread:', threading.current_thread())
    print('main thread:', threading.main_thread())
    print('active thread count:', threading.active_count())
    print('current thread id:', threading.get_ident())
    print('all alive thread:', threading.enumerate())


def work(name: str, count, *, num):
    show_thread_info()
    times = 0
    while True:
        if times > num:
            break
        time.sleep(1)
        print(f'{name} is working')
        times += 1
    print('finished')


t = threading.Thread(target=work, name="work", args=('jerry', 10), kwargs=dict(num=3))  # t即Thread实例 kwargs={'num': 3}
t.start()
print(1, t.getName())
t.setName('jerry')
print(2, t.name)

# show_thread_info() # 在主线程中执行该函数,对比不同

while True:
    time.sleep(0.5)
    print('{} {} {}'.format(t.name, t.ident, 'alive' if t.is_alive() else 'dead'))

    # if not t.is_alive():
    #     print(f'{t.name} restart')
    #     t.start()  # 线程死了后不能再次重启 RuntimeError: threads can only be started once
    if not t.is_alive():
        break

start()和run()的本质区别

import threading
import time

def work():
    print(threading.enumerate())
    print("i'm working~~~~~")
    time.sleep(1)
    print('finished')

class MyThread(threading.Thread):
    def start(self):
        print('start~~~~~')
        super().start()

    def run(self):
        print('run~~~~~')
        super().run()  # 只注释掉这行试试,并不能执行work()

t = MyThread(target=work, name='work')
# t.run()  # 执行run()看起来像是执行了work(),但这其实只是普通的函数调用,并没有创建新的线程
t.start()  # 先执行start启动一个新的线程,再执行run,将target函数运行该新的线程上

使用start方法启动线程,启动了一个新的线程,名字叫做work运行。但是使用run方法的,并没有启动新的线程,就是在主线程中调用了一个普通的函数而已。因此,启动线程需要使用start方法,且对于这个线程来说,start方法只能调用一次。(设置_started属性实现(可查看源码))

多线程


多线程,多个线程,一个进程中如果有多个线程运行,就是多线程,实现一种并发

  • 一个进程中至少有一个线程,并作为程序的入口,这个线程就是主线程
  • 一个进程至少有一个主线程,其他线程称为工作线程
import threading
import time

def work():
    t = threading.current_thread()
    for i in range(5):
        time.sleep(1)
        print(f"{t.name} {t.ident} is working~~~~~")
    print(f'{t.name} fnished')

class MyThread(threading.Thread):
    def start(self):
        super().start()

    def run(self):
        super().run()

t1 = MyThread(target=work, name='work1')
t2 = MyThread(target=work, name='work2')
t1.start()
t2.start()

daemon线程


# Thread中__init__关于daemon源码:
def __init__(self, group=None, target=None, name=None,args=(), kwargs=None, *, daemon=None):
.....
       if daemon is not None:  # 如果daemon不为None则用用户设定的daemon值(True/False)
            self._daemonic = daemon
        else:
            self._daemonic = current_thread().daemon # 如果为None则取当前线程的daemon值
.....

线程daemon属性,如果设定就是用户的设置,否则就取当前线程的daemon值

  • python中的daemon不是Linux中的守护进程
  • 构造线程的时候,可以设置daemon属性,这个属性必须在start方法前设置好
  • 主线程是non-daemon线程,即daemon == False

相关属性和方法

  • daemon:表示线程是否是daemon线程,这个值必须在start()之前设置,否则引发RuntimeError异常
  • isDaemon():是否是daemon线程
  • setDaemon:设置为daemon线程,必须在start方法之前设置
# 例1: daemon=None默认值
import threading
import time

def work():
    current = threading.current_thread()
    time.sleep(1)
    print(f"{current.name} {current.ident} is working~~~~~")
    print(f'work thread is_daemon:{current.isDaemon()}')


t = threading.Thread(target=work, name='work', daemon=None)  # None为默认值
t.start()
print(f'main thread is_daemon: {threading.current_thread().isDaemon()}')  # threading.current_thread().daemon
print("end~~~~~")
-------输出结果-----------
main thread is_daemon: False
end~~~~~
work 123145584820224 is working~~~~~
work thread is_daemon:False

例2: daemon为False
import threading
import time

def work():
    current = threading.current_thread()
    time.sleep(1)
    print(f"{current.name} {current.ident} is working~~~~~")
    print(f'work thread is_daemon:{current.isDaemon()}')

t = threading.Thread(target=work, name='work', daemon=False)
t.start()
print(f'main thread is_daemon: {threading.current_thread().isDaemon()}')
print("end~~~~~")
------输出结果(同例1)---------------------
main thread is_daemon: False
end~~~~~
work 123145391276032 is working~~~~~
work thread is_daemon:False

例3:daemon为True
import threading
import time

def work():
    current = threading.current_thread()
    print(current)
    time.sleep(1)
    print(f"{current.name} {current.ident} is working~~~~~")
    print(f'work thread is_daemon:{current.isDaemon()}')

t = threading.Thread(target=work, name='work')
print(1, t.daemon)
t.setDaemon(True)
print(2, t.daemon)
t.start()
print(f'main thread is_daemon: {threading.current_thread().isDaemon()}')
time.sleep(0.5)
print("end~~~~~")
----输出结果:主线程空闲后查还在执行的工作线程work为daemon线程,直接结束-------------
1 False
2 True
<Thread(work, started daemon 123145559367680)>
main thread is_daemon: False
end~~~~~

结论:

  • 线程具有一个daemon属性,可以手动设置为True或False,也可以不设置,则取默认值None
  • 如果不设置daemon,就取当前线程的daemon来设置它
  • 主线程是non-daemon线程,即daemon = False
  • 从主线程创建的所有线程的不设置daemon属性,则默认都是daemon = False
  • Python程序在没有活着的non-daemon线程运行时,程序才退出,也就是除主线程之外剩下的只能都是daemon线程,主线程才能退出,否则主线程就只能等待
    【我要退出了,还有non-daemon的吗?有就等会,没有就退出了】
# 执行如下例子,进一步理解daemon的参数的意义
import time
import threading

def worker(name, timeout):
    time.sleep(timeout)
    print('{} working'.format(name))

# 主线程 是non-daemon线程
t1 = threading.Thread(target=worker, args=('t1', 5), daemon=True)  # 调换5和10看看效果
t1.start()
t2 = threading.Thread(target=worker, args=('t2', 10), daemon=False)
t2.start()
print('Main Thread Exits')

join方法

join(timeout=None),是线程的标准方法之一

import time
import threading

def worker(name, timeout):
    time.sleep(timeout)
    print('{} working'.format(name))

t = threading.Thread(target=worker, args=('t', 3), daemon=True)
t.start()
t.join()  # 看看有join和没join的区别

print('Main Thread Exits')

daemon线程执行完了,主线程才退出了

import time
import threading

def worker(name, timeout):
    time.sleep(timeout)
    print('{} working'.format(name))

t = threading.Thread(target=worker, args=('t', 3), daemon=True)
t.start()
t.join(timeout=1)
print('~~~~~~~~~')
t.join(timeout=1)  # 延长timeout再试试

print('Main Thread Exits')
  • 一个线程中调用另一个线程的join方法,调用者将被阻塞,直到被调用线程终止
  • 一个线程可以被join多次
  • timeout参数指定调用者等待多久,没有设置超时,就一直等到被调用线程结束
  • 在哪(如主线程中main)调用谁(工作线程t)的join方法,main就要等t
    【join的线程哥俩好】

daemon线程应用场景

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

def worker2(name):
    while True:
        time.sleep(1)
        print('{} working'.format(name), threading.current_thread().isDaemon())

def worker1(name):
    current = threading.current_thread()
    print("{}'s daemon = {}".format(name, current.isDaemon()))
    t2 = threading.Thread(target=worker2, args=('t2',))
    t2.start()
    # t2.join() # (2)也可设置主线程等t1, t1等t2的来让t2一直运行

t1 = threading.Thread(target=worker1, args=('t1',), daemon=True)  # 使t2一直运行的最简单方式是设置t1.daemon为默认值及None/False
t1.start()
# t1.join() # (1)这里join的话只是主线程等t1,但t1很快就结束了,不能达到t2一直运行

time.sleep(4)
print('Main Thread Exits')
  • 如果果主线程退出,想所有其它工作线程一起退出,就使用daemon=True来创建工作线程
  • 如果在non-daemon线程A中,对另一个daemon线程B使用了join方法,这个线程B设置成daemon就没有什么意义了,因为non-daemon线程A总是要等待B。
  • 如果在一个daemon线程C中,对另一个daemon线程D使用了join方法,只能说明C要等待D,主线程退出,C和D不管是否结束,也不管它们谁等谁,都要被杀掉

参考


  • magedu