本文参考:
https://blog.csdn.net/youngwyj/article/details/124720041
https://blog.csdn.net/youngwyj/article/details/124833126

前言

在日常的开发中经常会用到多线程和多进程编程,使用多线程编程可降低程序的复杂度,使程序更简洁高效。

线程是程序执行流的最小单元,是进程的一个实体,一个进程可以拥有多个线程,多个线程可以共享进程所拥有的资源。

线程可以提升程序的整体性能,一般分为内核线程和用户线程,内核线程由操作系统内核创建和撤销,用户线程不需要内核支持,是在用户程序中实现的线程。

需要注意的是:线程不能独立运行,他必须依附于进程;线程可以被抢占(中断)和暂时搁置(休眠)。

threading模块

1.简介

在 python3 中有两种实现多线程编程的模块。

  • 1._thread模块。python2中使用了thread模块实现,而 python3 不再使用该模块,为实现兼容在 python3 中将该模块改为了 _thread 模块。
  • 2.threading模块。因为 _thread 模块提供的是一个简单低级的线程和锁,但 threading 模块是基于 Java 的线程模块设计,相较于 _thread 更高级,所以本文只讲解 threading 模块。

其实python并不适合做多线程的开发,因为 python 解释器使用了内部的全局解释器锁(GIL锁),使得在无论何时计算机只会允许在处理器上运行单个线程,即便多核GPU也是如此(即GIL锁同一时刻只允许一个进程只有一个线程被CPU调度),所以它的效率较其他语言低。

threading 模块创建线程的方式与 Java 类似,都是采用的类来创建线程。

threading模块的函数:

函数 说明
threading.active_count() 返回正运行的线程数量(包括主线程),与len(threading.enumerate())结果相同
threading.current_thread() 返回当前线程的变量
threading.main_thread() 返回主线程
threading.enumerate() 返回一个正在运行的线程的列表

先来对单线程和多线程做一个对比,代码示例如下:

不使用多线程的操作:

import time
def worker():
    print("worker")
    time.sleep(1)
    return

if __name__ == "__main__": 
    for i in range(5):
        worker()

运行结果:

"""
worker
worker
worker
worker
worker
"""

使用多线程并发的操作,花费时间要短很多:

import threading
import time

def worker():
    print("worker")
    time.sleep(1)
    return

if __name__ == "__main__":
    for i in range(5):
        t = threading.Thread(target=worker)
        t.start()

运行结果:

"""
worker
worker
worker
worker
worker
"""

2.创建线程————start()方法

方法一:
在创建线程时必须先定义一个继承threading.Thread的类,然后重写子类中的run()方法,使用start()方法启动线程。

import threading
# 类必须继承threading.Thread
class threadTest(threading.Thread):
    # args为传入线程的参数,可根据自己的需求进行定义
    def __init__(self,args) -> None:
        # 初始化super()内的必须和类名一样
        super(threadTest,self).__init__()         
        self.args = args

    # 定义run()方法,主要写线程的执行内容
    def run(self) -> None:
        print('test thread running...')
        print('args: ',self.args)
        return super().run()

if __name__ == "__main__":
    # 实例化
    test = threadTest('just test')
    # 启动线程。即运行run()方法
    test.start()

# 上述代码等于
import threading
import time
class threadTest(threading.Thread):
    def __init__(self,args):
        super(threadTest,self).__init__()
        self.args = args

    def run(self):
        print('test thread running...')
        print('args: ',self.args)
        return super().run()

if __name__ == '__main__':
    test = threadTest('worker')
    test.start()

方法二:
使用函数的方式创建线程。

import threading
def threadTest(arg):
    print("test thread running...")
    print("args: ",arg)

if __name__ == "__main__":
    # target传入函数名,注意不要写参数
    # args target传入的函数需要传入的参数,注意传入参数以元组的形式
    thread = threading.Thread(target=threadTest,args=('just test',))
    # 启动线程
    thread.start()

两种方式的运行结果都一样:

"""
test thread running...
args:  just test
"""

需要注意的是:创建的线程为子线程,是主线程创建的子线程。

除 start() 方法外,threading.Thread类还提供了以下方法:

方法 说明
join(timeout) 表示主线程等待子线程timeout时长(s)后子线程若还未结束,就强制结束子线程,不设置则主线程会一直等待子线程结束后再结束
getName() 获取线程名
setName() 设置线程名
isAlive() 返回线程是否正在运行
ident() 获取线程标识符
setDaemon(bool) 设置主,子线程运行时的关系。bool为True时主线程结束,子线程立即结束;为false主,子线程运行毫不相关,独立运行

