python线程间通信+20200918

本文深入探讨Python多线程编程的基础知识与高级技巧,包括线程间的通信方式、线程同步机制、守护线程及线程阻塞的概念,同时还介绍了如何使用线程池提升并发性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

python线程间通讯

#!/usr/bin/python
# -*- coding:utf8 -*-

from threading import Thread, Lock 
import random


def test_thread():    #线程间的通信,使用锁来控制共享资源的访问
    a = 0
    n = 10000000
    lock = Lock()

    def imcr(n):
        global a
        for i in range(n):
            lock.acquire()  # 可以写成with lock:
            a += 1          # a+=1
            lock.release()

    def decr(n):
        for i in range(n):
            global a
            lock.acquire()
            a -= 1
            lock.release()

    t = Thread(target=imcr, args=(n,))
    t2 = Thread(target=decr, args=(n,))
    t.start()

    t2.start()
    t.join()
    t2.join()
    print(a)


# 多线程的消费者与生产者模式
# from threading import Thread
from queue import Queue

q = Queue(3)


class Producter(Thread):
    def __init__(self, queue):
        super().__init__()
        self.queue = queue

    def run(self):
        while True:
            item = random.randint(0, 99)
            self.queue.put(item)
            print('已产生%s' % item)


class Consumer(Thread):
    def __init__(self, queue):
        super().__init__()
        self.queue = queue

    def run(self):
        while True:
            item = self.queue.get()
            print(item)
            self.queue.task_done()  # 告诉队列这个任务执行完成


product = Producter(q)
consumer = Consumer(q)
product.start()
consumer.start()

# 注意,mgr=Manger() q.mgr.Queue()
# 进程版本的需要加一个input()或者是
# producter.join consumer.join()  因为一旦主进程结束,代码就不会继续运行了

因为GIL的限制,python的线程是无法真正意义上并行的。相对于异步编程,其性能可以说不是一个等量级的。为什么我们还要学习多线程编程呢,虽然说异步编程好处多,但编程也较为复杂,逻辑不容易理解,学习成本和维护成本都比较高。毕竟我们大部分人还是适应同步编码的,除非一些需要高性能处理的地方采用异步。

进程:进程是操作系统资源分配的基本单位。
线程:线程是任务调度和执行的基本单位。

一个应用程序至少一个进程,一个进程至少一个线程。

两者区别:同一进程内的线程共享本进程的资源如内存、I/O、CPU等,但是进程之间的资源是独立的。

一、多线程

python 可以通过 thread 或 threading 模块实现多线程,threading 相比 thread 提供了更高阶、更全面的线程管理。
下文以threading模块介绍多线程
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。

import threading
import time
 
class thread(threading.Thread):
 def __init__(self, threadname):
 threading.Thread.__init__(self, name='线程' + threadname)
 
 def run(self):
 print('%s:Now timestamp is %s'%(self.name,time.time()))
 
threads = []
for a in range(int(5)): # 线程个数
 threads.append(thread(str(a)))
for t in threads: # 开启线程
 t.start()
for t in threads: # 阻塞线程
 t.join()
print('END')

。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
输出:
线程3:Now timestamp is 1557386184.7574518
线程2:Now timestamp is 1557386184.7574518
线程0:Now timestamp is 1557386184.7574518
线程1:Now timestamp is 1557386184.7574518
线程4:Now timestamp is 1557386184.7582724
END

start() 方法开启子线程。运行多次 start() 方法代表开启多个子线程。
join() 方法用来阻塞主线程,等待子线程执行完成。举个例子,主线程A创建了子线程B,并使用了 join() 方法,主线程A在 join() 处就被阻塞了,等待子线程B完成后,主线程A才能执行 print(‘END’)。如果没有使用 join() 方法,主线程A创建子线程B后,不会等待子线程B,直接执行 print(‘END’),如下:

。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。

import threading
import time
 
class thread(threading.Thread):
 def __init__(self, threadname):
 threading.Thread.__init__(self, name='线程' + threadname)
 
 def run(self):
 time.sleep(1)
 print('%s:Now timestamp is %s'%(self.name,time.time()))
 
