基于python的多线程编程与TCP多线程实现(多线程,线程锁,线程池 单例模式)

1.线程和进程

一个类比:

  • 一个工厂,至少有一个车间,一个车间中至少有一个工人,最终是工人在工作。

  • 一个程序,至少有一个进程,一个进程中至少有一个线程,最终是线程在工作。

    上述串行的代码示例就是一个程序,在使用python xx.py 运行时,内部就创建一个进程(主进程),在进程中创建了一个线程(主线程),由线程逐行运行代码。
    

进程和线程:

线程,是计算机中可以被cpu调度的最小单元(真正在工作)。
进程,是计算机资源分配的最小单元(进程为线程提供资源)。

一个进程中可以有多个线程,同一个进程中的线程可以共享此进程中的资源。

通过 进程线程 都可以将 串行 的程序变为并发,对于上述示例来说就是同时下载三个视频,这样很短的时间内就可以下载完成。
基于多线程对上述串行示例进行优化:

  • 一个工厂,创建一个车间,这个车间中创建 3个工人,并行处理任务。
  • 一个程序,创建一个进程,这个进程中创建 3个线程,并行处理任务。

2.多线程 多进程开发例子

常见的程序开发中,计算操作需要使用CPU多核优势,IO操作不需要利用CPU的多核优势,所以,就有这一句话:

  • 计算密集型,用多进程,例如:大量的数据计算【累加计算示例】。
  • IO密集型,用多线程,例如:文件读写、网络数据传输【下载抖音视频示例】。
    如果程序想利用 计算机的多核优势,让CPU同时处理一些任务,适合用多进程开发(即使资源开销大)。

2.1 多线程例子:

包含内容:import threading 模块 使用方式:
t = threading.Thread(target=task, args=(name, url))
t.start()

import time
import requests
import threading

url_list = [
    ("东北F4模仿秀.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0300f570000bvbmace0gvch7lo53oog"),
    ("卡特扣篮.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f3e0000bv52fpn5t6p007e34q1g"),
    ("罗斯mvp.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f240000buuer5aa4tij4gv6ajqg")
]


def task(file_name, video_url):
    res = requests.get(video_url)
    with open(file_name, mode='wb') as f:
        f.write(res.content)
    print(time.time())


for name, url in url_list:
    # 创建线程,让每个线程都去执行task函数(参数不同)
    t = threading.Thread(target=task, args=(name, url))
    t.start()

2.2 多进程 例子:

import multiprocessing
t = multiprocessing.Process(target=函数名, args=(name, url))
t.start()

import time
import requests
import multiprocessing

# 进程创建之后,在进程中还会创建一个线程。
# t = multiprocessing.Process(target=函数名, args=(name, url))
# t.start()
    
    

url_list = [
    ("东北F4模仿秀.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0300f570000bvbmace0gvch7lo53oog"),
    ("卡特扣篮.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f3e0000bv52fpn5t6p007e34q1g"),
    ("罗斯mvp.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f240000buuer5aa4tij4gv6ajqg")
]


def task(file_name, video_url):
    res = requests.get(video_url)
    with open(file_name, mode='wb') as f:
        f.write(res.content)
    print(time.time())


if __name__ == '__main__':
    print(time.time())
    for name, url in url_list:
        t = multiprocessing.Process(target=task, args=(name, url))
        t.start()

3.线程常见方法

t.start() 线程准备就绪,等待CPU调度,具体时间有CPU定
t.join() 等待当前线程的任务执行完毕后再向下执行
t.setDaemon(布尔值) ,守护线程(必须放在start之前)

  • t.setDaemon(True),设置为守护线程,主线程执行完毕后,子线程也自动关闭。
  • t.setDaemon(False),设置为非守护线程,主线程等待子线程,子线程执行完毕后,主线程才结束。
    线程的名字的设置与获取:
import threading

