多线程

简介: 多线程

1.线程与进程


1.1进程


计算机程序有静态和动态的区别。


静态的计算机程序就是存储在磁盘上的可执行二进制(或其他类型)文件,而动态的计算机程序就是将这些可执行文件加载到内存中并被操作系统调用,这些动态的计算机程序被称为一个进程,也就是说,进程是活跃的,只有可执行程序被调入内存中才称为进程。每个进程都拥有自己的地址空间、内存、数据栈以及其他用于跟踪执行的辅助数据。操作系统会管理系统中所有进程的执行,并为这些进程合理地分配时间。进程可以通过派生(fork或spawn)新的进程来执行其他任务,不过由于每个新进程也都拥有自己的内存和数据栈等,所以只能采用进程间通信(IPC)的方式共享信息。


1.2 线程


线程(有时候也被称为轻量级进程)与进程类似,不过线程是在同一个进程下执行的,并共享同一个上下文。也就是说,线程属于进程,而且线程必须要依赖进程才能执行。一个进程可以包含一个或多个线程。


线程包括开始、执行和结束三部分。它有一个指令指针,用于记录当前运行的上下文,当其他线程运行时,当前线程有可能被抢占(中断)或临时挂起(睡眠)。


一个进程中的各个线程与主线程共享同一片数据空间,因此相对于独立的进程而言,线程间的信易共享和通信更容易。线程一般是以并发方式执行的,正是由于这种并行和数据共享机制,使得多任务间的协作成为可能。当然,在单核CPU的系统中,并不存在真正的并发运行,所以线程的执行实际上还是同步执行的,只是系统会根据调度算法在不同的时间安排某个线程在CPU上执行一小会儿,然后就会让其他的线程在CPU上再执行一会儿,通过这种多个线程之间不断切换的方式让多个线程交替执行。因此,从宏观上看,即使在单核CPU的系统上仍然看着像多个线程并发运行一样。


当然,多线程之间共享数据并不是没有风险。如果两个或多个线程访问了同一片数据,由于数据访问顺序不同,可能导致结果的不一致。这种情况通常称为静态条件(static condition),幸运的是,大多数线程库都有一些机制让共享内存区域的数据同步,也就是说,当一个线程访问这片内存区域时,这片内存区域暂时被锁定,其他的线程只能等待这片内存区域解锁后再访问。


要注意的是,线程的执行时间是不平等的,例如,有6个线程,6s的CPU执行时间,并不是为这6个线程平均分配CPU执行时间(每个线程1s),而是根据线程中具体的执行代码分配CPU计算时间。例如,在调动一些函数时,这些函数会在完成之前保存阻塞状态(阻止其他线程获得CPU执行时间),这样这些函数就会长时间占用CPU资源,通常来讲,系统在分配CPU计算时间时更倾向于这些贪婪的函数。


2.Python与线程


现代的操作系统基本上都支持多线程。Python多线程在底层使用了兼容POSIX的线程,也就是众所周知的pthread。


2.1 使用单线程执行程序



2.2 使用多线程执行程序


Python提供了很多内建模块用于支持多线程,在Python3.x开始,用_thread,也就是在thread前加一个下划线(_)。


使用_thread模块中的start_new_thread函数会直接开启一个线程,该函数的第1个参数需要指定一个函数,可以把这个函数称为线程函数,当线程启动时会自动调用这个函数。


start_new_thread函数的第2个参数是给线程函数传递的参数,必须为元组类型


'''
进程:静态和动态,静态(exe、dll),进程是动态执行的程序
线程:线程依赖于进程,一个进程可以拥有多个线程,一个进程必须至少有一个主线程。
'''
import _thread as thread
from time import sleep,ctime
def fun1():
    print('开始运行fun1:',ctime())
    sleep(4)                       # 休眠4s
    print('fun1运行结束:',ctime())
def fun2():
    print('开始运行fun2:',ctime())
    sleep(2)                      # 休眠2s
    print('fun2运行结束:',ctime())
def main():
    print('开始运行时间:', ctime())
    thread.start_new_thread(fun1,())   # 启动一个线程运行fun1函数
    thread.start_new_thread(fun2,())   # 启动一个线程运行fun2函数
    sleep(6)                     # 休眠6s
    print('结束运行时间:',ctime())
if __name__ == "__main__":
    main()