threads = []
for a in range(int(5)): # 线程个数
 threads.append(thread(str(a)))
for t in threads: # 开启线程
 t.start()
# for t in threads: # 阻塞线程
# t.join()
print('END')

。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
输出:
END
线程0:Now timestamp is 1557386321.376941
线程3:Now timestamp is 1557386321.377937
线程1:Now timestamp is 1557386321.377937
线程2:Now timestamp is 1557386321.377937
线程4:Now timestamp is 1557386321.377937

二、线程之间的通信

1、threading.Lock()
如果多个线程对某一资源同时进行修改,可能会存在不可预知的情况。为了修改数据的正确性,需要把这个资源锁住,只允许线程依次排队进去获取这个资源。当线程A操作完后,释放锁,线程B才能进入。如下脚本是开启多个线程修改变量的值,但输出结果每次都不一样。

import threading
 
money = 0
def Order(n):
 global money
 money = money + n
 money = money - n
 
class thread(threading.Thread):
 def __init__(self, threadname):
 threading.Thread.__init__(self, name='线程' + threadname)
 self.threadname = int(threadname)
 
 def run(self):
 for i in range(1000000):
  Order(self.threadname)
 
t1 = thread('1')
t2 = thread('5')
t1.start()
t2.start()
t1.join()
t2.join()
print(money)


接下来我们用 threading.Lock() 锁住这个变量,等操作完再释放这个锁。lock.acquire() 给资源加一把锁,对资源处理完成之后,lock.release() 再释放锁。以下脚本执行结果都是一样的,但速度会变慢,因为线程只能一个个的通过。

2、threading.Rlock()
用法和 threading Lock() 一致,区别是 threading.Rlock() 允许多次锁资源,acquire() 和 release() 必须成对出现,也就是说加了几把锁就得释放几把锁。

lock = threading.Lock()
# 死锁
lock.acquire()
lock.acquire()
print('...')
lock.release()
lock.release()
 
rlock = threading.RLock()
# 同一线程内不会阻塞线程
rlock.acquire()
rlock.acquire()
print('...')
rlock.release()
rlock.release()

3、thereading.Condition()
threading.Condition() 可以理解为更加高级的锁,比 Lock 和 Rlock 的用法更高级,能处理一些复杂的线程同步问题。threading.Condition() 创建一把资源锁(默认是Rlock),提供 acquire() 和 release() 方法,用法和 Rlock 一致。此外 Condition 还提供 wait()、Notify() 和 NotifyAll() 方法。
wait():线程挂起,直到收到一个 Notify() 通知或者超时(可选参数),wait() 必须在线程得到 Rlock 后才能使用。
Notify() :在线程挂起的时候,发送一个通知,让 wait() 等待线程继续运行,Notify() 也必须在线程得到 Rlock 后才能使用。 Notify(n=1),最多唤醒 n 个线程。
NotifyAll() :在线程挂起的时候,发送通知,让所有 wait() 阻塞的线程都继续运行。

import threading,time
 
def TestA():
 cond.acquire()
 print('李白:看见一个敌人,请求支援')
 cond.wait()
 print('李白:好的')
 cond.notify()
 cond.release()
 
def TestB():
 time.sleep(2)
 cond.acquire()
 print('亚瑟:等我...')
 cond.notify()
 cond.wait()
 print('亚瑟:我到了,发起冲锋...')
 
if __name__=='__main__':
 cond = threading.Condition()
 testA = threading.Thread(target=TestA)
 testB = threading.Thread(target=TestB)
 testA.start()
 testB.start()
 testA.join()
 testB.join()

输出
李白:看见一个敌人,请求支援
亚瑟:等我…
李白:好的
亚瑟:我到了,发起冲锋…

