Python基础之线程(Thread)

进程

之前我们已经了解了操作系统中进程的概念,程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称之为进程。程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念。在多道编程中,我们允许多个程序同时加载到内存中,在操作系统的调度下,可以实现并发地执行。这是这样的设计,大大提高了CPU的利用率。进程的出现让每个用户感觉到自己独享CPU,因此,进程就是为了在CPU上实现多道编程而提出的。

为什么要有线程

进程有很多优点,它提供了多道编程,让我们感觉我们每个人都拥有自己的CPU和其他资源,可以提高计算机的利用率。很多人就不理解了,既然进程这么优秀,为什么还要线程呢?其实,仔细观察就会发现进程还是有很多缺陷的,主要体现在两点上:

  • 进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。
  • 进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行。

  如果这两个缺点理解比较困难的话,举个现实的例子也许你就清楚了:如果把我们上课的过程看成一个进程的话,那么我们要做的是耳朵听老师讲课,手上还要记笔记,脑子还要思考问题,这样才能高效的完成听课的任务。而如果只提供进程这个机制的话,上面这三件事将不能同时执行,同一时间只能做一件事,听的时候就不能记笔记,也不能用脑子思考,这是其一;如果老师在黑板上写演算过程,我们开始记笔记,而老师突然有一步推不下去了,阻塞住了,他在那边思考着,而我们呢,也不能干其他事,即使你想趁此时思考一下刚才没听懂的一个问题都不行,这是其二。

  现在你应该明白了进程的缺陷了,而解决的办法很简单,我们完全可以让听、写、思三个独立的过程,并行起来,这样很明显可以提高听课的效率。而实际的操作系统中,也同样引入了这种类似的机制——线程。

线程

进程由若干线程组成,一个进程至少一个线程。因此,线程于进程及其相似。

线程,有时被称为轻量进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。

一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。就绪状态是指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;运行状态是指线程占有处理机正在运行;阻塞状态是指线程在等待一个事件(如某个信号量),逻辑上不可执行。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。

线程和进程区别

  • 地址空间和其它资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
  • 通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。
  • 调度和切换:线程上下文切换比进程上下文切换要快得多。
  • 在多线程操作系统中,进程不是一个可执行的实体。

您可以通过漫画了解:进程与线程

线程特点

在多线程的操作系统中,通常是在一个进程中包括多个线程,每个线程都是作为利用CPU的基本单位,是花费最小开销的实体。线程具有以下属性。

1. 轻型实体

  • 线程中的实体基本上不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源。
  •  线程的实体包括程序、数据和TCB。线程是动态概念,它的动态特性由线程控制块TCB(Thread Control Block)描述。

2. 独立调度和分派的基本单位。

  • 在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。由于线程很“轻”,故线程的切换非常迅速且开销小(在同一进程中的)。

3. 共享进程资源。

  • 线程在同一进程中的各个线程,都可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的进程id,这意味着,线程可以访问该进程的每一个内存资源;此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。由于同一个进程内的线程共享内存和文件,所以线程之间互相通信不必调用内核。

4. 可并发执行。

  • 在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行;同样,不同进程中的线程也能并发执行,充分利用和发挥了处理机与外围设备并行工作的能力。

TCB

线程控制块(Thread Control Block,TCB)是与进程的控制块(PCB)相似的子控制块,只是TCB中所保存的线程状态比PCB中保存少而已(当然可以说这个定义是浅显或错误的)。

TCB包括以下信息:

  • 线程状态。
  • 当线程不运行时,被保存的现场资源。
  • 一组执行堆栈。
  • 存放每个线程的局部变量主存区。
  • 访问同一个进程中的主存和其它资源。

用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。

使用线程

开启一个字处理软件进程,该进程肯定需要办不止一件事情,比如监听键盘输入,处理文字,定时自动将文字保存到硬盘,这三个任务操作的都是同一块数据,因而不能用多进程。只能在一个进程里并发地开启三个线程,如果是单线程,那就只能是,键盘输入时,不能处理文字和自动保存,自动保存时又不能输入和处理文字。

线程内存表示

多个线程共享同一个进程的地址空间中的资源,是对一台计算机上多个进程的模拟,有时也称线程为轻量级的进程。