3.join()方法

join()常用于控制线程的运行方法,只能在线程启动后使用,设置主线程是否等待子线程的结束。

未使用join()方法:

import threading
import time

# 类必须继承threading.Thread
class threadTest(threading.Thread):
    def __init__(self) -> None:
        # 初始化super()内的必须和类名一样
        super(threadTest,self).__init__()

    # 定义run()方法,主要写线程的执行内容
    def run(self) -> None:
        print('子线程' + self.getName() + '开始: ' + str(time.time()))
        # 休眠1s
        time.sleep(1)
        print('子线程' + self.getName() + '结束: ' + str(time.time()))

        return super().run()

if __name__ == "__main__":
    # 创建线程列表
    threads = [] 

    for i in range(1,5):
        test = threadTest()
        # 设置线程名称
        test.setName('Thread - %s' %(str(i)))
        threads.append(test)

    for thread in threads:
        # 启动线程
        thread.start()

print("主线程已结束: %s" %(str(time.time())))

运行结果:

"""
子线程Thread - 1开始: 1663308417.2721999
子线程Thread - 2开始: 1663308417.2724535
子线程Thread - 3开始: 1663308417.2726364
子线程Thread - 4开始: 1663308417.2727745
主线程已结束: 1663308417.272802
子线程Thread - 1结束: 1663308418.2736351
子线程Thread - 2结束: 1663308418.273736
子线程Thread - 3结束: 1663308418.2737775
子线程Thread - 4结束: 1663308418.2737951
"""

或者这里可以将打印的时间戳写为具体的时间,这样效果更加明显,几个print的地方改为:

import threading
import time

class threadTest(threading.Thread):
    def __init__(self):
        super(threadTest,self).__init__()

    def run(self):
        print('子线程' + self.getName() + '开始: ' + time.strftime('%T', time.localtime()))
        time.sleep(1)
        print('子线程' + self.getName() + '结束: ' + time.strftime('%T', time.localtime()))

        return super().run()

if __name__ == "__main__":
    threads = []
    for i in range(1,5):
        test = threadTest()
        test.setName('Thread - %s' %(str(i)))
        threads.append(test)

    for thread in threads:
        thread.start()
print('主线程已结束: %s' %(time.strftime('%T',time.localtime())))

运行结果:

子线程Thread - 1开始: 13:33:48
子线程Thread - 2开始: 13:33:48
子线程Thread - 3开始: 13:33:48
子线程Thread - 4开始: 13:33:48
主线程已结束: 13:33:48
子线程Thread - 1结束: 13:33:49
子线程Thread - 3结束: 13:33:49
子线程Thread - 2结束: 13:33:49
子线程Thread - 4结束: 13:33:49

可以看到未使用join()方法时,多个子线程几乎是同时创建,同时结束的,并且主线程并没有等子线程结束就已经结束了。
下面是使用了join()方法。

import threading
import time

# 类必须继承threading.Thread
class threadTest(threading.Thread):
    def __init__(self) -> None:
        # 初始化super()内的必须和类名一样
        super(threadTest,self).__init__()

    # 定义run()方法,主要写线程的执行内容
    def run(self) -> None:
        print('子线程' + self.getName() + '开始: ' + time.strftime('%T', time.localtime()))
        # 休眠1s
        time.sleep(1)
        print('子线程' + self.getName() + '结束: ' + time.strftime('%T', time.localtime()))

        return super().run()

if __name__ == "__main__":
    
    # 创建线程列表
    threads = []

    for i in range(1,5):
        test = threadTest()
        # 设置线程名称
        test.setName('Thread-%s' %(str(i)))
        threads.append(test)

    for thread in threads:
        # 启动线程
        thread.start()
        thread.join()

print('主线程已结束: %s' %(time.strftime('%T', time.localtime())))

运行结果:

"""
子线程Thread-1开始: 13:45:14
子线程Thread-1结束: 13:45:15
子线程Thread-2开始: 13:45:15
子线程Thread-2结束: 13:45:16
子线程Thread-3开始: 13:45:16
子线程Thread-3结束: 13:45:17
子线程Thread-4开始: 13:45:17
子线程Thread-4结束: 13:45:18
主线程已结束: 13:45:18
"""

可以看到使用了join()后线程是依次运行的,且主线程是等待子线程结束后再结束的。

4.setDaemon(bool)方法

setDaemon(bool)方法用于设置主线程的守护线程,在启动线程启动之前使用。