4、threading.Event()
threading.Event() 原理是在线程中立了一个 Flag ,默认值是 False ,当一个或多个线程遇到 event.wait() 方法时阻塞,直到 Flag 值 变为 True 。threading.Event() 通常用来实现线程之间的通信,使一个线程等待其他线程的通知 ,把 Event 传递到线程对象中。
event.wait() :阻塞线程,直到 Flag 值变为 True
event.set() :设置 Flag 值为 True
event.clear() :修改 Flag 值为 False
event.isSet() : 仅当 Flag 值为 True 时返回
下面这个例子,主线程启动子线程后 sleap 2秒,子线程因为 event.wait() 被阻塞。当主线程醒来后执行 event.set() ,子线程才继续运行,两者输出时间差 2s。

import threading
import datetime,time
 
class thread(threading.Thread):
 def __init__(self, threadname):
 threading.Thread.__init__(self, name='线程' + threadname)
 self.threadname = int(threadname)
 
 def run(self):
 event.wait()
 print('子线程运行时间:%s'%datetime.datetime.now())
 
if __name__ == '__main__':
 event = threading.Event()
 t1 = thread('0')
 #启动子线程
 t1.start()
 print('主线程运行时间:%s'%datetime.datetime.now())
 time.sleep(2)
 # Flag设置成True
 event.set()
 t1.join()

输出
主线程运行时间:2019-05-30 15:51:49.690872
子线程运行时间:2019-05-30 15:51:51.691523

5、其他方法
threading.active_count():返回当前存活的线程对象的数量
threading.current_thread():返回当前线程对象
threading.enumerate():返回当前所有线程对象的列表
threading.get_ident():返回线程pid
threading.main_thread():返回主线程对象

1、共享变量:

定义一个全局变量,然后在不同的线程函数中,使用 global 关键字声明为全局变量:
共享变量方法要考虑到线程间安全性。

    detail_url_list = []    # 全局变量
     
    def get_detail_html():
        # 爬取文章详情页
        global detail_url_list    # 用 global 声明为全局变量
        while True:
            if len(detail_url_list):
                url = detail_url_list.pop()
                # for url in detail_url_list:
                print("get detail html started")
                time.sleep(2)
                print("get detail html end")
     
    def get_detail_url():
        global detail_url_list    # 用 global 声明为全局变量
        while True:
            # 爬取文章列表页
            print("get url started")
            time.sleep(2)
            for i in range(20):
                detail_url_list.append("http://projectstedu.com/{id}".format(id=i))
            print("get detail url end")

2、通过Queue的方式进行线程间同步

Queue本身就是线程安全的

    from queue import Queue
    import time
    import threading
     
     
    def get_detail_html(queue):
        # 爬取文章详情页
        while True:
            url = queue.get()
            # for url in detail_url_list:
            print("get detail html started")
            time.sleep(2)
            print("get detail html end")
     
     
    def get_detail_url(queue):
        # 爬取文章列表页
        while True:
            print("get detail url started")
            time.sleep(4)
            for i in range(20):
                queue.put("http://projectsedu.com/{id}".format(id=i))
            print("get detail url end")
     
    detail_url_queue = Queue(maxsize=1000)    # 设置 Queue的最大数
     
    thread_detail_url = threading.Thread(target=get_detail_url, args=(detail_url_queue,))
    thread_detail_url.start()
     
    for i in range(10):    # 启动10条线程
        html_thread = threading.Thread(target=get_detail_html, args=(detail_url_queue,))
        html_thread.start()
     
    detail_url_queue.task_done()
    detail_url_queue.join()