而对一台计算机上多个进程,则共享物理内存、磁盘、打印机等其他物理资源。多线程的运行也多进程的运行类似,是cpu在多个线程之间的快速切换。

不同的进程之间是充满敌意的,彼此是抢占、竞争cpu的关系,如果迅雷会和QQ抢资源。而同一个进程是由一个程序员的程序创建,所以同一进程内的线程是合作关系,一个线程可以访问另外一个线程的内存地址,大家都是共享的,一个线程干死了另外一个线程的内存,那纯属程序员脑子有问题。

类似于进程,每个线程也有自己的堆栈,不同于进程,线程库无法利用时钟中断强制线程让出CPU,可以调用thread_yield运行线程自动放弃cpu,让另外一个线程运行。

线程通常是有益的,但是带来了不小程序设计难度,线程的问题是:

  1. 父进程有多个线程,那么开启的子线程是否需要同样多的线程
  2. 在同一个进程中,如果一个线程关闭了文件,而另外一个线程正准备往该文件内写内容时会出现矛盾。

因此,在多线程的代码中,需要更多的心思来设计程序的逻辑、保护程序的数据。

用户级线程和内核级线程

用户级线程:

在用户空间模拟操作系统对进程的调度,来调用一个进程中的线程,每个进程中都会有一个运行时系统,用来调度线程。此时当该进程获取cpu时,进程内再调度出一个线程去执行,同一时刻只有一个线程执行。

内核级线程:

切换由内核控制,当线程进行切换的时候,由用户态转化为内核态。切换完毕要从内核态返回用户态;可以很好的利用smp,即利用多核cpu。windows线程就是这样的。

用户级和内核级线程区别:

  • 内核支持线程是OS内核可感知的,而用户级线程是OS内核不可感知的。
  • 用户级线程的创建、撤消和调度不需要OS内核的支持,是在语言(如Java)这一级处理的;而内核支持线程的创建、撤消和调度都需OS内核提供支持,而且与进程的创建、撤消和调度大体是相同的。
  • 用户级线程执行系统调用指令时将导致其所属进程被中断,而内核支持线程执行系统调用指令时,只导致该线程被中断。
  • 在只有用户级线程的系统内,CPU调度还是以进程为单位,处于运行状态的进程中的多个线程,由用户程序控制线程的轮换运行;在有内核支持线程的系统内,CPU调度则以线程为单位,由OS的线程调度程序负责线程的调度。
  • 用户级线程的程序实体是运行在用户态下的程序,而内核支持线程的程序实体则是可以运行在任何状态下的程序。

内核级线程优缺点

  • 优点:当有多个处理机时,一个进程的多个线程可以同时执行。
  • 缺点:由内核进行调度。

用户级线程优缺点

优点:

  • 线程的调度不需要内核直接参与,控制简单。
  • 可以在不支持线程的操作系统中实现。
  • 创建和销毁线程、线程切换代价等线程管理的代价比内核线程少得多。
  • 允许每个进程定制自己的调度算法,线程管理比较灵活。
  • 线程能够利用的表空间和堆栈空间比内核级线程多。
  • 同一进程中只能同时有一个线程在运行,如果有一个线程使用了系统调用而阻塞,那么整个进程都会被挂起。另外,页面失效也会产生同样的问题。

缺点:

  • 资源调度按照进程进行,多个处理机下,同一个进程中的线程只能在同一个处理机下分时复用。

用户级和内核级线程可混合实现。

全局解释锁GIL

Python代码的执行由Python虚拟机(也叫解释器主循环)来控制。Python在设计之初就考虑到要在主循环中,同时只有一个线程在执行。虽然 Python 解释器中可以“运行”多个线程,但在任意时刻只有一个线程在解释器中运行。

对Python虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。

在多线程环境中,Python 虚拟机按以下方式执行:

  1. 设置 GIL;
  2. 切换到一个线程去运行;
  3. 运行指定数量的字节码指令或者线程主动让出控制(可以调用 time.sleep(0));
  4. 把线程设置为睡眠状态;
  5. 解锁 GIL;
  6. 再次重复以上所有步骤。