def task(arg):
    # 获取当前执行此代码的线程
    name = threading.current_thread().getName()
    print(name)


for i in range(10):
    t = threading.Thread(target=task, args=(11,))
    t.setName('日魔-{}'.format(i))
    t.start()
  • 线程类 在run方法内写入要做的事情
import threading

class MyThread(threading.Thread):
    def run(self):
        print('执行此线程', self._args)

t = MyThread(args=(100,))
t.start()

4.线程安全 线程锁和死锁

4.1 线程安全

一个进程中可以有多个线程,且线程共享所有进程中的资源。

多个线程同时去操作一个"东西",可能会存在数据混乱的情况,例如:

  • 示例1

    import threading
    
    loop = 10000000
    number = 0
    
    
    def _add(count):
        global number
        for i in range(count):
            number += 1
    
    
    def _sub(count):
        global number
        for i in range(count):
            number -= 1
    
    
    t1 = threading.Thread(target=_add, args=(loop,))
    t2 = threading.Thread(target=_sub, args=(loop,))
    t1.start()
    t2.start()
    
    t1.join()  # t1线程执行完毕,才继续往后走
    t2.join()  # t2线程执行完毕,才继续往后走
    
    print(number)
    

    这样子,由CPU调度两者的执行顺序,两者同时开始执行的话,会因为使用了相同的数字number而导致资源抢占。资源使用混乱了,比如add线程把number调过去,调去前数字为99,增加1,此时数字为100,在同时sub线程调用了number,也是99,减去1,再保存回去,此时的数字就,98,因为减法后保存的,用的是最后的值。(原本的值应该是99的,但是这样一来变为98了)
    解决办法:加锁

4.2 线程锁

lock_object = threading.RLock() 递归锁 可以多次申请和释放锁
lock_object = threading.Lock() 同步锁 不能多次申请锁和释放

import threading

lock_object = threading.RLock()

loop = 10000000
number = 0


def _add(count):
    lock_object.acquire() # 加锁
    global number
    for i in range(count):
        number += 1
    lock_object.release() # 释放锁


def _sub(count):
    lock_object.acquire() # 申请锁(等待)
    global number
    for i in range(count):
        number -= 1
    lock_object.release() # 释放锁


t1 = threading.Thread(target=_add, args=(loop,))
t2 = threading.Thread(target=_sub, args=(loop,))
t1.start()
t2.start()

t1.join()  # t1线程执行完毕,才继续往后走
t2.join()  # t2线程执行完毕,才继续往后走

print(number)
import threading

num = 0
lock_object = threading.RLock()


def task():
    print("开始")
    lock_object.acquire()  # 第1个抵达的线程进入并上锁,其他线程就需要再此等待。
    global num
    for i in range(1000000):
        num += 1
    lock_object.release()  # 线程出去,并解开锁,其他线程就可以进入并执行了
    print(num)


for i in range(2):
    t = threading.Thread(target=task)
    t.start()
  • 基于上下文管理器,在内部自动执行acquire 和release
import threading

num = 0
lock_object = threading.RLock()


def task():
    print("开始")
    with lock_object: # 基于上下文管理,内部自动执行 acquire 和 release
        global num
        for i in range(1000000):
            num += 1
    print(num)


for i in range(2):
    t = threading.Thread(target=task)
    t.start()

递归锁 RLock:

import threading
import time

lock_object = threading.RLock()


def task():
    print("开始")
    lock_object.acquire()
    lock_object.acquire()
    print(123)
    lock_object.release()
    lock_object.release()


for i in range(3):
    t = threading.Thread(target=task)
    t.start()

4.3 死锁:

由于资源竞争导致死锁,编程时避免:

import threading
import time 

lock_1 = threading.Lock()
lock_2 = threading.Lock()


def task1():
    lock_1.acquire()
    time.sleep(1)
    lock_2.acquire()
    print(11)
    lock_2.release()
    print(111)
    lock_1.release()
    print(1111)