结果:

开始运行时间: Mon Dec  9 19:28:49 2019
开始运行fun1: Mon Dec  9 19:28:49 2019
开始运行fun2: Mon Dec  9 19:28:49 2019
fun2运行结束: Mon Dec  9 19:28:51 2019
fun1运行结束: Mon Dec  9 19:28:53 2019
结束运行时间: Mon Dec  9 19:28:55 2019


在第1个线程运行fun1函数的过程中,会使用第2个线程运行fun2函数。这是因为在fun1函数中调用了sleep函数休眠了4s,当程序休眠时,会释放CPU的计算资源,这时fun2函数抢占了fun1函数的CPU计算资源。而fun2函数只通过sleep函数休眠了2s,所以当fun2函数执行完,fun1函数还没有休眠完。4s后,fun1函数继续执行,这时已经没有要执行的函数与fun1函数抢CPU计算资源,所以fun1函数会顺利地执行完。在main函数中使用sleep函数休眠6s,等待fun1函数和fun2函数都执行完,再结束程序。


2.3 为线程函数传递参数


start_new_thread函数的第2个参数是给线程函数传递的参数,必须为元组类型。


# 为线程传递参数
import random
from time import sleep
import _thread as thread
def fun(a,b):   # 线程函数,其中a,b是通过start_new_thread函数传入的参数
    print(a,b)
    sleep(random.randint(1,5))  # 随机休眠一段时间(1-4s)
for i in range(8):     # 启动8个线程
    thread.start_new_thread(fun,(i+1,'a' * (i + 1)))   # 为每一个线程函数传入 两个 参数值
input()  # 通过从终端输入一个字符串的方式让程序暂停
"""
最后使用input函数从终端采集一个字符串,其实程序对这个从终端输入的字符串并不关心,只是让程序暂停而已。
如果程序启动线程后不暂停,还没等线程函数运行,程序就结束了,这样线程函数将永远不会执行了。
"""


2.4 线程和锁


使用多线程运行线程函数,


1.在main函数的最后使用sleep函数让程序处于休眠状态,

2.使用input函数从终端采集一个字符串,


目的:让程序暂停,即在所有的线程执行完之前,阻止程序退出。因为程序无法感知是否有线程正在执行,以及是否所有的线程函数都执行完毕。因此,只能采用这些手段让程序暂时不退出。


这里的锁并不是将程序锁住不退出,而是通过锁让程序了解是否还有线程函数没执行完,而且可以做到当所有的线程函数执行完成后,程序会立刻退出,无须任何等待。


锁的使用分为创建锁、获取锁、释放锁。完成这三个功能需要_thread模块中的一个函数和两个方法,


1.allocate_lock函数用于创建锁对象,

2.然后使用锁对象的acquire方法获取锁,

3.如果不需要锁了,可以使用锁对象的release方法释放锁;如果要判断锁是否被释放,可以使用锁对象的locked方法。

# 线程锁
import _thread as thread
from time import sleep,ctime
# 线程函数,index是一个整数类型的索引,sec是休眠时间(单位:秒),lock是锁对象
def fun(index, sec,lock):
    print('开始执行',index,'执行时间:',ctime)
    sleep(sec)   #休眠
    print('执行结束',index,'执行时间:',ctime)
    lock.release()   # 释放锁对象
lock1 = thread.allocate_lock()   # 创建第1个锁对象
lock1.acquire()  #  获取锁,(相当于把锁锁上)
# 启动第1个线程,并传入第一个锁对象,10是索引,4是休眠时间,lock1是锁对象
thread.start_new_thread(fun,(10,4,lock1))
lock2 = thread.allocate_lock()  # 创建第2个锁对象
lock2.acquire()                 #  获取锁,(相当于把锁锁上)
# 启动第1个线程,并传入第一个锁对象,20是索引,2是休眠时间,lock2是锁对象
thread.start_new_thread(fun,(20,2,lock2))
# 使用while循环和locked方法判断lock1和lock2是否被释放
# 只要有一个没有释放,while循环就不会退出。
while lock1.locked() or lock2.locked():
    pass

20191209215525409.png


3.高级线程模块-threading


在threading模块中有一个非常重要的Thread类,该类的实例表示一个执行线程的对象。

_thread模块可以看作线程的面向过程版本。

