Python线程的简单介绍

本文介绍了Python中的线程,包括多任务概念、线程的创建与执行、线程间共享全局变量、互斥锁与死锁、线程同步方法。重点讲述了线程同步的实现,如使用Lock、Rlock、Condition和线程同步队列queue,强调了正确使用锁以避免资源竞争和死锁的重要性。

多任务

  • 说起线程多多少少会和多任务挂钩,什么是多任务呢?
  • 多任务其实就是在同一时间同时做很多件事情
    • 并发:CPU 小于当前执行的任务,是假的多任务
    • 并行:CPU 大于当前执行的任务,是真的多任务
  • 多任务实现的几种方式:
    • 线程
    • 进程
    • 协程

线程

  • 线程,也被称为轻量进程,是处理器能够进行运算调度的最小单位,是进程内的一个执行单元,是进程的实际运作单位。
  • 多线程类似于执行多个同时执行多个不同的程序,有以下的优点:
    • 使用线程可以把占据长时间的程序中的任务放到后台去处理
    • 程序运行速度加快
    • 在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,线程就比较有用了。在这种情况下我们可以释放一些珍贵的资源如内存占用等等。

Python中线程的创建方式

  • 函数式:


import threading
import time

"""
 使用线程来完成边唱歌边跳舞
"""
def sing():

    for i in range(4):

        print('I am a singer')
        time.sleep(1)

def dance():
    for i in range(4):

        print('I am a dancer')
        time.sleep(1)

def main():

    t1 = threading.Thread(target=sing)
    t2 = threading.Thread(target=dance)

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

if __name__ == '__main__':

    main()

  • 通过继承Threading.Thread类来创建线程

import threading

class A(threading.Thread):

    pass

class B(threading.Thread):

    pass

a = A()
b = B()
a.start()
b.start()

线程的具体分析

线程类Thread参数说明
  • group:线程组,目前只能使用None
  • target:执行的目标任务名
  • args:以元组的方式给执行任务传参
  • kwargs:以字典的方式给执行任务传参
  • name:线程名,一般不用设置
子线程与主线程的执行
  • setDaemon:用来保护主线程的

import threading
import time

def demo1():

    for i in range(4):

        print('--demo1--')
        time.sleep(1)

def main():

    t = threading.Thread(target=demo1)
    t.setDaemon(True) # 必须放在线程启动之前
    t.start()
    # t.join()

    print('111')

if __name__ == '__main__':

    main()
 
# --demo1--
# 111
  • join()

import threading
import time

def demo1():

    for i in range(4):

        print('--demo1--')
        time.sleep(1)

def main():

    t = threading.Thread(target=demo1)
    # t.setDaemon(True)
    t.start()
    t.join()

    print('111')

if __name__ == '__main__':

    main()
   
# --demo1--
# --demo1--
# --demo1--
# --demo1--
# 111

综上情况:
setDaemon(True)是用来保护主线程的,主线程不会去等待子线程结束。
join():是用来阻塞线程的,主线程会等待子线程的结束,再去结束主线程

线程的启动与线程的数量
  • 使用threading.enumerate()判断线程的数量从而确定线程启动的位置

import threading
import time

def demo1():

    for i in range(3):

        print('--demo1--')
        time.sleep(1)

def demo2():

    for i in range(4):

        print('--demo2--')
        time.sleep(1)