当bool为True时,该线程为守护线程,主线程结束,子线程立即结束;为false主,子线程运行毫不相关,独立运行,子线程继续运行到结束。

先来看一个简单的例子:

# threading.setDaemon()的使用,设置后台进程。
import time
import threading

def worker():
    time.sleep(3)
    print("worker")

t = threading.Thread(target=worker)
t.setDaemon(True)
t.start()
print("haha")

运行结果:

"""
haha
可以看出worker()方法中的打印操作并没有显示出来,说明已经成为后台进程。
"""

示例如下:

import threading
import time

# 类必须继承threading.Thread
class threadTest(threading.Thread):
    def __init__(self) -> None:
        # 初始化super()内的必须和类名一样
        super(threadTest,self).__init__()

    # 定义run()方法,主要写线程的执行内容
    def run(self) -> None:
        print('子线程' + self.getName() + '开始: ' + time.strftime('%T', time.localtime()))
        # 休眠3s
        time.sleep(3)
        print('子线程' + self.getName() + '结束: ' + time.strftime('%T', time.localtime()))

        return super().run()

if __name__ == "__main__":
    print('主线程已开始: %s' %(time.strftime('%T',time.localtime())))
    thread = threadTest()
    # 设置线程名称
    thread.setName('Thread-1')
    # 参数设置为False
    thread.setDaemon(False)
    thread.start()
    time.sleep(1)
    print('主线程已结束: %s' %(time.strftime('%T',time.localtime())))

运行结果:

"""
主线程已开始: 14:43:25
子线程Thread-1开始: 14:43:25
主线程已结束: 14:43:26
子线程Thread-1结束: 14:43:28
"""

可以看到bool为false主线程结束,子线程继续运行到结束。

import threading
import time

# 类必须继承threading.Thread
class threadTest(threading.Thread):
    def __init__(self) -> None:
        # 初始化super()内的必须和类名一样
        super(threadTest,self).__init__()

    # 定义run()方法,主要写线程的执行内容
    def run(self) -> None:
        print('子线程' + self.getName() + '开始: ' + time.strftime('%T', time.localtime()))
        # 休眠3s
        time.sleep(3)
        print('子线程' + self.getName() + '结束: ' + time.strftime('%T', time.localtime()))

        return super().run()

if __name__ == "__main__":
    print('主线程已开始: %s' %(time.strftime('%T',time.localtime())))
    thread = threadTest()
    # 设置线程名称
    thread.setName('Thread-1')
    # 参数设置为True
    thread.setDaemon(True)
    # 启动线程
    thread.start()
    time.sleep(1)
    print('主线程已结束: %s' %(time.strftime('%T',time.localtime())))

运行结果:

"""
主线程已开始: 14:44:03
子线程Thread-1开始: 14:44:03
主线程已结束: 14:44:04
"""

可以看到bool为True时,主线程一结束,子线程就立即结束。

5.activeCount()方法

threading.activeCount()的使用,此方法返回当前进程中线程的个数。返回的个数中包含主线程。
示例如下:

import threading
import time

def worker():
    print("test")
    time.sleep(1)
    return

if __name__ == "__main__":
    for i in range(5):
        t = threading.Thread(target=worker)
        t.start()

print("current has %d threads" %(threading.activeCount() - 1))

运行结果:

"""
test
test
test
test
test
current has 5 threads
"""

6.enumerate()方法

threading.enumerate()的使用,此方法返回当前运行中的Thread对象列表。

import time
import threading

def worker():
    print("test")
    time.sleep(2)

threads = []
if __name__ == "__main__":
    for i in range(5):
        t = threading.Thread(target=worker)
        threads.append(t)
        t.start()

for item in threading.enumerate():
    print(item)
print

运行结果:

"""
test
test
test
test
test
<_MainThread(MainThread, started 139837462284096)>
<Thread(Thread-1, started 139837333047040)>
<Thread(Thread-2, started 139837324654336)>
<Thread(Thread-3, started 139837316261632)>
<Thread(Thread-4, started 139837307868928)>
<Thread(Thread-5, started 139837299476224)>
"""

线程同步是多线程中很重要的概念,当多个线程需要共享数据时,如果不使用线程同步,就会存在数据不同步的情况。
要做到线程同步有两种方法,线程锁和条件变量Condition。

线程锁

1.Lock锁

threading模块中Lock锁和_thread模块中的锁是一样的。

代码示例:

import threading
import time