在调用外部代码(如 C/C++扩展函数)的时候,GIL将会被锁定,直到这个函数结束为止(由于在这期间没有Python的字节码被运行,所以不会做线程切换)编写扩展的程序员可以主动解锁GIL。

Python中线程模块的选择

Python提供了几个用于多线程编程的模块,包括thread、threading和Queue等。thread和threading模块允许程序员创建和管理线程。thread模块提供了基本的线程和锁的支持,threading提供了更高级别、功能更强的线程管理的功能。Queue模块允许用户创建一个可以用于多个线程之间共享数据的队列数据结构。

避免使用thread模块,因为更高级别的threading模块更为先进,对线程的支持更为完善,而且使用thread模块里的属性有可能会与threading出现冲突;其次低级别的thread模块的同步原语很少(实际上只有一个),而threading模块则有很多;再者,thread模块中当主线程结束时,所有的线程都会被强制结束掉,没有警告也不会有正常的清除工作,至少threading模块能确保重要的子线程退出后进程才退出。 

thread模块不支持守护线程,当主线程退出时,所有的子线程不论它们是否还在工作,都会被强行退出。而threading模块支持守护线程,守护线程一般是一个等待客户请求的服务器,如果没有客户提出请求它就在那等着,如果设定一个线程为守护线程,就表示这个线程是不重要的,在进程退出的时候,不用等待这个线程退出。

threading模块

multiprocess模块的完全模仿了threading模块的接口,二者在使用层面,有很大的相似性。

线程创建

from threading import Thread
import time


def func(name):
    time.sleep(2)
    print("My name is", name)


if __name__ == '__main__':
    t = Thread(target=func, args=('Mike',))
    t.start()
    print("Main Thread")

结果:

Main Thread
My name is Mike

Process finished with exit code 0

当然,与进程一样,你也可以这样创建线程:

from threading import Thread
import time


class MyThread(Thread):
    def __init__(self, name):
        super().__init__()
        self.name = name

    def run(self):
        time.sleep(2)
        print("My name is", self.name)


if __name__ == '__main__':
    t = MyThread("Mike")
    t.start()
    print("Main Thread")

结果:

Main Thread
My name is Mike

Process finished with exit code 0

多线程与多进程

pid对比:

from threading import Thread
from multiprocessing import Process
import os
import time


def work():
    print("子:", os.getpid())


if __name__ == '__main__':
    print("------------线程-------------")
    t_one = Thread(target=work)
    t_two = Thread(target=work)
    t_one.start()
    t_two.start()
    time.sleep(1)
    print("主:", os.getpid())

    print("------------进程-------------")
    p_one = Process(target=work)
    p_two = Process(target=work)
    p_one.start()
    p_two.start()
    time.sleep(1)
    print("主:", os.getpid())

结果:

------------线程-------------
子: 26600
子: 26600
主: 26600
------------进程-------------
子: 26628
子: 23172
主: 26600

Process finished with exit code 0

结论:同一作用效果线程由一个进程产生,共用资源;进程由另一个进程产生,不共享资源。

效率对比:

from threading import Thread
from multiprocessing import Process
import os
import time


def func(l):
    res_list = []
    for i in l:
        res = i ** i
        res_list.append(res)
    print(res_list)


if __name__ == '__main__':
    lst = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    t_one = time.time()
    t = Thread(target=func, args=(lst,))
    t.start()
    t_two = time.time()
    print("线程结束,花费时间为", t_two - t_one)

    p = Process(target=func, args=(lst,))
    p.start()
    t_thr = time.time()
    print("进程结束,花费时间为", t_thr - t_two)

结果:

[1, 4, 27, 256, 3125, 46656, 823543, 16777216, 387420489, 10000000000]
线程结束,花费时间为 0.0009953975677490234
进程结束,花费时间为 0.010952949523925781
[1, 4, 27, 256, 3125, 46656, 823543, 16777216, 387420489, 10000000000]

Process finished with exit code 0

内存数据共享

from threading import Thread
from multiprocessing import Process
import os
import time


def func():
    global n
    n = 0
    print(n)