Queue 队列中 join() 与 task_done() 的关系
如果线程里每从队列里取一次,但没有执行 task_done(),则 join 无法判断队列到底有没有结束,在最后执行个 join() 是等不到结果的,会一直挂起。可以理解为,每 task_done() 一次 就从队列里删掉一个元素,这样在最后 join 的时候根据队列长度是否为零来判断队列是否结束,从而执行主线程。
task_done() 给了 join()信号,告诉 join() 取出了一个元素,join() 就会判断队列长度是否为零了,如果为零,则结束线程,停止阻塞,回到主线程。
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。一、GIL全局解释器锁(cpython)
1.为什么会有这个锁:为了线程安全,减少python使用者的上手难度 GIL 使得同一个时刻只有一个线程在一个CPU上执行字节码,无法隐射到多个CPU,多核上执行。
2.特殊情况下会释放GIL:达到特定字节码行数、到底特定数目时间片、IO操作(主动)
二、并发与并行的区别
并发:描述程序的组织结构,指程序要被设计成多个可独立执行的子任务
并行:描述程序的执行状态,指多任务需要同时执行
三、守护线程&线程阻塞
守护线程:thread.setDaemon(true),当主程序退出的时候让子程序也一并退出
子线程阻塞:thread.join(),当子程序都结束后主程序再退出
四、多线程的写法
实例化Threading,调用Threading的方法去进行多线程编程
写子类继承Theading,重写相应的方法。说明:当程序简单时可使用实例化方法,当程序较复杂的时候,实现逻辑较多,第二种方法。
五、线程间通信
1.共享变量:
方法简单,也可以写入到单独的py文件中。问题:线程不安全,易出问题。
2.queue 队列:
使用queue 的 Queue,这个是线程安全的,多线程取数据不会出错。内部使用的是deque Python 的双端队列,在字节码的层面上就已经到达了线程安全。

q = Queue()
# 方法:
q.put()  # 放入数据
q.get()  # 取出数据
q.put_nowait()  # 放入数据,不用等待它完成再返回,异步的方法
q.get_nowait()  # 取出数据,不用等待它完成再返回,异步的方法

get()put(),可以设置是否阻塞的,默认是阻塞的
q.join()方法:只有q.task_done()调用了join()才会让主线程退出,成对使用。
六、线程同步

lock= Theading.Lock()
# 获取锁:
lock.acquire()
lock.release()

# 另一种方法:
with lock:
    # do something

加锁的代码段同时只有这一个代码段在执行,方式数据出问题。
缺点:1.用锁会影响性能 2. 可能引起死锁
死锁情况:1.有acquire 没有release 2. 相互等待
RLock 可重入锁
当在一个线程中多个地方需要加锁的时候用Lock 是不行的,需要用到RLock ,但是要注意的是获取和释放锁的数量要一致,成对出现。
Condition 条件变量
用于复杂的线程间同步,是一个同步锁。例如:先后顺序的多线程通信。
重点函数:wait() notify()

con = theading.Condition()
with con:
    # do something
    cond.notify()   #通知其他线程
    cond.wait()    # 等待其他线程通知
    # do something

注意:
1.先con.acquire()或者with con,获取condition锁,不然wait() notify() 不能用
2.Condition 有两把锁:一把底层锁会在线程调用了wait() 的时候释放,上面的锁会在每次调用wait的时候分配一把并放入condition的等待队列中,等到notify()的唤醒
222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222
Semaphore 信号量
用于控制某段代码进入线程的数量,比如控制爬虫的并发量。

import threading
import time

class HtmlSppier(threading.Thread):
    def __init__(self, url, sem):
        super().__init__()
        self.sem = sem
        self.url = url

    def run(self):
        time.sleep(2)
        print('download html success')
        self.sem.release()

class UrlProducer(threading.Thread):
    def __init__(self,sem):
        super().__init__()
        self.sem = sem
        
    def run(self):
        for i in range(20):
            self.sem.acquire()
            html_thread = HtmlSppier(f'http://www.qq.com/pn={i}',self.sem)
            html_thread.start()

if __name__ == '__main__':
    sem = threading.Semaphore(3)
    url_produce = UrlProducer(sem)
    url_produce.start()

七、线程池
为什么要使用线程池?
主线程中可以获取某一个线程的状态或者某一个的任务的状态以及返回值当一个线程完成的时候主线程能立即知道

import requests

def download_html(i):
    url = f'https://www.baidu.com/s?ie=UTF-8&wd={i}'
    response = requests.get(url).text
    print(response)

ids = list(range(100))

# 线程池方式一:
import threadpool
def thread_main(item):
    pool = threadpool.ThreadPool(30)
    tasks = threadpool.makeRequests(download_html, ids)
    [pool.putRequest(req) for req in tasks]
    pool.wait()