Thread类可以看作线程的面向对象版本。


3.1 Thread类与线程函数(重点)


可以直接使用Thread对象的join方法等待线程函数执行完毕再往下执行,也就是说,在主线程(main函数)中调用Thread对象的join方法,并且Thread对象的线程函数没有执行完毕,主线程会处于阻塞状态。


使用方法:


1.创建Thread类的实例,通过Thread类构造方法的target关键字参数执行线程函数,通过args关键字参数指定传给线程函数的参数。


2.调用Thread对象的start方法启动线程。


# Thread类与线程函数
import threading
from time import sleep,ctime
# 线程函数,index表示整数类型的索引,sec表示休眠时间(单位:秒)
def fun(index,sec):
    print('开始执行',index,'时间:',ctime())
    sleep(sec)            # 休眠
    print('结束执行',index,'时间:',ctime())
def main():
    # 创建第1个Thread对象,通过target关键字参数执行线程函数fun,args关键字参数指定传给线程函数的参数(传入索引10和休眠时间4s)
    thread1 = threading.Thread(target = fun,args = (10,4))
    thread1.start()   # 启动第1个线程
    thread2 = threading.Thread(target=fun,args=(20,2))
    thread2.start()   # 启动第2个线程
    thread1.join()   # 等待第1个线程函数执行完毕
    thread2.join()   # 等待第2个线程函数执行完毕
    print('程序退出')
if __name__ == "__main__":
    main()

结果:

开始执行 10 时间: Tue Dec 10 10:38:45 2019
开始执行 20 时间: Tue Dec 10 10:38:45 2019
结束执行 20 时间: Tue Dec 10 10:38:47 2019
结束执行 10 时间: Tue Dec 10 10:38:49 2019
程序退出
# 通过Thread对象启动的线程只需要使用join方法就可以保证让所有的线程函数都执行完再往下执行。

3.2 Thread类与线程对象


Thread类构造方法的target关键字参数不仅可以是一个函数,还可以是一个对象,可以称这个对象为线程对象。其实线程调用的仍然是函数,只是这个函数用对象进行了封装。这么做的好处:可以将与线程函数相关的代码都放在对象对应的类中,这样更能体现面向对象的封装性。


线程对象对应的类需要有一个可以传入线程函数和参数的构造方法,而且在类中还必须有一个名为“ __ call __ ”的方法。当线程启动时,会自动调用线程对象的“__ call __”方法,然后在该方法中会调用线程函数。


# 从Thread类继承
import threading
from time import sleep,ctime
# 线程对象对应的类
class MyThread(object):
    # fun表示线程函数,args表示线程函数的参数
    def __init__(self,fun,args):
        # 将线程函数与线程函数的参数 赋给当前 类的成员变量
        self.fun = fun
        self.args = args
    # 线程启动时,会调用该方法
    def __call__(self):
        # 调用线程函数,并将元组类型的参数值分解为单个的参数值传入线程函数
        self.fun(*self.args)
# 线程函数,index表示整数类型的索引,sec表示休眠时间(单位:秒)
def fun(index,sec):
    print('开始执行',index,'时间:',ctime())
    sleep(sec)            # 休眠
    print('结束执行',index,'时间:',ctime())
def main():
    # 创建第1个线程,通过通过target关键字参数执行线程对象(MyThread),延迟4s。
    thread1 = threading.Thread(target = MyThread(fun,(10,4)))
    thread1.start()   # 启动第1个线程
    thread2 = threading.Thread(target=MyThread(fun, (20, 2)))
    thread2.start()   # 启动第2个线程
    thread1.join()   # 等待第1个线程函数执行完毕
    thread2.join()   # 等待第2个线程函数执行完毕
    print('程序退出')
if __name__ == "__main__":
    main()

结果:

开始执行 10 时间: Tue Dec 10 11:29:06 2019
开始执行 20 时间: Tue Dec 10 11:29:06 2019
结束执行 20 时间: Tue Dec 10 11:29:08 2019
结束执行 10 时间: Tue Dec 10 11:29:10 2019
程序退出

3.3 从Thread类继承(建议使用)


为了更好地对与线程有关的代码进行封装,可以从Thread类派生一个子类,然后将与线程有关的代码都放到这个类中。Thread类的子类的使用方法与Thread相同。