def task2():
    lock_2.acquire()
    time.sleep(1)
    lock_1.acquire()
    print(22)
    lock_1.release()
    print(222)
    lock_2.release()
    print(2222)


t1 = threading.Thread(target=task1)
t1.start()

t2 = threading.Thread(target=task2)
t2.start()

这里的情况是:t1线程,t2线程定义锁的启动和解除,但是t1要求先解除1,后解除2, t2要求先解除2后解除1这就导致两者都解不开。

5.线程池

5.1 背景

Python3中官方才正式提供线程池。

线程不是开的越多越好,开的多了可能会导致系统的性能更低了,例如:如下的代码是不推荐在项目开发中编写。

不建议:无限制的创建线程。

5.2 线程池使用方式

import time
from concurrent.futures import ThreadPoolExecutor

# pool = ThreadPoolExecutor(100)
# pool.submit(函数名,参数1,参数2,参数...)


def task(video_url,num):
    print("开始执行任务", video_url)
    time.sleep(5)

# 创建线程池,最多维护10个线程。
pool = ThreadPoolExecutor(10)

url_list = ["www.xxxx-{}.com".format(i) for i in range(300)]

for url in url_list:
    # 在线程池中提交一个任务,线程池中如果有空闲线程,则分配一个线程去执行,执行完毕后再将线程交还给线程池;如果没有空闲线程,则等待。
    pool.submit(task, url,2)
    
print("END")
  • 等待线程池的任务执行完毕
import time
from concurrent.futures import ThreadPoolExecutor


def task(video_url):
    print("开始执行任务", video_url)
    time.sleep(5)


# 创建线程池,最多维护10个线程。
pool = ThreadPoolExecutor(10)

url_list = ["www.xxxx-{}.com".format(i) for i in range(300)]
for url in url_list:
    # 在线程池中提交一个任务,线程池中如果有空闲线程,则分配一个线程去执行,执行完毕后再将线程交还给线程池;如果没有空闲线程,则等待。
    pool.submit(task, url)

print("执行中...")
pool.shutdown(True)  # 等待线程池中的任务执行完毕后,在继续执行
print('继续往下走')

5.3 线程池与回调函数

如果要求在线程池管理的线程函数执行完毕后再干点其他的事情,就用到了回调函数(call_back),回调函数是在线程函数执行完毕后触发的。以下为示例:

import time
import random
from concurrent.futures import ThreadPoolExecutor, Future


def task(video_url):
    print("开始执行任务", video_url)
    time.sleep(2)
    return random.randint(0, 10)


def done(response):
    print("任务执行后的返回值", response.result())


# 创建线程池,最多维护10个线程。
pool = ThreadPoolExecutor(10)

url_list = ["www.xxxx-{}.com".format(i) for i in range(15)]

for url in url_list:
    # 在线程池中提交一个任务,线程池中如果有空闲线程,则分配一个线程去执行,执行完毕后再将线程交还给线程池;如果没有空闲线程,则等待。
    future = pool.submit(task, url)
    future.add_done_callback(done) # 是子主线程执行
    
# 可以做分工,例如:task专门下载,done专门将下载的数据写入本地文件。

6.单例模式

6.1 单例类的定义

在某种情况下,要求一个类只有一个对象,在后面每次定义这个对象时用的都是同一个对象,这就要求类的形式是单例类。
以后开发会遇到单例模式,每次实例化类的对象时,都是最开始创建的那个对象,不再重复创建对象。