if __name__ == '__main__':
    n = 100
    p = Process(target=func)
    p.start()
    p.join()
    print("主进程:", n)  # 100
    print("-------LINE-------")
    t = Thread(target=func)
    t.start()
    t.join()
    print("主线程:", n)

结果:

0
主进程: 100
-------LINE-------
0
主线程: 0

Process finished with exit code 0

练习:多线程实现socket

服务端:

import threading
import socket

HOST = '127.0.0.1'
PORT = 8080
ADDRESS = (HOST, PORT)
BUFF_SIZE = 1024

ss = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
ss.bind(ADDRESS)
ss.listen(5)


def action(conn):
    while 1:
        data = conn.recv(BUFF_SIZE)
        print(data)
        conn.send(data.upper())


if __name__ == '__main__':
    while 1:
        conn, addr = ss.accept()
        p = threading.Thread(target=action, args=(conn,))
        p.start()

客户端:

import socket

sc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sc.connect(('127.0.0.1', 8080))

while True:
    msg = input('>>: ').strip()
    if not msg:
        continue
    sc.send(msg.encode('utf-8'))
    data = sc.recv(1024)
    print(data)

结果:(略)

线程其它方法

Python线程常用方法
方法说明
Thread.isAlive()返回线程是否活动。
Thread.getName()返回线程名字。
Thread.setName()设置线程名字。
threading.currentThread()返回当前的线程变量。
threading.enumerate()返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
threading.activeCount()返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。

实例:

import threading
import time


def func():
    time.sleep(3)
    print(threading.current_thread().getName())


if __name__ == '__main__':
    t = threading.Thread(target=func)
    t.start()

    print(threading.current_thread().getName())
    print(threading.current_thread())  # 主线程
    print(threading.enumerate())  # 连同主线程在内有两个运行的线程
    print(threading.active_count())  # 正在运行的线程数量
    print("主线程")

结果:

MainThread
<_MainThread(MainThread, started 19096)>
[<_MainThread(MainThread, started 19096)>, <Thread(Thread-1, started 12764)>]
2
主线程
Thread-1

Process finished with exit code 0

join()方法实例:

from threading import Thread
import time


def func(name):
    time.sleep(2)
    print("My name is", name)


if __name__ == '__main__':
    t = Thread(target=func, args=("Mike",))
    t.start()
    t.join()
    print("主线程")  # 等待子线程执行完毕
    print(t.is_alive())

结果:

My name is Mike
主线程
False

Process finished with exit code 0

守护线程

无论是进程还是线程,都遵循:守护线程(进程)会等待主线程(进程)运行完毕后被销毁。需要强调的是:运行完毕并非终止运行

主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束。

主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束。

实例1:

from threading import Thread
import time


def func(name):
    time.sleep(2)
    print("My name is", name)


if __name__ == '__main__':
    t = Thread(target=func, args=('Mike',))
    t.setDaemon(True)  # 必须在start之前设置
    t.start()
    '''守护进程没有打印'''
    print("主线程")
    print(t.is_alive())

结果:

主线程
True

Process finished with exit code 0

实例2:

from threading import Thread
import time


def func_one():
    print("func_one")
    time.sleep(3)
    print("func_one end")


def func_two():
    print("func_two")
    time.sleep(1)
    print("func_two end")


if __name__ == '__main__':
    t_one = Thread(target=func_one)
    t_two = Thread(target=func_two)
    '''等所有子线程结束,便会回收资源,守护线程时间太长,故而没有后面的打印'''
    t_one.setDaemon(True)
    t_one.start()
    t_two.start()
    time.sleep(0.5)
    print("主函数")

结果:

func_one
func_two
主函数
func_two end

Process finished with exit code 0

锁与GIL

链接:https://www.cnblogs.com/ajaxa/p/9111884.html

同步锁

详情进程锁:https://blog.csdn.net/qq_33567641/article/details/81947832

实例1:

from threading import Thread
import time


def work():
    global n
    temp = n
    time.sleep(1)
    n = temp - 1


if __name__ == '__main__':
    n = 100
    lst = []
    for i in range(100):
        p = Thread(target=work)
        lst.append(p)
        p.start()
    for p in lst:
        p.join()
    print(n)

结果:

99

Process finished with exit code 0

实例2:

from threading import Thread, Lock
import time


def work():
    global n
    lock.acquire()
    temp = n
    time.sleep(0.1)
    n = temp - 1
    lock.release()


if __name__ == '__main__':
    lock = Lock()
    n = 100
    lst = []
    for i in range(100):
        p = Thread(target=work)
        lst.append(p)
        p.start()
    for p in lst:
        p.join()
    print(n)

结果:

0

Process finished with exit code 0

对比实例1和实例2,程序由原来的并发执行变为串行,虽然牺牲了效率,但是保证了数据安全。

实例3:

from threading import current_thread, Thread, Lock
import time


def work():
    global n
    print('%s is running' % current_thread().getName())
    temp = n
    time.sleep(0.5)
    n = temp - 1


if __name__ == '__main__':
    n = 100
    lock = Lock()
    threads = []
    start_time = time.time()
    for i in range(100):
        t = Thread(target=work)
        threads.append(t)
        t.start()
    for t in threads:
        t.join()

    stop_time = time.time()
    print('主函数时间:%s n值:%s' % (stop_time - start_time, n))

结果:(略)

实例4:

from threading import current_thread, Thread, Lock
import time


def work():
    # 未加锁的代码并发运行
    time.sleep(1)
    print('unlock %s start to run' % current_thread().getName())
    global n
    # 加锁的代码串行运行
    lock.acquire()
    print('lock %s start to run' % current_thread().getName())
    temp = n
    time.sleep(0.5)
    n = temp - 1
    print(n)
    lock.release()


if __name__ == '__main__':
    n = 100
    lock = Lock()
    threads = []
    start_time = time.time()
    for i in range(100):
        t = Thread(target=work)
        threads.append(t)
        t.start()
    for t in threads:
        t.join()
    stop_time = time.time()
    print('主线程时间:%s n值:%s' % (stop_time - start_time, n))

结果:(略)

有的同学可能有疑问:既然加锁会让运行变成串行,那么我在start之后立即使用join,不用加锁了,也是串行的效果啊。

没错。在start之后立刻使用jion,肯定会将100个任务的执行变成串行,毫无疑问,最终n的结果也肯定是0,是安全的。但问题是start后立即join,任务内的所有代码都是串行执行的,而加锁,只是加锁的部分即修改共享数据的部分是串行的。单从保证数据安全方面,二者都可以实现,但很明显是加锁的效率更高。

实例5:

from threading import current_thread, Thread, Lock
import time


def work():
    time.sleep(1)
    print('%s start to run' % current_thread().getName())
    global n
    temp = n
    time.sleep(0.5)
    n = temp - 1
    print(n)


if __name__ == '__main__':
    n = 100
    lock = Lock()
    start_time = time.time()
    for i in range(100):
        t = Thread(target=work)
        t.start()
        t.join()
    stop_time = time.time()
    print('主线程时间:%s n值:%s' % (stop_time - start_time, n))

结果:(略)

进过计算,实例4花费时间为51.05577826499939秒,实例5花费时间为150.1835799217224。由此可知,使用join方法耗时非常恐怖。

死锁与递归锁

与进程一样,线程有死锁与递归锁。

所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程,如下就是死锁:

from threading import Lock as Lock
import time


mutexA=Lock()
mutexA.acquire()
mutexA.acquire()
print(123)
mutexA.release()
mutexA.release()

解决方法,递归锁,在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。

这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁:

from threading import RLock as Lock
import time


mutexA=Lock()
mutexA.acquire()
mutexA.acquire()
print(123)
mutexA.release()
mutexA.release()

下面演示上厕所与卫生纸的案例:

from threading import Thread, RLock
import time


# RLock是递归锁 --- 是无止尽的锁,但是所有锁都有一个共同的钥匙
# 想解决死锁,配一把公共的钥匙就可以了。

def man(l_toilet, l_paper):
    l_toilet.acquire()  # 是男的获得厕所资源,把厕所锁上了
    print('男人占用厕所')
    time.sleep(1)
    l_paper.acquire()  # 男的拿纸资源
    print('男人占用卫生纸')
    time.sleep(0.5)
    print('男人上完厕所')
    l_paper.release()  # 男的先还纸
    l_toilet.release()  # 男的还厕所


