【Python】从入门到上头— 多线程(9)

进程和线程的区别

详见【Java基础】多线程从入门到掌握第一节(一.多线程基础)

在这里插入图片描述

一. _thread模块和threading模块

Python的标准库提供了两个模块:_thread和threading,_thread是低级模块,threading是高级模块,对_thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块。

  1. _thread 提供了低级别的、原始的线程以及一个简单的锁,它相比于 threading 模块的功能还是比较有限的。

    • threading 模块除了_thread 模块中的所有方法外,还提供的其他方法:

      • threading.currentThread(): 返回当前的线程变量。
      • threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
      • threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
  2. 除了使用方法外,线程模块同样提供了Thread类来处理线程,Thread类提供了以下方法:

    • run(): 用以表示具体执行的方法。
    • start(): 启动线程
    • join([time]): 等待至线程中止。这阻塞调用线程直至线程的join() 方法被调用中止-正常退出或者抛出未处理的异常-或者是可选的超时发生。
    • isAlive(): 返回线程是否运行的。
    • getName(): 返回线程名。
    • setName(): 设置线程名。

二.如何创建线程

Python中使用线程有两种方式:函数或者用来包装线程对象

1.函数式创建线程

  • _thread 模块函数式生成线程:调用中的start_new_thread()函数来产生新线程。

    如下::

    import _thread,threading
    import time
    
    
    # 为线程定义一个函数
    # 线程名,线程休眠时间s
    def loop(num):
        print('线程 %s 运行中...' % threading.current_thread().name)
        n = 0
        while n < 5:
            n = n + 1
            print('线程 %s >>> %s' % (threading.current_thread().name, n))
            time.sleep(1)
        print('线程 %s 运行结束.' % threading.current_thread().name)
    
    
    print('主线程 %s 运行中...' % threading.current_thread().name)
    # 创建两个线程
    try:
        _thread.start_new_thread(loop, (5,))
    except:
        print("Error: 无法启动线程")
    
    # 主线程一直保持运行
    while 1:
        pass
    print('主线程 %s 运行结束.' % threading.current_thread().name)
    

    在这里插入图片描述

  • threading 模块函数式生成线程:调用中的Thread函数来产生新线程

    import time, threading
    
    # 新线程执行的函数:
    def loop():
        print('线程 %s 运行中...' % threading.current_thread().name)
        n = 0
        while n < 5:
            n = n + 1
            print('线程 %s >>> %s' % (threading.current_thread().name, n))
            time.sleep(1)
        print('线程 %s 运行结束.' % threading.current_thread().name)
    
    
    # 主线程执行
    print('主线程 %s 运行中...' % threading.current_thread().name)
    #绑定要执行的函数以及设置函数名称
    t = threading.Thread(target=loop, name='LoopThread')
    t.start()
    t.join()
    print('主线程 %s 运行结束.' % threading.current_thread().name)
    

    在这里插入图片描述

2.使用 threading 模块用类包装形式创建线程

  • 我们可以通过直接从threading.Thread继承创建一个新的子类,并实例化后调用 start() 方法启动新线程,即它调用了线程的 run() 方法

    import threading
    import time
    
    exitFlag = 0
    # 1.定义线程方法
    def loop(threadName, delay, counter):
        print('线程 %s 开始运行.' % threadName)
        while counter:
            if exitFlag: threadName.exit()
            time.sleep(delay)
            print('线程%s>>> %s' % (threadName, time.ctime(time.time())))
    
            counter -= 1
        print('线程 %s 运行结束.' % threadName)
    
    
    # 2.线程类继承threading.Thread,实现run()和初始化线程变量
    class myThread(threading.Thread):
        def __init__(self, threadID, name, delay):
            threading.Thread.__init__(self)
            self.threadID = threadID
            self.name = name
            self.delay = delay
    
        def run(self):
            loop(self.name, self.delay, 5)
    
    
    if __name__ == "__main__":
        print('主线程 %s 运行中...' % threading.current_thread().name)
        # 创建新线程
        thread1 = myThread(1, "Thread-1", 1)
        thread2 = myThread(2, "Thread-2", 2)
    
        # 开启新线程
        thread1.start()
        thread1.join()
        print('主线程 %s 运行结束.' % threading.current_thread().name)
    

    在这里插入图片描述

    • 由于任何进程默认就会启动一个主线程基于主线程又可以启动新的线程
      • Python的threading模块有个current_thread()函数,用于返回当前线程的实例。
      • 主线程实例的名字叫MainThread子线程的名字在创建时指定

三.线程锁

多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响

  • 多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时修改一个变量。

多线程的优势在于可以同时运行多个任务(至少感觉起来是这样)。但是当线程需要存取共享数据时,可能存在数据不同步的问题。也就是线程安全问题,如以下实例:

import time, threading

#银行总存款:
balance = 0

def change_it(n):
    # 先存后取,结果应该为0:
    global balance
    #总存款存入n元
    balance = balance + n
    #总存款取出n元
    balance = balance - n

def run_thread(n):
    for i in range(2000000):
        change_it(n)