从Thread类继承最简单的方式是在子类的构造方法中通过super函数调用父类的构造方法,并传入相应的参数值。


# 从Thread类继承
import threading
from time import sleep,ctime
# 从Thread类派生的子类
class MyThread(threading.Thread):
    # 重写父类的构造方法,其中fun表示线程函数,args表示传入线程函数的参数,name是线程名
    def __init__(self,fun,args,name=''):
        # 调用父类的构造方法,并传入相应的参数值
        super().__init__(target = fun,name=name,args=args)
    # 重写父类的run方法(回调方法或回调函数)
    def run(self):
        self._target(*self._args)   # _target指向fun,从类的外部传进来
# 线程函数
def fun(index,sec):
    print('开始执行',index,'时间:',ctime())
    # 休眠sec秒
    sleep(sec)
    print('执行完毕',index,'时间:',ctime())
def main():
    thread1 = MyThread(fun,(10,4),'线程1')  # 创建第1个线程,并指定线程名为“线程1”
    thread2 = MyThread(fun,(20,2),'线程2')  # 创建第2个线程,并指定线程名为“线程2”
    thread1.start()            # 开启第1个线程
    thread2.start()            # 开启第2个线程
    print(thread1.name)        # 输出第1个线程的名字
    print(thread2.name)
    thread1.join()             # 等待第1个线程结束
    thread2.join()
    print('结束:',ctime())
if __name__ == "__main__":
    main()

结果:

20191210154218254.png


在调用Thread类的构造方法时,需要将线程函数、参数等值传入构造方法,其中name表示线程的名字,如果不指定这个参数,默认的线程名字格式为Thread-1、Thread-2。

每一个传入构造方法的参数值,在Thread类中都有对应的成员变量保存这些值,这些成员变量都以下划线(__)开头,如:_ treget、_args等。


在run方法中,需要使用这些变量调用传入的线程函数,并为线程函数传递参数。


4.线程同步


多线程的目的:让多段程序并发运行。但在一些情况下,让多段程序同时运行会造成很多麻烦,如果这些并发运行的程序还共享数据,则有可能造成脏数据以及其他数据不一致的后果。


脏数据:在多段程序同时读写一个或一组变量时,由于读写顺序的问题导致的与期望值不一样的后果。


4.1 线程锁


线程锁的目的:将一段代码锁住,一旦获得了锁权限,除非释放线程锁,否则其他任何代码都无法再次获得锁权限。


为了使用线程锁:


1.创建Lock类的实例;

2.通过Lock对象的acquire方法获取锁权限,当需要完成原子操作的代码段执行完成后,

再使用Lock对象的release方法释放锁,其它代码就可以再次获得这个锁权限。


注意:锁对象要放到线程函数的外面作为一个全局变量,这样所有的线程函数实例都可以共享这个变量;如果将锁对象放到线程函数内部,那么这个锁对象就变成了局部变量,多个线程函数实例使用的是不同的锁对象,所以仍然不能有效保护原子操作的代码。


import random
from atexit import register
from threading import Thread,Lock,currentThread
from time import sleep,ctime
# 创建线程锁对象
lock = Lock()
def fun():
    # 获取线程锁权限
    lock.acquire()
    # 将for循环变成原子操作
    for i in range(5):
        print('Thread Name','=',currentThread().name,'i','=',i)
        sleep(random.randint(1,5))  # 休眠一段时间(1-4s)
    # 释放线程锁,其它线程函数可以获得这个线程锁的权限了
    lock.release()
def main():
    # 通过循环创建并启动了3个线程
    for i in range(3):
        Thread(target=fun).start()
# 当程序结束时,会调用这个函数
@register
def exit():
    print('线程执行完毕',ctime())
if __name__ == "__main__":
    main()

结果:

Thread Name = Thread-1 i = 0
Thread Name = Thread-1 i = 1
Thread Name = Thread-1 i = 2
Thread Name = Thread-1 i = 3
Thread Name = Thread-1 i = 4
Thread Name = Thread-2 i = 0
Thread Name = Thread-2 i = 1
Thread Name = Thread-2 i = 2
Thread Name = Thread-2 i = 3
Thread Name = Thread-2 i = 4
Thread Name = Thread-3 i = 0
Thread Name = Thread-3 i = 1
Thread Name = Thread-3 i = 2
Thread Name = Thread-3 i = 3
Thread Name = Thread-3 i = 4
线程执行完毕 Tue Dec 10 22:35:42 2019
# 为fun函数加上线程锁,那么只有当某个线程的线程函数执行完,才会运行另一个线程的线程函数。