def woman(l_toilet, l_paper):
    l_paper.acquire()  # 女的拿纸资源
    print('女人占用卫生纸')
    time.sleep(1)
    l_toilet.acquire()  # 是女的获得厕所资源,把厕所锁上了
    print('女人占用厕所')
    time.sleep(0.5)
    print('女人上完厕所')
    l_toilet.release()  # 女的还厕所
    l_paper.release()  # 女的先还纸


if __name__ == '__main__':
    l_toi = l_pap = RLock()
    t_man = Thread(target=man, args=(l_toi, l_pap))
    t_woman = Thread(target=woman, args=(l_toi, l_pap))
    t_man.start()
    t_woman.start()

结果:

男人占用厕所
男人占用卫生纸
男人上完厕所
女人占用卫生纸
女人占用厕所
女人上完厕所

Process finished with exit code 0

信号量

同进程的一样,信号量(Semaphore)管理一个内置的计数器,每当调用acquire()时内置计数器-1,调用release() 时内置计数器+1。计数器不能小于0,当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。

实例:(同时只有5个线程可以获得semaphore,即可以限制最大连接数为5):

from threading import Thread, Semaphore
import threading
import time


def func(sm):
    sm.acquire()
    print('%s get sm' % threading.current_thread().getName())
    time.sleep(1)
    sm.release()


if __name__ == '__main__':
    s = Semaphore(2)
    for i in range(10):
        t = Thread(target=func, args=(s,))
        t.start()

结果:

Thread-1 get sm
Thread-2 get sm
Thread-3 get sm
Thread-4 get sm
Thread-5 get sm
Thread-6 get sm
Thread-7 get sm
Thread-8 get sm
Thread-9 get sm
Thread-10 get sm

Process finished with exit code 0

与进程池是完全不同的概念,进程池Pool(4),最大只能产生4个进程,而且从头到尾都只是这四个进程,不会产生新的,而信号量是产生一堆线程/进程。

事件

同进程的一样。线程的一个关键特性是每个线程都是独立运行且状态不可预测。如果程序中的其它线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就会变得非常棘手。为了解决这些问题,我们需要使用threading库中的Event对象。 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在初始情况下,Event对象中的信号标志被设置为假。如果有线程等待一个Event对象,,而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件,,继续执行。

事件常用方法:

Event.isSet():返回event的状态值;
Event.wait():如果 event.isSet()==False将阻塞线程;
Event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;
Event.clear():恢复event的状态值为False。

我们模拟尝试链接SQL过程:

import threading
import random
import time


def connect_sql(event):
    count = 1
    while not event.is_set():
        if count > 3:
            raise TimeoutError("链接超时")  # 次数大于3时候抛出异常
        print("<%s>第%s次尝试链接" % (threading.current_thread().getName(), count))
        event.wait(0.5)
        count += 1
    print("<%s>链接成功" % threading.current_thread().getName())


def check_sql(event):
    print('\033[45m[%s]正在检查mysql\033[0m' % threading.current_thread().getName())
    time.sleep(random.randint(2, 4))
    event.set()


if __name__ == '__main__':
    e = threading.Event()
    connect_one = threading.Thread(target=connect_sql, args=(e,))
    connect_two = threading.Thread(target=connect_sql, args=(e,))
    check = threading.Thread(target=check_sql, args=(e,))

    connect_one.start()
    connect_two.start()
    check.start()

结果:(略)

条件

Python提供的Condition对象提供了对复杂线程同步问题的支持。Condition被称为条件变量,除了提供与Lock类似的acquire和release方法外,还提供了wait和notify方法。线程首先acquire一个条件变量,然后判断一些条件。如果条件不满足则wait;如果条件满足,进行一些处理改变条件后,通过notify方法通知其他线程,其他处于wait状态的线程接到通知后会重新判断条件。不断的重复这一过程,从而解决复杂的同步问题。

实例:

import threading
import time


def run(n):
    con.acquire()
    con.wait()
    print("执行线程:", n)
    con.release()