6.2 实现方式:new 和init方法中定义

  • 简单的实现单例模式

    class Singleton:
        instance = None
    
        def __init__(self, name):
            self.name = name
            
        def __new__(cls, *args, **kwargs):
            # 返回空对象
            if cls.instance:
                return cls.instance
                # 就是说cls内默认有一个参数  instance,判断出这个参数是空的就说明没有定义过对象,如果是有数的就说明定义过
                # 如果定义过就不会在定义了,用之前的然后返回
            cls.instance = object.__new__(cls)
            return cls.instance
    
    obj1 = Singleton('alex')
    obj2 = Singleton('SB')
    
    print(obj1,obj2)
    
  • 多线程执行单例模式,有BUG

    import threading
    import time
    
    
    class Singleton:
        instance = None
    
        def __init__(self, name):
            self.name = name
    
        def __new__(cls, *args, **kwargs):
            if cls.instance:
                return cls.instance
            time.sleep(0.1)
              # bug在于如果多线程的方式执行,如果多个类都执行到这里,因为有 延时,所以在判断到没有对象时要新建一个对象,在等待0.1s时另一个线程也到了
              #另一个线程中因为没有检测到这个线程新建的实例,所以也认为是没有实例,所以也要执行新建一个实例,这样就新建了2个实例
            cls.instance = object.__new__(cls)
            return cls.instance
    
    
    def task():
        obj = Singleton('x')
        print(obj)
    
    
    for i in range(10):
        t = threading.Thread(target=task)
        t.start()
    
    
  • 加锁解决BUG

    import threading
    import time
    class Singleton:
        instance = None
        lock = threading.RLock()
        # 类中定义递归锁
    
        def __init__(self, name):
            self.name = name
            
        def __new__(cls, *args, **kwargs):
            with cls.lock:
                if cls.instance:
                    return cls.instance
                time.sleep(0.1)
                cls.instance = object.__new__(cls)
                # 只有当释放这个锁之后才会由其他的线程获得这个锁,才能够新建对象,所以每次新建都只有一个线程执行
            return cls.instance
    
    def task():
        obj = Singleton('x')
        print(obj)
    
    for i in range(10):
        t = threading.Thread(target=task)
        t.start()
    
  • 加判断,提升性能

    import threading
    import time
    class Singleton:
        instance = None
        lock = threading.RLock()
    
        def __init__(self, name):
            self.name = name
            
        def __new__(cls, *args, **kwargs):
    
            if cls.instance:
                return cls.instance
               # 因为只有在sleep时出现的问题,所以可以把sleep前的判断到有实例存在的部分拿到前面执行,如果有实例就不要申请锁和释放锁了
            with cls.lock:
                if cls.instance:
                    return cls.instance
                time.sleep(0.1)
                cls.instance = object.__new__(cls)
            return cls.instance
    
    def task():
        obj = Singleton('x')
        print(obj)
    
    for i in range(10):
        t = threading.Thread(target=task)
        t.start()
    
## 6.3 在模块实例化

```python
# utils.py

class Singleton:
    
    def __init__(self):
        self.name = "武沛齐"
        
    ...
        
single = Singleton()
from xx import single

print(single)

from xx import single
print(single)

7.多线程的TCP通讯

  • 服务端
import socket
import threading


def task(conn):
    while True:
        client_data = conn.recv(1024)
        data = client_data.decode('utf-8')
        print("收到客户端发来的消息:", data)
        if data.upper() == "Q":
            break
        conn.sendall("收到收到".encode('utf-8'))
    conn.close()


def run():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('127.0.0.1', 8001))
    sock.listen(5)
    while True:
        # 等待客户端来连接(主线程)
        # 每检测到一个连接就会创建一个子线程
        conn, addr = sock.accept()
        # 创建子线程
        t = threading.Thread(target=task, args=(conn,))
        t.start()
        
    sock.close()


if __name__ == '__main__':
    run()
  • 客户端
import socket

# 1. 向指定IP发送连接请求
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 8001))

while True:
    txt = input(">>>")
    client.sendall(txt.encode('utf-8'))
    if txt.upper() == 'Q':
        break
    reply = client.recv(1024)
    print(reply.decode("utf-8"))

# 关闭连接,关闭连接时会向服务端发送空数据。
client.close()
  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值