4.2 信号量(Semaphore)


信号量是最古老的同步原语之一,它是一个计数器,用于记录资源的消耗情况。当资源消耗时递减,当资源释放时递增。可以认为信号量代表资源是否可用。消耗资源使计数器递减的操作,习惯上称为 P,当一个线程对一个资源完成操作时,该资源需要返回资源池中,这个操作一般称为 V。Python语言统一了所有的命名,使用与线程锁同样的方法名消耗和释放资源。acquire方法用于消耗资源,调用该方法计数器会减1;release方法释放资源,调用该方法计数器会加1。


1.使用信号量首先要创建BoundedSemaphore类的实例,并且通过该类的构造方法传入计数器的最大值;


2.然后就可以使用BoundedSemaphore对象的acquire方法获取资源(计数器减1)和release方法释放资源(计数器加1)。


信号量用来控制线程并发数的,Semaphore管理一个内置的计数 器,每当调用acquire()时-1,调用release()时+1。计数器不能小于0,当计数器为 0时,acquire()将阻塞线程至同步锁定状态,直到其他线程调用release()。其实就是控制最多几个线程可以操作同享资源。


# 线程同步-信号量
'''
信号量是一个计数器  消耗资源:P,释放资源:V
acquire   release
BoundedSemaphore
'''
from threading import BoundedSemaphore
MAX = 2
# 创建信号量对象,并设置了计数器的最大值(也是资源的最大值),计数器不能超过这个数值
semaphore = BoundedSemaphore(MAX)
print(semaphore._value)    # 输出当前计数器的值,输出结果为 2
# 获取资源,计数器减1
semaphore.acquire()
print(semaphore._value)    # 输出结果为 1
# 获取资源,计数器减1
semaphore.acquire()
print(semaphore._value)    # 输出结果为 0
# 当计数器为0时,不能再获取资源,所以acquire方法会返回False,输出结果为 False
print(semaphore.acquire(False))
# 释放资源,计数器加1
semaphore.release()
print(semaphore._value)   # 输出结果为 1
# 释放资源,计数器加1
semaphore.release()
print(semaphore._value)   # 输出结果为 2
# 抛出异常,当计数器达到最大值时,不能再次释放资源,否则会抛出异常
semaphore.release()

结果:

File "D:\python\python3.7.2\lib\threading.py", line 483, in release
    raise ValueError("Semaphore released too many times")
ValueError: Semaphore released too many times
2
1
0
False
1
2
import threading
import time
semaphore = threading.Semaphore(5)
def func():
    if semaphore.acquire():
        print(threading.currentThread().getName() + '获取共享资源')
        time.sleep(2)
        semaphore.release()
for i in range(10):    # 创建10个线程,让每次只让5个线程去执行func函数。
    t1 = threading.Thread(target=func)
    t1.start()

结果:5个线程一批一批的执行打印,中间停格2s

Thread-1获取共享资源
Thread-2获取共享资源
Thread-3获取共享资源
Thread-4获取共享资源
Thread-5获取共享资源
Thread-6获取共享资源
Thread-8获取共享资源
Thread-7获取共享资源
Thread-9获取共享资源
Thread-10获取共享资源
目录
相关文章
|
11月前
|
消息中间件 前端开发
C++11多线程相关
C++11多线程相关
74 0
|
5月前
|
Java API 调度
多线程 02
多线程 02
27 0
|
5月前
|
前端开发 Java
|
12月前
|
Java
多线程
java多线程是指在程序中并发执行多个指令流,每个指令流都称为一个线程,彼此间互相独立。Java中所有变量都储存在主存中,对于所有线程都是共享的。Java中的多线程可以通过继承Thread类或者实现Runnable接口来创建,然后通过start()方法来启动线程。Java中的多线程可以提高程序的并发性和执行效率。
|
监控 Java API
多线程专题
多线程专题
|
Linux 调度
多线程具体实现(上)
多线程具体实现
73 0
|
调度
多线程之争用条件
多线程之争用条件
148 2
多线程之争用条件