t1 = threading.Thread(target=run_thread, args=(5,))
t2 = threading.Thread(target=run_thread, args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print(balance)

全局共享变量balance,初始值为0,并且启动2个线程,先存后取,理论上结果应该为0,由于线程调度是由操作系统决定的,当t1、t2交替执行时,只要循环次数足够多,balance的结果就不一定是0了。

  • 原因是因为高级语言的一条语句在CPU执行时会被解析成若干条指令,即使一个简单的计算:

    如: 
    balance = balance + n 
    也分2步:
    	1.计算balance + n,存入临时变量中;
    	2.将临时变量的值赋给balance。
    
  • 也就是可以看成:

    x = balance + n
    balance = x
    

由于x是局部变量,两个线程各自都有自己的x,当代码单线程正常执行时:

初始值 balance = 0

t1: x1 = balance + 5 # x1 = 0 + 5 = 5
t1: balance = x1     # balance = 5
t1: x1 = balance - 5 # x1 = 5 - 5 = 0
t1: balance = x1     # balance = 0

t2: x2 = balance + 8 # x2 = 0 + 8 = 8
t2: balance = x2     # balance = 8
t2: x2 = balance - 8 # x2 = 8 - 8 = 0
t2: balance = x2     # balance = 0
    
结果 balance = 0

但是t1和t2是多线程交替运行的,如果操作系统以下面的顺序执行t1、t2:


初始值 balance = 0

t1: x1 = balance + 5  # x1 = 0 + 5 = 5

t2: x2 = balance + 8  # x2 = 0 + 8 = 8
t2: balance = x2      # balance = 8

t1: balance = x1      # balance = 5
t1: x1 = balance - 5  # x1 = 5 - 5 = 0
t1: balance = x1      # balance = 0

t2: x2 = balance - 8  # x2 = 0 - 8 = -8
t2: balance = x2      # balance = -8

结果 balance = -8

究其原因,是因为修改balance需要操作系统执行多条指令,而执行这几条语句时,线程可能中断,从而导致多个线程把同一个对象的内容改乱了。

  • 因此需要给change_it()加上锁,当某个线程开始执行change_it()时获得了锁,其他线程不能同时执行change_it(),只能等待,直到锁被释放后,获得该锁以后才能改由于锁只有一个,无论多少线程,同一时刻最多只有一个线程持有该锁,所以不会造成修改的冲突。创建一个锁就是通过**threading.Lock()来**实现:

    balance = 0
    #获取锁实例
    lock = threading.Lock()
    
    def run_thread(n):
        for i in range(100000):
            # 先要获取锁:
            lock.acquire()
            try:
                # 放心地改吧:
                change_it(n)
            finally:
                # 改完了一定要释放锁:
                lock.release()
    

考虑这样一种情况:一个列表里所有元素都是0,线程"set"从后向前把所有元素改成1,而线程"print"负责从前往后读取列表并打印。

  • 那么,可能线程"set"开始改的时候,线程"print"便来打印列表了,输出就成了一半0一半1,这就是数据的不同步。为了避免这种情况,引入了锁的概念。
import threading
import time


class myThread(threading.Thread):
    def __init__(self, threadID, name, delay):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name
        self.delay = delay

    def run(self):
        print("开启线程: " + self.name)
        # 获取锁,用于线程同步
        threadLock.acquire()
        try:
            # 放心地改吧:
            print_time(self.name, self.delay, 3)
        finally:
       	 	# 释放锁,开启下一个线程
        	threadLock.release()


def print_time(threadName, delay, counter):
    while counter:
        time.sleep(delay)
        print("%s: %s" % (threadName, time.ctime(time.time())))
        counter -= 1


if __name__ == "__main__":
    threadLock = threading.Lock()
    threads = []

    # 创建新线程
    thread1 = myThread(1, "Thread-1", 1)
    thread2 = myThread(2, "Thread-2", 2)

    # 开启新线程
    thread1.start()
    thread2.start()

    # 添加线程到线程列表
    threads.append(thread1)
    threads.append(thread2)

    # 等待所有线程完成
    for t in threads:
        t.join()
    print("退出主线程")

总结:

  • 当多个线程同时执行lock.acquire()时,只有一个线程能成功地获取锁,然后继续执行代码,其他线程就继续等待直到获得锁为止。

  • 获得锁的线程用完后一定要释放锁,否则等待锁的线程将永远等待下去,成为死线程。所以我们用try...finally来确保锁一定会被释放。

  • 锁的好处就是确保了某段关键代码只能由一个线程从头到尾完整地执行,坏处当然也很多,首先是阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。

  • 如果存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,既不能执行,也无法结束,只能靠操作系统强制终止

四.队列( Queue)(新手了解即可)

Queue 模块中提供了同步的、线程安全的队列类,包括FIFO(先入先出)队列QueueLIFO(后入先出)队列LifoQueue,和优先级队列 PriorityQueue

  • 这些队列都在多线程中直接使用,可以使用队列来实现线程间的同步。

Queue 模块中的常用方法:

  • Queue.qsize() 返回队列的大小
  • Queue.empty() 如果队列为空,返回True,反之False
  • Queue.full() 如果队列满了,返回True,反之False
  • Queue.full 与 maxsize 大小对应
  • Queue.get([block[, timeout]])获取队列,timeout等待时间
  • Queue.get_nowait() 相当Queue.get(False)
  • Queue.put(item) 写入队列,timeout等待时间
  • Queue.put_nowait(item) 相当Queue.put(item, False)
  • Queue.task_done() 在完成一项工作之后,Queue.task_done()函数向任务已经完成的队列发送一个信号
  • Queue.join() 实际上意味着等到队列为空,再执行别的操作
import queue
import threading
import time

exitFlag = 0


class myThread(threading.Thread):
    def __init__(self, threadID, name, q):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name
        self.q = q

    def run(self):
        print("开启线程:" + self.name)
        process_data(self.name, self.q)
        print("退出线程:" + self.name)


def process_data(threadName, q):
    while not exitFlag:
        #加锁
        queueLock.acquire()
        #如果工作队列不为空
        if not workQueue.empty():
            #获取队头
            data = q.get()
            #释放锁
            queueLock.release()
            print("%s processing %s" % (threadName, data))
        else:
            #释放锁
            queueLock.release()
        #休眠1s    
        time.sleep(1)


if __name__ == "__main__":
    # 线程集合
    threadList = ["Thread-1", "Thread-2", "Thread-3"]
    # 名称集合
    nameList = ["One", "Two", "Three", "Four", "Five"]
    # 线程锁
    queueLock = threading.Lock()
    # 生成一个长度为10的队列
    workQueue = queue.Queue(10)
    # 保存线程数组
    threads = []
    # 线程编号
    threadID = 1

    # 1.循环创建新线程
    for tName in threadList:
        thread = myThread(threadID, tName, workQueue)
        thread.start()
        threads.append(thread)
        threadID += 1

    # 2.填充队列
    queueLock.acquire()
    for word in nameList:
        workQueue.put(word)
    queueLock.release()

    # 等待队列清空
    while not workQueue.empty():
        pass

    # 通知线程是时候退出
    exitFlag = 1

    # 等待所有线程完成
    for t in threads:
        t.join()
    print("退出主线程")

在这里插入图片描述

五.ThreadLocal(新手了解即可)

在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他线程,而全局变量的修改必须加锁

  • ThreadLocal应运而生,每个线程都只能读写自己线程的独立副本,互不干扰。ThreadLocal解决了参数在一个线程中各个函数之间互相传递的问题。
import threading
    
# 创建全局ThreadLocal对象:
local_school = threading.local()

def process_student():
    # 获取当前线程关联的student:
    std = local_school.student
    print('Hello, %s (in %s)' % (std, threading.current_thread().name))

def process_thread(name):
    # 绑定ThreadLocal的student:
    local_school.student = name
    process_student()

t1 = threading.Thread(target= process_thread, args=('Alice',), name='Thread-A')
t2 = threading.Thread(target= process_thread, args=('Bob',), name='Thread-B')
t1.start()
t2.start()
t1.join()
t2.join()

执行结果:

Hello, Alice (in Thread-A)
Hello, Bob (in Thread-B)
  • 全局变量local_school就是一个ThreadLocal对象,每个Thread对它都可以读写student属性,但互不影响。你可以把local_school看成全局变量,但每个属性如local_school.student都是线程的局部变量可以任意读写而互不干扰,也不用管理锁的问题,ThreadLocal内部会处理。

  • 可以理解为全局变量local_school是一个dict,不但可以用local_school.student,还可以绑定其他变量,如local_school.teacher等等

    • ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。

六.多核CPU

多核CPU该可以同时执行多个线程。 如果写一个死循环的话,会出现什么情况呢?

  • 打开Mac OS X的Activity Monitor,或者Windows的Task Manager,可以监控到一个死循环线程会100%占用一个CPU。

    • 如果有两个死循环线程,在多核CPU中,可以监控到会占用200%的CPU,也就是占用两个CPU核心。

    • 要想把N核CPU的核心全部跑满,就必须启动N个死循环线程。

试用Python启动CPU的核心N个死循环线程。:

import threading, multiprocessing

def loop():
    x = 0
    while True:
        x = x ^ 1

for i in range(multiprocessing.cpu_count()):
    t = threading.Thread(target=loop)
    t.start()
  • 启动与CPU核心数量相同的N个线程,在4核CPU上可以监控到CPU占用率仅有102%,也就是仅使用了一核

    • 但是用C、C++或Java来写相同的死循环,直接可以把全部核心跑满,4核就跑到400%,8核就跑到800%,为什么Python不行呢?

Python的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。

  • GIL全局锁实际上把所有线程的执行代码都给上了锁,所以多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。**

  • GIL是Python解释器设计的历史遗留问题,通常我们用的解释器是官方实现的CPython,要真正利用多核,除非重写一个不带GIL的解释器

    • 所以Python多线程不能有效利用多核。如果一定要通过多线程利用多核,那只能通过C扩展来实现,不过这样就失去了Python简单易用的特点。

      • 不过,也不用过于担心,Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务多个Python进程有各自独立的GIL锁,互不影响。

猜你喜欢

转载自blog.csdn.net/qq877728715/article/details/132604852