Python threading实现多线程 基础篇

讲多线程前,先要了解什么是进程,什么是线程,已经知道的请略过。

一、进程与线程:

进程是资源分配的最小单位,一个程序至少有一个进程。

线程是程序执行的最小单位,一个进程至少有一个线程。

进程都有自己独立的地址空间,内存,数据栈等,所以进程占用资源多。由于进程的资源独立,所以通讯不方便,只能使用进程间通讯(IPC)。

线程共享进程中的数据,他们使用相同的地址空间,使用线程创建快捷,创建开销比进程小。同一进程下的线程共享全局变量、静态变量等数据,所以线程通讯非常方便,但会存在数据同步与互斥的问题,如何处理好同步与互斥是编写多线程程序的难点。

一个进程中可以存在多个线程,在单核CPU中每个进程中同时刻只能运行一个线程,只有在多核CPU中才能存在线程并发的情况。

当线程需要运行但没有运行空间时,会对线程的优先级进行判断,高优先级先运行,低优先级进程让行。

二、全局解释器锁(GIL)

Python的多线程,只有用于I/O密集型程序时效率才会有明显的提高。

原因如下:

Python代码的执行是由Python虚拟机进行控制。它在主循环中同时只能有一个控制线程在执行,意思就是Python解释器中可以运行多个线程,但是在执行的只有一个线程,其他的处于等待状态。

这些线程执行是有全局解释器锁(GIL)控制,它来保证同时只有一个线程在运行。在多线程运行环境中,Python虚拟机执行方式如下:

  1. 设置GIL
  2. 切换进线程
  3. 执行下面操作之一
    1. 运行指定数量的字节码指令
    2. 线程主动让出控制权
  4. 切换出线程(线程处于睡眠状态)
  5. 解锁GIL
  6. 进入1步骤
注意:Python运行计算密集型的多线程程序时,更倾向于让线程在整个时间片内始终占据GIL,而I/O秘籍型的多线程程序在I/O被调用前会释放GIL,以允许其他线程在I/O执行的时候运行。

三、Python 的 threading 模块

Python 常用的多线程模块有threading 和 Queue,在这里我们将 threading 模块。

threading 模块的Thread 类是主要的执行对象。使用Thread 类,可以有很多方法来创建线程。最常用的有下面三种:

创建Thread 的实例,传给它一个可调用对象(函数或者类的实例方法)。
派生Thread 的子类,并创建子类的实例。

3.1 可调用对象(函数,类的实例方法)使用多线程

步骤如下:

示例:创建Thread实例,传递给他一个函数

from threading import Thread
from time import sleep, ctime

def func(name, sec):
    print('---开始---', name, '时间', ctime())
    sleep(sec)
    print('***结束***', name, '时间', ctime())

# 创建 Thread 实例
t1 = Thread(target=func, args=('第一个线程', 1))
t2 = Thread(target=func, args=('第二个线程', 2))

# 启动线程运行
t1.start()
t2.start()

# 等待所有线程执行完毕
t1.join()  # join() 等待线程终止,要不然一直挂起
t2.join()

示例:创建Thread实例,传递给他一个类的实例方法

from threading import Thread
from time import sleep, ctime


class MyClass(object):

    def func(self,name,sec):
        print('---开始---', name, '时间', ctime())
        sleep(sec)
        print('***结束***', name, '时间', ctime())

def main():
    # 创建 Thread 实例
    t1 = Thread(target=MyClass().func, args=(1, 1))
    t2 = Thread(target=MyClass().func, args=(2, 2))

    # 启动线程运行
    t1.start()
    t2.start()

    # 等待所有线程执行完毕
    t1.join()  # join() 等待线程终止,要不然一直挂起
    t2.join()

if __name__=="__main__":
    main()

运行结果:

---开始--- 一 时间 Fri Nov 29 11:34:31 2019
---开始--- 二 时间 Fri Nov 29 11:34:31 2019
***结束*** 一 时间 Fri Nov 29 11:34:32 2019
***结束*** 二 时间 Fri Nov 29 11:34:33 2019

程序总共运行两秒,如果程序按照线性运行需要3秒,节约1秒钟。

Thread 实例化时需要接收 target,args(kwargs)两个参数。

target 用于接收需要使用多线程调用的对象。

args 或 kwargs 用于接收调用对象的需要用到的参数,args接收tuple,kwargs接收dict。对args与kwargs你理解的看 木头人:Python入门 函数 提高篇 中的Python函数的可变参数(*args, **kwargs) 部分。

start() 是方法用来启动线程的执行。