num = 0
# 申请线程锁
lock = threading.Lock()
# 类必须继承threading.Thread
class threadTest(threading.Thread):
    def __init__(self) -> None:
        # 初始化super()内的必须与类名一样
        super(threadTest,self).__init__()

    def run(self) -> None:
        # 声明全局变量num
        global num
        # 申请线程锁
        # lock.acquire()
        print('子线程' + self.getName() + '开始: ' + time.strftime('%T',time.localtime()))

        while num < 5:
            # 休眠2s
            time.sleep(2)
            print(self.getName(), 'num: ', num)
            num += 1
        print('子线程' + self.getName() + '结束: ' + time.strftime('%T',time.localtime()))
        
        # 释放线程锁
        # lock.release()

        return super().run()

if __name__ == "__main__":
    print('主线程开始: %s' %(time.strftime('%T',time.localtime())))
    thread1 = threadTest()
    # 设置线程名称
    thread1.setName('Thread-1')
    thread2 = threadTest()
    # 设置线程名称
    thread2.setName('Thread-2')
    # 启动线程
    thread1.start()
    # 启动线程
    thread2.start()
    time.sleep(1)
    thread1.join()
    thread2.join()
    print('主线程已结束: %s' %(time.strftime('%T',time.localtime())))

运行结果:

"""
主线程开始: 16:19:31
子线程Thread-1开始: 16:19:31
子线程Thread-2开始: 16:19:31
Thread-1 num:  0
Thread-2 num:  1
Thread-1 num:  2
Thread-2 num:  3
Thread-1 num:  4
Thread-2 num:  4
子线程Thread-1结束: 16:19:37
子线程Thread-2结束: 16:19:37
主线程已结束: 16:19:37
"""

可以看到在未使用锁线程时,线程1和线程2对num操作出现了混乱。
将上面代码 lock.acquire() lock.release()这两行代码的注释去掉,将线程锁添加。
运行结果如下:可以看到不会像上面出现混乱的情况。

import threading
import time

num = 0
# 申请线程锁
lock = threading.Lock()
# 类必须继承threading.Thread
class threadTest(threading.Thread):
    def __init__(self) -> None:
        # 初始化super()内的必须与类名一样
        super(threadTest,self).__init__()

    # 定义run()方法,主要写线程的执行内容
    def run(self) -> None:
        # 声明全局变量num
        global num

        # 申请线程锁
        lock.acquire()

        print('子线程' + self.getName() + '开始: ' + time.strftime('%T',time.localtime()))

        while num < 5:
            time.sleep(2)
            print(self.getName(), 'num: ', num)
            num += 1
        
        print('子线程' + self.getName() + '结束: ' + time.strftime('%T',time.localtime()))
        
        # 释放线程锁
        lock.release()

        return super().run()

if __name__ == "__main__":
    print('主线程开始: %s' %(time.strftime('%T',time.localtime())))
    thread1 = threadTest()
    # 设置线程名称
    thread1.setName('Thread-1')
    thread2 = threadTest()
    # 设置线程名称
    thread2.setName('Thread-2')
    # 启动线程
    thread1.start()
    # 启动线程
    thread2.start()
    time.sleep(1)
    thread1.join()
    thread2.join()
    print('主线程已结束: %s' %(time.strftime('%T',time.localtime())))

运行结果:

"""
主线程开始: 16:25:36
子线程Thread-1开始: 16:25:37
Thread-1 num:  0
Thread-1 num:  1
Thread-1 num:  2
Thread-1 num:  3
Thread-1 num:  4
子线程Thread-1结束: 16:25:47
子线程Thread-2开始: 16:25:47
子线程Thread-2结束: 16:25:47
主线程已结束: 16:25:47
"""

2.RLock锁

RLock锁又称递归锁,其与Lock锁的差别在于,Lock锁只允许在同一线程中申请一次,否则线程会进入死锁,但是RLock允许在同一线程多次调用。
使用Lock锁产生死锁示例代码:

import threading
import time

print('主线程开始: %s' %(str(time.strftime('%T', time.localtime()))))
lock = threading.Lock()

# 申请线程锁
lock.acquire()
print(threading.enumerate())

# 再次申请线程锁,产生了死锁
lock.acquire()
print(threading.enumerate())

lock.release()
lock.release()

print('主线程结束: %s' %(str(time.strftime('%T',time.localtime()))))

运行结果:

此处为我在终端中主动杀死了进程,而非程序自己结束。
"""
主线程开始: 16:29:33
[<_MainThread(MainThread, started 32092)>, <WriterThread(pydevd.Writer, started daemon 26816)>, <ReaderThread(pydevd.Reader, started daemon 3100)>, <_TimeoutThread(Thread-4, started daemon 14560)>, <PyDBCommandThread(pydevd.CommandThread, started daemon 4772)>, <CheckAliveThread(pydevd.CheckAliveThread, started 9552)>]
"""

使用RLock锁不会产生死锁示例代码:


import threading
import time

print('主线程开始: %s' %(str(time.strftime('%T', time.localtime()))))

lock = threading.RLock()

# 申请线程锁
lock.acquire()
print(threading.enumerate())

# 再次申请线程锁,不会产生死锁
lock.acquire()
print(threading.enumerate())

lock.release()
lock.release()

print('主线程结束: %s' %(str(time.strftime('%T',time.localtime()))))

运行结果:

"""
主线程开始: 16:37:45
[<_MainThread(MainThread, started 5632)>, <WriterThread(pydevd.Writer, started daemon 2252)>, <ReaderThread(pydevd.Reader, started daemon 22752)>, <_TimeoutThread(Thread-4, started daemon 30420)>, <PyDBCommandThread(pydevd.CommandThread, started daemon 27404)>, <CheckAliveThread(pydevd.CheckAliveThread, started 31420)>]
[<_MainThread(MainThread, started 5632)>, <WriterThread(pydevd.Writer, started daemon 2252)>, <ReaderThread(pydevd.Reader, started daemon 22752)>, <_TimeoutThread(Thread-4, started daemon 30420)>, <PyDBCommandThread(pydevd.CommandThread, started daemon 27404)>, <CheckAliveThread(pydevd.CheckAliveThread, started 31420)>]
主线程结束: 16:37:45
"""

从上面可以看到Lock与RLock的区别
注意线程锁需要成对出现

条件变量 Condition

Condition是python3中一种更高级的锁,除和线程锁类似的 acquire() 和 release() 函数外,还提供以下函数。

函数 说明
wait() 使线程挂起
notify() 唤醒挂起的线程使其运行
notifyAll() 唤醒所有线程使其运行

注意:线程使用前需要获得锁,否则会抛出RuntimeError异常

可以理解为,Condition提供了一种多线程通信机制,若线程1需要数据,线程1就会阻塞等待,线程2制造出数据,等待线程2制造好数据并通知线程1后,线程1就可以去获取数据了

下面是一个使用条件变量 Condition 模拟成语接龙示例代码:

import threading
import time

# 类必须继承threading.Thread
class Man1(threading.Thread):    
    def __init__(self,lock) -> None:
        # 初始化super()内的必须和类名一样
        super(Man1,self).__init__()
        self.lock = lock

    def run(self):
        # 申请锁
        self.lock.acquire()
        print('子线程' + self.getName() + '为所欲为')

        # 挂起线程,等待回答
        self.lock.wait()
        print('子线程' + self.getName() + '逼上梁山')
        # 运行挂起的线程
        self.lock.notify()

        self.lock.wait()
        print('子线程' + self.getName() + '尽力而为')
        self.lock.notify()

        self.lock.release()

# 类必须继承threading.Thread
class Man2(threading.Thread):
    def __init__(self,lock) -> None:
        # 初始化super()内的必须和类名一样
        super(Man2,self).__init__()
        self.lock = lock

    def run(self):
        # 申请锁
        self.lock.acquire()
        # 唤醒对方线程
        self.lock.notify()
        print('子线程' + self.getName() + '为法自弊')
        # 挂起线程,等待回答
        self.lock.wait()

        self.lock.notify()
        print('子线程' + self.getName() + '山穷水尽')
        self.lock.wait()

        self.lock.notify()
        print('子线程' + self.getName() + '为所欲为')
        
        self.lock.release()

if __name__ == '__main__':
    lock = threading.Condition()
    man1 = Man1(lock)
    man2 = Man2(lock)

    # 设置线程名称
    man1.setName('Thread-1')
    # 设置线程名称
    man2.setName('Thread-2')
    print('成语接龙开始: ')
    man1.start()
    man2.start()

运行结果:

"""
成语接龙开始: 
子线程Thread-1为所欲为
子线程Thread-2为法自弊
子线程Thread-1逼上梁山
子线程Thread-2山穷水尽
子线程Thread-1尽力而为
子线程Thread-2为所欲为
"""

可以看到在条件变量的控制下,两个线程按照顺序执行,直到结束。

posted on 2022-09-19 17:21  jiayou111  阅读(471)  评论(0编辑  收藏  举报