def main():

    print(threading.enumerate())

    t1 = threading.Thread(target=demo1, name='demo1')
    t2 = threading.Thread(target=demo2, name='demo2')

    print(threading.enumerate())
    t1.start()
    t2.start()
    print(threading.enumerate()
    
	# while True:
    #     print(threading.enumerate())
    #
    #     if len(threading.enumerate()) <= 1:
    #         break
    #     time.sleep(1)

if __name__ == '__main__':

    main()
    
# [<_MainThread(MainThread, started 7624)>]
# [<_MainThread(MainThread, started 7624)>]
# --demo1--
# --demo2--
# [<_MainThread(MainThread, started 7624)>, <Thread(demo1, started 11120)>, <Thread(demo2, started 7832)>]
# --demo2--
# --demo1--
# --demo2--
# --demo1--
# --demo2--

综上描述:线程是在 运行t.start()之后才开始启动的

线程间共享全局变量

import threading
import time

num = 100

def demo1():

    global num
    for i in range(1000000):
    #for i in range(100):

        num += 1
    print('demo1-- {}'.format(num))

def demo2():
    global num
	
	for i in range(1000000):
    #for i in range(100):
        num += 1
    print('demo2-- {}'.format(num))

def main():

    t1 = threading.Thread(target=demo1)
    t2 = threading.Thread(target=demo2)

    t1.start()
    t2.start()

    print('--main-- {}'.format(num))
if __name__ == '__main__':

    main()
    
# demo1-- 200
# demo2-- 300
# --main-- 300

# --main-- 336402
# demo1-- 1213786
# demo2-- 1321174

此处需要注意一个问题: 当我们循环的时候过大时候,会造成资源抢占混乱的情况。可以使用给线程上锁或者线程同步的方法 解决

线程之间传参
  • 我们可以使用 args 和 kwargs 在线程间传递参数

import threading
import time

num = 100
def demo1(m):

    global num
    for i in range(m):
        num += 1

    print('--demo1-- {}'.format(num))

def demo2(m):
    global num
    for i in range(m):
        num += 1

    print('--demo2-- {}'.format(num))

def main():

    t1 = threading.Thread(target=demo1, args=(100,))
    t2 = threading.Thread(target=demo2, args=(100,))

    t1.start()
    t2.start()

    print('--main-- {}'.format(num))

if __name__ == '__main__':

    main()
# --demo1-- 200
# --demo2-- 300
# --main-- 300
  • 使用kwargs 传值:
num = 100
def demo1(m):

    global num
    for i in range(m):
        num += 1

    print('--demo1-- {}'.format(num))

def demo2(m):
    global num
    for i in range(m):
        num += 1

    print('--demo2-- {}'.format(num))

def main():

    t1 = threading.Thread(target=demo1, kwargs={'m': 100})
    t2 = threading.Thread(target=demo2, kwargs={'m': 100})

    t1.start()
    t2.start()

    print('--main-- {}'.format(num))

if __name__ == '__main__':

    main()

# --demo1-- 200
# --demo2-- 300
# --main-- 300
互斥锁与死锁
  • 互斥锁:对共享数据进行锁定,保证同一时刻只能有一个线程去操作,从而保证多线程的情况下数据的准确性
  • 使用互斥锁的目的:保证多个线程访问共享数据时不会出现资源竞争以及数据混乱
  • 上锁与解锁的过程:
    • 当一个线程调用锁的 acquire() 方法获得锁时,锁就进入“Locked”状态
    • 每次只有一个线程获得锁。当另一个线程想要获得锁时,该线程进入“Locked”状态,俗称“阻塞”,直到拥有锁的线程调用锁的“release()”方法释放锁后,锁进入“unLocked”状态
    • 线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使该线程进入运行(running)状态
# 1.创建锁
mutex = threading.Lock()

# 2.上锁
mutex.acquire()

# 3.解锁
mutex.release()

注意:

  • 抢到锁的线程先执行,没有抢到锁的线程需要等待,等锁用完后需要释放,然后其他等待的线程再去抢这个锁,哪个线程先抢到哪个线程先执行。
  • 具体哪个线程 抢到的锁不是由我们决定的,是由CPU的资源调度决定的
  • 线程同步只能保证多个先线程安全访问竞争资源,最简单的同步机制是引入互斥锁
  • 当我们要更改共享数据时,先将其锁定,此时资源的状态为“ 锁定 ”,其他线程不能更改;直到该线程释放资源,将资源的状态变为“ 非锁定 ”,其他的线程才能再次锁定改资源。互斥锁保证了每次只有一个线程进行功写入操作,从而保证了数据的准确性

使用互斥锁来解决之前的资源抢占导致数据混乱的问题:


import threading
import time

mutex = threading.Lock()

num = 100
def demo1(m):

    global num
    mutex.acquire()
    for i in range(m):
        num += 1
    mutex.release()
    print('--demo1-- {}'.format(num))

def demo2(m):
    global num

    mutex.acquire()
    for i in range(m):
        num += 1
    mutex.release()

    print('--demo2-- {}'.format(num))

def main():

    t1 = threading.Thread(target=demo1, args=(1000000,))
    t2 = threading.Thread(target=demo2, kwargs={'m': 1000000})

    t1.start()
    t2.start()

    mutex.acquire()
    print('--main-- {}'.format(num))
    mutex.release()

if __name__ == '__main__':

    main()
# --demo1-- 1000100
# --demo2-- 2000100
# --main-- 2000100
  • 死锁:在线程间共享多个资源的时候,如果两个线程分别得到一部分资源并且同时等待对方释放的资源,这时就会形成死锁
import threading
import time

class MyThread1(threading.Thread):
    def run(self):
        # 对mutexA上锁
        mutexA.acquire()

        # mutexA上锁后,延时1秒,等待另外那个线程 把mutexB上锁
        print(self.name+'----do1---up----')
        time.sleep(1)

        # 此时会堵塞,因为这个mutexB已经被另外的线程抢先上锁了
        mutexB.acquire()
        print(self.name+'----do1---down----')
        mutexB.release()

        # 对mutexA解锁
        mutexA.release()

class MyThread2(threading.Thread):
    def run(self):
        # 对mutexB上锁
        mutexB.acquire()

        # mutexB上锁后,延时1秒,等待另外那个线程 把mutexA上锁
        print(self.name+'----do2---up----')
        time.sleep(1)

        # 此时会堵塞,因为这个mutexA已经被另外的线程抢先上锁了
        mutexA.acquire()
        print(self.name+'----do2---down----')
        mutexA.release()

        # 对mutexB解锁
        mutexB.release()

mutexA = threading.Lock()
mutexB = threading.Lock()

if __name__ == '__main__':
    t1 = MyThread1()
    t2 = MyThread2()
    t1.start()
    t2.start()
线程同步

如果多个线程共同对某个数据进行修改,则可能出现不可预料的后果,为了保证数据的正确性,对多个线程进行同步。
​ 使用Thread对象的Lock和Rlock可以实现简单的线程同步,这两个对象都有acquire方法和release方法,对于那些需要每次只允许一个线程操作的数据,可以将其操作放到acquire和release方法之间

  • 使用Condition方法实现线程同步:
    • 要求 :
      • 天猫精灵:小爱同学
      • 小爱:在
      • 天猫精灵:现在几点了
      • 小爱:你猜猜现在几点了
import threading
from threading import Condition
# 方法一:
class TianMao(threading.Thread):
    def __init__(self, cond):
        super().__init__(name='天猫精灵')
        self.cond = cond
    def run(self):

        self.cond.acquire()
        print('{}:小爱同学'.format(self.name))
        self.cond.notify()

        self.cond.wait()
        print('{}:现在几点了'.format(self.name))
        self.cond.notify()
        self.cond.release()

class XiaoAi(threading.Thread):

    def __init__(self, cond):
        super().__init__(name='小爱')
        self.cond = cond

    def run(self) -> None:

        with self.cond:
            # self.cond.acquire()

            self.cond.wait()
            print('{}:在'.format(self.name))
            self.cond.notify()

            self.cond.wait()
            print('{}:你猜现在几点了?'.format(self.name))
            self.cond.notify()

            # self.cond.release()

if __name__ == '__main__':

    # 实例化 Condition 对象
    cond = threading.Condition()

    # 实例化两个线程对象
    xiaoai = XiaoAi(cond)
    tianmao = TianMao(cond)

    # 启动线程
    xiaoai.start()
    tianmao.start()

# 方法二:
import threading
from threading import Condition
"""
天猫精灵:小爱同学
小爱:在
天猫精灵:现在几点了
小爱:你猜猜现在几点了
"""

class XiaoAi(threading.Thread):

    def __init__(self, cond):
        super().__init__(name='小爱同学')
        self.cond = cond

    def run(self) -> None:

        with self.cond:

            self.cond.wait()    
            print('{}:在'.format(self.name))
            self.cond.notify()

            self.cond.wait()
            print('{}:你猜现在几点了'.format(self.name))
            self.cond.notify()

class TianMao(threading.Thread):

    def __init__(self, cond):
        super().__init__(name='天猫精灵')
        self.cond = cond

    def run(self) -> None:

        with self.cond:


            print('{}:小爱同学'.format(self.name))
            self.cond.notify()

            self.cond.wait()
            print('{}:现在几点了?'.format(self.name))
            self.cond.notify()

def main():

    cond = Condition()

    xiaoai = XiaoAi(cond)
    tianmao = TianMao(cond)

    xiaoai.start()
    tianmao.start()

if __name__ == '__main__':

    main()

这里需要注意:Condition需要加锁和解锁,将唤醒和通知的方法 放到加锁和解锁之间。 或者使用上下文管理器代替Condition的上锁与解锁

  • 线程同步队列queue
    • Python的queue模块中提供了同步的、线程安全的队列类,包括FIFO(先入先出)队列Queue,LIFO(后入先出)队列LifoQueue,和优先级队列PriorityQueue。这些队列都实现了锁原语,能够在多线程中直接使用。可以使用队列来实现线程间的同步。
  • queue模块的常用方法有:

    • queue.empty() 如果队列为空,返回True,反之False
    • queue.full() 如果队列满了,返回True,反之False
    • queue.get([block[, timeout]])获取队列,timeout等待时间
    • queue.get_nowait() 相当Queue.get(False)
    • queue.put(item) 写入队列,timeout等待时间
    • queue.put_nowait(item) 相当Queue.put(item, False)
    • queue.qsize() 返回队列的大小
from queue import Queue
# 实例化队列对象
q = Queue(4)

q.put(1)
q.put(2)
q.put(3)
q.put(4)
# q.put_nowait(5)  # queue.Full

print(q.empty())  # False
print(q.full())   # True
print(q.qsize())  # 4

print(q.get())    # 1
print(q.get())	  # 2
print(q.get())	  # 3
print(q.get())    # 4
"""
put_nowait()和get_nowait()方法一样。
put_nowait()向队列中存放数据,当队列已经满的时候,会直接抛出queue.Full异常,而put()则会一直等待
"""

"""
 在线程中,访问⼀些全局变量,加锁是⼀个经常的过程。如果你是想把⼀些数据存储到某个队列中,那么Python内置了⼀个线程安全的模块叫做queue模块。Python中的queue模块中提供了同步的、线程安全的队列类,包括 FIFO(先进先出)队列Queue,LIFO(后⼊先出)队列LifoQueue。这些队列都实现了锁原语(可以理解为原⼦操作,即要么不做,要么都做完),能够在多线程中直接使⽤。可以使⽤队列来实现线程间的同步。
"""

总结
  • 使用多线程可以完成多任务
  • 只有线程启动,线程才会加入到活动线程列表
  • 线程之间的执行时无序的
  • 主线程会等待所有子线程结束后执行,如果有需要可以使用setDaemon 保护主线程
  • 自定义线程不能指定target,因为自定义线程里面的任务都指定在 run 方法中执行
  • 启动线程调用 start 方法,不要直接调用 run 方法
  • 多线程共享全局变量,很方便在多个线程间共享数据
  • 多个线程同时对同一个全局变量操作,会有可能出现资源竞争导致的数据错误问题
  • 线程的同步可以解决资源竞争的问题,但是会使多任务变为单任务
  • 锁的好处:
    • 确保某段代码只能由一个线程从头至尾地完整执行
  • 锁的坏处:
    • 多线程执行变成了包含锁的某段代码,实际上只能以单线程模式执行,大大降低了执行效率
    • 锁使用不好容易出现死锁的状况
  • 使用互斥锁时要注意死锁问题,需要及时将锁释放
  • 死锁一旦发生将会导致程序一直处于阻塞状态
### 三、Python多线程简介 Python中的多线程是通过 `threading` 模块实现的,它允许序在同一进中并发执行多个线程。每个线程是独立执行的轻量级子任务,共享同一进的内存空间,因此线程之间的通信和资源共享比进间更加高效[^1]。 在Python中,由于全局解释器锁(GIL)的存在,多线程并不能真正实现多核并行计算,但仍然适用于I/O密集型任务,如网络请求、文件读写等。在这些场景下,一个线程等待I/O操作完成时,其他线程可以继续执行任务,从而提升整体效率[^2]。 ### 四、多线程的工作原理 多线程的核心在于线程调度与资源共享。在Python中,主线程负责启动其他线程,并可以对它们进行管理。每个线程具有独立的执行路径,但共享全局变量、堆内存等资源。线程的调度由操作系统内核完成,Python通过 `threading` 模块对线程进行封装,提供高级接口[^1]。 线程的生命周期包括创建、就绪、运行、阻塞和终止五个状态。通过调用 `start()` 方法创建并启动线程线程执行完毕后自动进入终止状态。若线程中执行的任务发生阻塞(如等待用户输入、网络响应),操作系统会调度其他就绪线程执行,从而实现并发[^2]。 以下是一个简单多线程示例: ```python import threading import time def print_numbers(): for i in range(5): time.sleep(1) print(i) # 创建线程 thread = threading.Thread(target=print_numbers) # 启动线程 thread.start() # 等待线程执行完毕 thread.join() print("主线程结束") ``` 上述代码中,主线程启动一个子线程执行 `print_numbers` 函数,并通过 `join()` 方法等待子线程执行完毕。 ### 五、线程同步机制 在多线程环境中,多个线程同时访问共享资源可能导致数据不一致或竞态条件。为了解决这些问题,Python提供了多种同步机制,如锁(`Lock`)、条件变量(`Condition`)、事件(`Event`)和信号量(`Semaphore`)等。这些机制可以确保在任意时刻只有一个线程访问共享资源,从而保证数据的一致性和序的稳定性。 例如,使用 `threading.Lock` 来保护共享资源: ```python import threading lock = threading.Lock() shared_counter = 0 def increment_counter(): global shared_counter for _ in range(100000): with lock: shared_counter += 1 threads = [] for _ in range(4): thread = threading.Thread(target=increment_counter) threads.append(thread) thread.start() for thread in threads: thread.join() print(f"共享计数器最终值: {shared_counter}") ``` 在上述示例中,多个线程并发执行 `increment_counter` 函数,通过 `with lock` 确保每次只有一个线程修改 `shared_counter` 的值,从而避免竞态条件。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值