join() 方法是一种自旋锁,它用来等待线程终止。也可以提供超时的时间,当线程运行达到超时时间后结束线程,如join(500),500毫秒后结束线程运行。

注意:如果当你的主线程还有其他事情要做,而不是等待这些线程完成,就可以不调用join()。join()方法只有在你需要等待线程完成然后在做其他事情的时候才是有用的。

3.2 派生Thread 的子类,并创建子类的实例。

我们可以通过继承Thread类,派生出一个子类,使用子类来创建多线程。

示例:派生Thread 的子类,传递给他一个可调用对象

from threading import Thread
from time import sleep, ctime


# 创建 Thread 的子类
class MyThread(Thread):
    def __init__(self, func, args):
        '''
        :param func: 可调用的对象
        :param args: 可调用对象的参数
        '''
        Thread.__init__(self)   # 不要忘记调用Thread的初始化方法
        self.func = func
        self.args = args

    def run(self):
        self.func(*self.args)


def func(name, sec):
    print('---开始---', name, '时间', ctime())
    sleep(sec)
    print('***结束***', name, '时间', ctime())

def main():
    # 创建 Thread 实例
    t1 = MyThread(func, (1, 1))
    t2 = MyThread(func, (2, 2))
    # 启动线程运行
    t1.start()
    t2.start()
    # 等待所有线程执行完毕
    t1.join()
    t2.join()

if __name__ == '__main__':
    main()

注意:不要忘记在子类中初始化父类的方法Thread.__init__(self) 。需要重构 run() 方法来执行多线程的程序。

3.3 拓展:获取可调用对象的返回值

在多线程中运行的程序时与主线程分开,我们没法直接获取线程中程序的返回值。这时就可以使用派生Thread 的子类,将给过保存的实例属性中,通过一个新方法获取运行结果。

示例: 获取多线程中程序运行的结果

from threading import Thread
from time import sleep, ctime

# 创建 Thread 的子类
class MyThread(Thread):
    def __init__(self, func, args):
        '''
        :param func: 可调用的对象
        :param args: 可调用对象的参数
        '''
        Thread.__init__(self)
        self.func = func
        self.args = args
        self.result = None

    def run(self):
        self.result = self.func(*self.args)

    def getResult(self):
        return self.result


def func(name, sec):
    print('---开始---', name, '时间', ctime())
    sleep(sec)
    print('***结束***', name, '时间', ctime())
    return sec


def main():
    # 创建 Thread 实例
    t1 = MyThread(func, (1, 1))
    t2 = MyThread(func, (2, 2))
    # 启动线程运行
    t1.start()
    t2.start()
    # 等待所有线程执行完毕
    t1.join()
    t2.join()
    # 或线程中程序的运行结果
    print(t1.getResult())
    print(t2.getResult())


if __name__ == '__main__':
    main()

线程同步

木头人:Python threading实现多线程 提高篇 线程同步,以及各种锁

补充1:threading 模块的类与函数

1. threading 模块的类对象

Thread 执行线程
Timer 在运行前等待一段时间的执行线程
Lock 原语锁(互斥锁,简单锁)
RLock 重入锁,使单一线程可以(再次)获得已持有的锁
Condition 条件变量,线程需要等待另一个线程满足特定条件
Event 事件变量,N个线程等待某个事件发生后激活所有线程
Semaphore 线程间共享资源的寄存器
BoundedSemaphore 与Semaphore 相似,它不允许超过初始值
Barrie 执行线程达到一定数量后才可以继续

2. threading 模块的函数

activeCount() 获取当前活动中的Thread对象个数
currentThread() 获取当前的Thread对象
enumerate() 获取当前活动的Thread对象列表
settrace(func) 为所有线程设置一个跟踪(trace)函数
setprofile(func) 为所有线程设置配置文件(profile)函数
stack_size(size=None) 获取新创建线程的栈大小,也可设置线程栈的大小为size。


补充2 :Thread 类的属性与方法

threading 中的 Thread 类是主要的执行对象,主要方法和属性如下:

属性

name 线程名称
ident 线程标识符号
daemon 是否为守护线程

方法

___init__(self, group=None, target=None, name=None, args=(), kwargs=None, *, daemon=None)
参数:group 无用,保留参数
target 可调用的目标
name 线程的名称
args,kwargs 调用目标的参数
daemon 是否为守护线程
start() 开始执行
join(timeout=None) 阻塞timeout秒,否则直到启动的线程终止前一直挂起
is_alive () 线程是否存活
isDaemon() 是否为守护线程
setDaemon(daemonic) 设置为守护线程

线程同步

木头人:Python threading实现多线程 提高篇 线程同步,以及各种锁

编辑于 2019-12-16 17:01