if __name__ == '__main__':
    con = threading.Condition()
    for i in range(1000):
        t = threading.Thread(target=run, args=(i,))
        t.start()

    while 1:
        info = input(">>>")
        if info == 'q':
            break
        con.acquire()
        con.notify(int(info))
        con.release()
        time.sleep(5)
        print("主线程结束")

结果:(略)

定时器

指定一段时间后执行某个操作。

实例:

from threading import Timer


def hello():
    print("Hello,world!")


t = Timer(5, hello)
print("5秒后输出:")
t.start()

结果:

5秒后输出:
Hello,world!

Process finished with exit code 0

队列

queue队列 :使用import queue,用法与进程Queue一样。

class queue.Queue(maxsize=0),先进先出。

实例:

import queue

q = queue.Queue()
q.put('A')
q.put('B')
q.put('C')

print(q.get())
print(q.get())
print(q.get())
# print(q.get())  # 取完了不会报错,程序阻塞

结果:

A
B
C

Process finished with exit code 0

class queue.LifoQueue(maxsize=0) ,后进先出。

实例:

import queue

q = queue.LifoQueue()
q.put('A')
q.put('B')
q.put('C')

print(q.get())
print(q.get())
print(q.get())
# print(q.get())  # 取完了不会报错,程序阻塞

结果:

C
B
A

Process finished with exit code 0

class queue.PriorityQueue(maxsize=0) ,存储数据时可设置优先级的队列。

实例:

import queue

q = queue.PriorityQueue()

q.put((20, 'A'))  # 放入的数据必须是元组,第一个数为优先级,数字越小优先级越高
q.put((10, 'B'))
q.put((30, 'C'))

# 优先级越高者先出列
print(q.get())
print(q.get())
print(q.get())
# print(q.get())  # 取完了不会报错,程序阻塞

结果:

(10, 'B')
(20, 'A')
(30, 'C')

Process finished with exit code 0

线程/进程池

与进程池相同,下文将两者结合在一起研究。

Python标准库为我们提供了threading和multiprocessing模块编写相应的多线程/多进程代码,但是当项目达到一定的规模,频繁创建/销毁进程或者线程是非常消耗资源的,这个时候我们就要编写自己的线程池/进程池,以空间换时间。

但从Python3.2开始,标准库为我们提供了concurrent.futures模块,它提供了ThreadPoolExecutor和ProcessPoolExecutor两个类,实现了对threading和multiprocessing的进一步抽象,对编写线程池/进程池提供了直接的支持。

Executor和Future

concurrent.futures模块的基础是Exectuor,Executor是一个抽象类,它不能被直接使用。但是它提供的两个子类ThreadPoolExecutor和ProcessPoolExecutor却是非常有用,顾名思义两者分别被用来创建线程池和进程池的代码。我们可以将相应的tasks直接放入线程池/进程池,不需要维护Queue来操心死锁的问题,线程池/进程池会自动帮我们调度。

使用submit来操作线程池/进程池

实例:

from concurrent.futures import ThreadPoolExecutor
import time


def func(msg):
    time.sleep(2)
    return msg


pool = ThreadPoolExecutor(2)  # 创建一个最大容量为2的线程池
func_one = pool.submit(func, ('Hello'))  # 线程池加入任务
func_two = pool.submit(func, ('World'))
print(func_one.done())  # 判断任务是否结束
time.sleep(3)
print(func_two.done())
print(func_one.result())  # 返回结果
print(func_two.result())

结果:

False
True
Hello
World

Process finished with exit code 0

使用submit方法来往线程池中加入一个task,submit返回一个Future对象,对于Future对象可以简单地理解为一个在未来完成的操作。在第一个print语句中很明显因为time.sleep(2)的原因我们的future1没有完成,因为我们使用time.sleep(3)暂停了主线程,所以到第二个print语句的时候我们线程池里的任务都已经全部结束。

上述代码为线程模式,我们可改写为进程池模式,必须加上if __name__ == '__main__':

from concurrent.futures import ProcessPoolExecutor
import time


def func(msg):
    time.sleep(2)
    return msg