# 线程池方式二:
from multiprocessing.dummy import   Pool as thpool

def thread_pool(item):
    pool = thpool(20)
    pool.map(download_html, ids)
    pool.close()
    pool.join()

# 线程池方式三(推荐):
from concurrent.futures import ThreadPoolExecutor

with ThreadPoolExecutor(max_workers=8) as exe:
    exe.map(download_html,ids)

444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444
ThreadPoolExecutor其他方法使用:

# 其他接口使用:
from concurrent.futures import ThreadPoolExecutor, as_completed,wait

executor = ThreadPoolExecutor(max_workers=8)

# 通过 submit 提交执行的函数到线程中
task1 = executor.submit(download_html, (1))
task2 = executor.submit(download_html, (3))

# done() 判断 task 是否完成
print(task1.done())
time.sleep(4)
print(task1.done())

# result() 获取 task 的执行结果 阻塞
print(task1.result())

# cancel() 取消任务,如果任务在执行中或者执行完了是不能取消的
# 现在线程池是8 两个任务都会被提交任务去执行,如果 max_workers = 1,执行task2.cancel()就会成功取消
print(task2.cancel())

# as_completed() 获取已经成功的task的返回数据,阻塞
# as_completed实际上是一个生成器,里面有 yield 会把已经完成的 future (task) 返回结果
ids = list(range(10))
all_task = [executor.submit(download_html,(i)) for i in ids]
time.sleep(8)
# 这是异步的,谁完成就处理谁
for future in as_completed(all_task):
    data = future.result()
    print(f'html response {data}')

# 通过 executor 获取已经完成的task
for data in executor.map(download_html,ids):
    print(f'html response {data}')

# wait() 等待task完成
ids = list(range(10))
all_task = [executor.submit(download_html,(i)) for i in ids]

#  wait 的 return_when 可选项
FIRST_COMPLETED = 'FIRST_COMPLETED'
FIRST_EXCEPTION = 'FIRST_EXCEPTION'
ALL_COMPLETED = 'ALL_COMPLETED'
_AS_COMPLETED = '_AS_COMPLETED'

wait(all_task, return_when=ALL_COMPLETED)

利用PyQt中的QThread类实现多线程

利用PyQt中的pyqtSignal类实现信息的触发和捕获,即定义事件和订阅事件

1、新建一个python类,继承自QThread

from PyQt5.QtCore import QThread
class SubThread(QThread):

,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
2、重写__init__(),del() 和 run()函数

from PyQt5.QtCore import QThread

class SubThread(QThread):
    def __init__(self):
        super().__init__()
        #以下加入需要的代码

    def __del__(self):
        self.wait()
 
    def run(self):
        #以下加入子线程执行的代码

,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
3、将继承自QThread类实例化,然后调用实例对象的start()函数,即可开启新线程
QThread的子类的实例,必须放在app = QtWidgets.QApplication(sys.argv)和sys.exit(app.exec_())代码之间。

if __name__ =='__main__':
    app = QtWidgets.QApplication(sys.argv)
    subthread = SubThread()
    subthread.start()
    sys.exit(app.exec_())

利用pyqtSingal类实现信息触发和捕获的方法
1、在类内头部定义pyqtSingal属性

from PyQt5.QtCore import QThread,pyqtSingal
class SubThread(QThread):
    messagetrigger = pyqtSingal(str)

    def __init__(self):
        super().__init__()
        #以下加入需要的代码
    def __del__(self):
        self.wait()
    def run(self):
        #以下加入子线程执行的代码

2、在需要触发信息的地方使用,调用pyqtSingal的emit()函数触发消息

import time from PyQt5.QtCore
import QThread,pyqtSignal,QObject

class SubThread(QThread):
    messagetrigger = pyqtSingal(str)

    def __init__(self):
        super().__init__()
    def __del__(self):
        self.wait()
    def run(self):
        self.messagetrigger.emit('子线程开始')
        time.sleep(2)
        self.messagetrigger.emit('子线程结束')
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值