if __name__ == '__main__':
    pool = ProcessPoolExecutor(2)  # 创建一个最大容量为2的进程池
    func_one = pool.submit(func, ('Hello'))  # 进程池加入任务
    func_two = pool.submit(func, ('World'))
    print(func_one.done())  # 判断任务是否结束
    time.sleep(3)
    print(func_two.done())
    print(func_one.result())  # 返回结果
    print(func_two.result())

结果:

False
True
Hello
World

Process finished with exit code 0

使用map/wait来操作线程池/进程池

除了submit,Exectuor还为我们提供了map方法,和内建的map用法类似,下面我们通过两个例子来比较一下两者的区别。

使用submit()

实例1:

import concurrent.futures
import urllib.request
import time

URLS = ['http://www.xiao4j.com/', 'http://www.xiaohua100.cn/']


def load_data(url, timeout):
    with urllib.request.urlopen(url, timeout=timeout) as conn:
        return conn.read()


start_time = time.time()
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
    future_to_url = {executor.submit(load_data, url, 10): url for url in URLS}
    for future in concurrent.futures.as_completed(future_to_url):
        url = future_to_url[future]
        try:
            data = future.result()
        except Exception as exc:
            print('%r generated an exception: %s' % (url, exc))
        else:
            print('%r page is %d bytes' % (url, len(data)))
print("Time:", time.time() - start_time)

结果:

'http://www.xiaohua100.cn/' page is 48577 bytes
'http://www.xiao4j.com/' page is 42358 bytes
Time: 1.0040547847747803

Process finished with exit code 0

使用map()

实例2:

import concurrent.futures
import urllib.request
import time

URLS = ['http://www.xiao4j.com/', 'http://www.xiaohua100.cn/']


def load_data(url):
    with urllib.request.urlopen(url, timeout=10) as conn:
        return conn.read()


start_time = time.time()
with concurrent.futures.ThreadPoolExecutor(3) as executor:
    for url, data in zip(URLS, executor.map(load_data, URLS)):
        print('%r page is %d bytes' % (url, len(data)))
print("Time:", time.time() - start_time)

结果:

'http://www.xiao4j.com/' page is 42358 bytes
'http://www.xiaohua100.cn/' page is 48577 bytes
Time: 1.0533778667449951

Process finished with exit code 0

时间上差不多,效率也相当,感兴趣的朋友可以试试更多状态下的性能。不同点就是实例1中的as_completed不是按照URLS列表元素的顺序返回的,而实例2中的map是按照URLS列表元素的顺序返回的,并且写出的代码更加简洁直观,可以根据具体的需求任选一种。

使用wait()

wait方法接会返回一个tuple(元组),tuple中包含两个set(集合),一个是completed(已完成的)另外一个是uncompleted(未完成的)。使用wait方法的一个优势就是获得更大的自由度,它接收三个参数FIRST_COMPLETED, FIRST_EXCEPTION 和ALL_COMPLETE,默认设置为ALL_COMPLETED。

实例3:

from concurrent.futures import ThreadPoolExecutor, wait, as_completed
import time
import random


def func(num):
    time.sleep(random.randint(2, 5))
    return num


pool = ThreadPoolExecutor(5)
lst = []
for i in range(5):
    lst.append(pool.submit(func, i))
print(wait(lst))

结果:

DoneAndNotDoneFutures(done={<Future at 0x2998313e320 state=finished returned int>, <Future at 0x2998313a320 state=finished returned int>, <Future at 0x2998313a748 state=finished returned int>, <Future at 0x299812e5160 state=finished returned int>, <Future at 0x2998313e1d0 state=finished returned int>}, not_done=set())

Process finished with exit code 0

如果采用默认的ALL_COMPLETED,程序会阻塞直到线程池里面的所有任务都完成。

当我们将最后输出语句修改为

print(wait(lst, timeout=None, return_when='FIRST_COMPLETED'))

也就是改变默认值,再次运行结果:

DoneAndNotDoneFutures(done={<Future at 0x190681e4470 state=finished returned int>}, not_done={<Future at 0x1906843e208 state=running>, <Future at 0x1906843a358 state=running>, <Future at 0x1906843a780 state=running>, <Future at 0x1906843e358 state=running>})

Process finished with exit code 0

感兴趣的同学可以写一下更多任务下的线程池不同方法之间处理效率的差别!

  • 8
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值