线程进程协程

一、多任务
  • 多任务就是同一时间内运行多个程序

    单核cpu多任务原理:操作系统轮流让各个任务交替执行,QQ执行2us,切换到微信执行2us,CPU调度执行速度太快了,导致我们感觉就像所有任务都在同时执行一样。

  • 多核CPU实现多任务原理:真正的执行多任务只能在多核CPU上实现,但是由于任务数量远远大于CPU的核心数量,所以操作系统也会自动把很多任务轮流调度到每个核心上执行。

  • 并行和并发
    • 并发:当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间 段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状,这种方式我们称之为并发(Concurrent)
    • 并行:当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。
  • 实现多任务的方式:

    • 多进程模式
    • 多线程模式
    • 协程
二、进程
  • 进程是一个正在运行的程序,进程是系统进行资源分配的基本单位,进程拥有自己的内存空间,资源消耗大, 系统整体开销大, 数据通信不方便。

  • 进程效率低的原因:

    • 进程之间不共享状态,进程之间由操作系统完成,每个进程都有自己的独立内存空间。
    • 进程之间的通讯主要是通过信号传递的,实现的方式由多种,信号量,管道,事件等,任何一种方式的通讯方式都要通过内核,所以通讯效率比较低。

    • 由于是独立的内存空间,上下文切换要需要保存先调用栈的信息,CPU寄存器的信息,虚拟内存,以及相关的句柄等信息,所有导致上下文切换开销很大,通讯麻烦。

  • 进程执行开销大,但是有利于资源 的管理和保护,进程可以跨机器迁移,操作系统能同时运行进程数目有限

三、线程
  • 线程是操作系统能够运行运算调度的最小单位,线程是属于进程的,不能单独存在,是进程中的实际运作单位,也叫执行路径。 资源消耗小, 可共享数据。上下文开销大。按时间片强制切换, 不够灵活

  • 一条线程指的是进程中一个单一顺序的控制流,一个程序可以并发多个线程,每条线程执行不同的任务。

  • 一个线程是一个execution context (执行上下文) 即一个cpu执行时所需要的一串指令

  • 操作系统通过给不同的线程分配时间片(CPU运行时长)来调度线程,当CPU执行完一个线程的时间片后就会快速切换到下一个线程,

  • 线程特点:

    • 多线程适合在IO密集型操作(读写数据操作较多的,如爬虫)

    • 多个线程数据共享内存(数据共享和全局变量)

进程和线程的区别:

进程是操作系统进行资源分配和调度的一个独立单位.

线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.

- 区别:
  - 一个程序至少有一个进程,一个进程至少有一个线程.
  - 线程的划分尺度小于进程(资源比进程少),使得多线程程序的并发性高。
  - 进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率
  - 线程不能够独立执行,必须依存在进程中
- 优缺点
  - 线程执行开销小,但不利于资源的管理和保护;而进程正相反。
  
 GIL
 线程全局锁 (Global Interpreter Lock), 就是 CPython 为了保证线程安全而采取的独立
线程运行的限制 ,说白了就是一个核只能在同一时间运行一个线程
四、协程
  • 用户态的轻量级线程,调度完全由用户控制,可根据事件切换,。拥有自己的寄存器上下文和栈,切换基本没有内核切换的开销,切换灵活。内存开销更小, 上下文切换开销更小。
五、进程池
  • 进程池作用:通过维护一个进程池来控制进程数目
  • 开多进程的目的是为了并发,如果有多核,通常有几个核就开几个进程,进程开启过多,效率反而会下降,但很明显需要并发执行的任务要远大于核数,这时我们就可以通过维护一个进程池来控制进程数目。
  • 进程池Pool可以提供指定数量的进程,供用户调用,当有新的请求提交到pool中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到规定最大值,那么该请求就会等待,直到池中有进程结束,就重用进程池中的进程。
六、进程创建

linux下可以使用fork函数创建进程,在windows系统上可以引入multiprocessing模块,创建进程。我们可以使用multiprocessing模块中Process类创建新的进程

Process类说明
方法名称参数功能
init()name:进程名称
args:任意位置参数
kwargs:任意关键字参数
target:进程实例所调动的对象
group:一般用不到
构造方法
start()启动进程
terminate()结束进程
join()timeout:等等秒数,可选是否阻塞主进程
run()如果没有给定target参数,
对这个对象调用start()方法时,
就将执行对象中的run()方法
is_alive()判断进程实例是否还在执行
close()释放资源,线程执行或睡眠会报错
常用属性说明
name进程名称
pid当前进程的PID
import multiprocessing
import time

def child(interval):
    print("子进程开始")
    time.sleep(interval)
    print("子进程结束")

if __name__ == "__main__":
	print("父进程开始")
    p1 = multiprocessing.Process(target = child, args = (1,))
    p1.start()

    print("cpu数目:" + str(multiprocessing.cpu_count()))
    for p in multiprocessing.active_children():
        print("子进程名称:p.name:" + p.name + "\tp.id" + str(p.pid))
    print("父进程结束")
  • 进程间不能共享数据
import os
from multiprocessing import Process,Pool
from time import sleep
from random import randint

num = 100
def work():
    print("子进程开始")
    global num
    num += 1
    print("子进程:num=",num)
    print("子进程结束")
if __name__ == "__main__":
    print("主进程开始")
    p = Process(target=work)
    p.start()
    p.join()
    print("主进程:num=", num)
    print("主进程结束")
进程同步

临界资源(临界区):指一次只能允许一个进程使用的共享资源称为临界资源;

同步:指为完成某种任务而建立的两个或多个进程,这些进程在合作的过程中需要协调工作次序进行有序的访问而出现等待所产生的制约关系。

互斥:指两个或多个进程访问临界资源时只能一个进程访问,其他进程等待的一种相互制约的关系。

import time
# 文件内容是一个数字:100
def shakedown(fileName,lock):
    with lock:
        with open(fileName,'r') as fp:
            num = int(fp.read().strip())
            num -= 1
            print("当前票数:{} ".format(num))
            time.sleep(3)
        with open(fileName,'w') as fp:
            fp.write(str(num))

if __name__ == '__main__':
    from multiprocessing import Process,Lock
    lock = Lock()
    for i in range(5):
        p = Process(target=shakedown,args=('1.txt',lock))
        p.start()
进程通信

进程之间是隔离的,要实现数据共享需要通过进程通信实现,进程通信可以通过队列和管道进行。队列Queue用于创建共享的进程队列,Queue是多进程安全的队列,可以使用Queue实现多进程之间的数据传递。

q.put方法用以插入数据到队列中
put方法还有两个可选参数:blocked和timeout。
如果blocked为True(默认值),并且timeout为正值,该方法会阻塞timeout指定的时间,直到该队列有剩余的空间,如果超时,会抛出Queue.Full异常。如未设timeout,则一直阻塞
如果blocked为False,但该Queue已满,会立即抛出Queue.Full异常。

q.get方法可以从队列读取并且删除一个元素。
get方法有两个可选参数:blocked和timeout。
如果blocked为True(默认值),并且timeout为正值,那么在等待时间内没有取到任何元素,会抛出Queue.Empty异常。如未设timeout,则会一直阻塞
如果blocked为False,如果队列为空,则立即抛出Queue.Empty异常.

q.empty():调用此方法时q为空则返回True,该结果不可靠,比如在返回True的过程中,如果队列中又加入了项目。
q.full():调用此方法时q已满则返回True,该结果不可靠,比如在返回True的过程中,如果队列中的项目被取走。
q.qsize():返回队列中目前项目的正确数量,结果也不可靠,比如在返回size的过程种,该队列的项目加入或取走
  • 队列
from multiprocessing import Queue,Process
from time import sleep
from random import randint
q = Queue(3)

def consume(q):
   while 1:
       sleep(randint(1,3))
       res = q.get()
       if  res:
           print("消费者拿到了:{}".format(res))
       else:
           break

def product(q):
   for i in range(5):
       sleep(randint(1, 3))
       print("生产者生产了:包子%d"%i)
       q.put("包子%d"%i)

if __name__ == "__main__":
   q = Queue()
   c = Process(target=consume,args=(q,))
   c.start()
   product(q)
   q.put(None)
   c.join()
   print("main")
  • 管道
Pipe([duplex]):在进程之间创建一条管道,并返回元组(conn1,conn2),其中conn1,conn2表示管道两端的连接对象,强调一点:必须在产生Process对象之前产生管道
参数:dumplex默认管道是全双工的,如果将duplex射成False,conn1只能用于接收,conn2只能用于发送。
返回值:
    conn1.recv():接收conn2.send(obj)发送的对象。如果没有消息可接收,recv方法会一直阻塞。如果连接的另外一端已经关闭,那么recv方法会抛出EOFError。
    conn1.send(obj):通过连接发送对象。obj是与序列化兼容的任意对象
进程池

开多进程的目的是为了并发,如果有多核,通常有几个核就开几个进程,进程开启过多,效率反而会下降,但很明显需要并发执行的任务要远大于核数,这时我们就可以通过维护一个进程池来控制进程数目。

进程池Pool可以提供指定数量的进程,供用户调用,当有新的请求提交到pool中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到规定最大值,那么该请求就会等待,直到池中有进程结束,就重用进程池中的进程。

Pool(self,[processes  [,initializer [, initargs]]]):创建进程池
参数:
    processes:要创建的进程数,如果省略,将默认使用cpu_count()的值
    initializer:是每个工作进程启动时要执行的可调用对象,默认为None
    initargs:是要传给initializer的参数组
返回值:进程池对象
常见方法
   close()  关闭进程池
   terminate() 立即终止所有工作进程,同时不执行任何清理或结束任何挂起工作。
   join() 等所有进程退出,这个方法只能在close或terminate后执行
  • apply_async非阻塞模式,池子中进程异步运行

    apply_async(self, func, args=(), kwds={}, callback=None,error_callback=None)

    #func你要传的任务callback回调函数3

  • apply 阻塞模式,池子中进程同步执行,也就是一个进程执行完毕后再执行下一个进程

import os
from multiprocessing import Process,Pool
from time import sleep
from random import randint

def worker(num):
    print("进程{}开始".format(num))
    sleep(randint(1,3))
    print("进程{}结束".format(num))
if __name__ == "__main__":
    pool = Pool(3)
    for x in range(5):
        pool.apply_async(worker,(x,))
    pool.close()
    pool.join()
    print("主进程结束")
七、线程创建

线程,有时被称为轻量进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程有三种基本状态:就绪、阻塞和运行。就绪状态是指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;运行状态是指线程占有处理机正在运行;阻塞状态是指线程在等待一个事件(如某个信号量),逻辑上不可执行。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。

线程是进程内有一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指令运行时的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程

多线程:多线程(英语:multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理(Chip-level multithreading)或同时多线程(Simultaneous multithreading)处理器。 在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理(Multithreading)”。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程(台湾译作“执行绪”),进而提升整体处理性能

  • 优点:
    • 使用线程可以把占据长时间的程序中的任务放到后台去处理。
    • 用户界面可以更加吸引人,这样比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度
    • 程序的运行速度可能加快
    • 在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,线程就比较有用了。在这种情况下我们可以释放一些珍贵的资源如内存占用等等。

Python通过两个标准库thread和threading提供对线程的支持。thread提供了低级别的、原始的线程以及一个简单的锁。

threading 模块提供的其他方法:

  • threading.currentThread(): 返回当前的线程变量。
  • threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
  • threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate()有相同的结果。

除了使用方法外,线程模块同样提供了Thread类来处理线程,Thread类提供了以下方法:

  • run(): 用以表示线程活动的方法。
  • start():启动线程活动。
  • join([time]): 等待至线程中止。
  • isAlive() 返回线程是否活动的。
  • getName():返回线程名。
  • setName():设置线程名。
线程创建

Thread(self, group=None, target=None, name=None, args=(), kwargs=None, *, daemon=None))

参数:

​ target:线程执行体

args:任意位置参数

kwargs:任意关键字参数

import threading
from random import randint
from time import sleep

def run(name):
    print("{}线程开始".format(name))
    sleep(randint(1,3))
    print("{}线程结束".format(name))

if __name__ == "__main__":
    print("主线程:{}".format(threading.current_thread().name))
    t1 = threading.Thread(target=run,args=("线程1",))
    t1.start()
    t1.join()
    print("主线程结束")
  • 线程之间数据共享

    多线程和多进程最大的不同在于,多进程中,同一个全局变量,每个子进程各自有一份拷贝,互不影响。而在多线程中,所有变量都由线程共享,所以任何一个变量都可以被任意线程所修改。因此,多个线程同时改变一个变量,容易把内容改乱了

    import threading
    import random
    import time
    
    money = 1000
    
    def run1():
        global money
        for i in range(100):
            money -= 1
    
    def run2():
        global money
        for i in range(100):
            money -= 1
    
    if __name__ == "__main__":
        #创建线程
        th1 = threading.Thread(target=run1, name="th1")
        th2 = threading.Thread(target=run2, name="th2")
    
        #启动
        th1.start()
        th2.start()
    
        #等待子线程结束
        th1.join()
        th2.join()
    
        print("money = %d"%money)
    
线程同步
import threading
import random
import time

list1 = [0]*10
def run1():
    global list1
    #获取线程锁,如果已上锁,则阻塞等待锁的释放
    for i in range(len(list1)):
        list1[i] = 1
        time.sleep(0.2)
def run2():
    global list1
    for i in range(len(list1)):
        print(list1[i],end=' ')
        time.sleep(0.2)
if __name__ == "__main__":
    #创建线程
    th1 = threading.Thread(target=run1,  name="th1")
    th2 = threading.Thread(target=run2,  name="th2")

    #启动
    th1.start()
    th2.start()

    #等待子线程结束
    th1.join()
    th2.join()
    print("\nlist1 = {}".format(list1))

问题产生的原因就是没有控制多个线程对同一资源的访问,对数据造成破坏,使得线程运行的结果不可预期。这种现象称为“线程不安全”。

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

多线程的优势在于可以同时运行多个任务(至少感觉起来是这样)。但是当线程需要共享数据时,可能存在数据不同步的问题。为了避免这种情况,引入了锁的概念。

import threading
import random
import time
#线程锁
lock = threading.Lock()

list1 = [0]*10

def run1():
    global list1

    #获取线程锁,如果已上锁,则阻塞等待锁的释放
    lock.acquire()
    for i in range(len(list1)):
        list1[i] = 1
        time.sleep(0.2)
    lock.release()

def run2():
    global list1
    with lock:
        for i in range(len(list1)):
            print(list1[i],end=' ')
            time.sleep(0.2)

if __name__ == "__main__":
    #创建线程
    th1 = threading.Thread(target=run1,  name="th1")
    th2 = threading.Thread(target=run2,  name="th2")

    #启动
    th1.start()
    th2.start()

    #等待子线程结束
    th1.join()
    th2.join()

    print("\nlist1 = {}".format(list1))
  • ThreadLocal

    一个ThreadLocal变量虽然是全局变量,但每个线程都只能读写自己线程的独立副本,互不干扰。ThreadLocal解决了参数在一个线程中各个函数之间互相传递的问题

    import threading
    #创建全局对象global_data
    global_data = threading.local()
    def show():
        print(threading.current_thread().getName(), global_data.num)
    def thread_cal():
    	# num是当前线程的局部变量
        global_data.num = 0
        for _ in range(1000):
            global_data.num += 1
        show()
    
    if __name__ == "__main__":
        threads = []
        for i in range(10):
            threads.append(threading.Thread(target=thread_cal))
            threads[i].start()
        for i in range(10):
            threads[i].join()
        print("Main thread: ", global_data.__dict__ ) # {}
    
    
  • 生产者和消费者

Python的Queue模块中提供了同步的、线程安全的队列类,包括FIFO(先入先出)队列Queue,LIFO(后入先出)队列LifoQueue,和优先级队列PriorityQueue。这些队列都实现了锁原语(可以理解为原子操作,即要么不做,要么就做完),能够在多线程中直接使用。可以使用队列来实现线程间的同步。

import threading
import queue
import random
import time

def produce(q):
    i = 0
    while i < 10:
        num = random.randint(1, 100)
        q.put("生产者产生数据:%d"%num)
        print("生产者产生数据:%d"%num)
        time.sleep(1)
        i += 1
    q.put(None)
    # 完成任务
    q.task_done()


def consume(q):
    while True:
        item = q.get()
        if item is None:
            break
        print("消费者获取到:%s"%item)
        time.sleep(4)
    # 完成任务
    q.task_done()


if __name__ == "__main__":
    q = queue.Queue(10)
    arr = []

    #创建生产者
    th = threading.Thread(target=produce, args=(q, ))
    th.start()

    # 创建消费者
    tc = threading.Thread(target=consume, args=(q, ))
    tc.start()


    th.join()
    tc.join()
    print("END")
条件变量

有些复杂问题互斥锁搞不定了。Python提供的Condition对象提供了对复杂线程同步问题的支持。Condition被称为条件变量,除了提供与Lock类似的acquirerelease方法外,还提供了waitnotify方法。

import threading
# 可传入一个互斥锁或者可重入锁
cond = threading.Condition()
实例方法:
acquire([timeout])/release(): 线程锁/释放锁 
wait([timeout]):  线程挂起,直到收到一个notify通知或者超时(可选的,浮点数,单位是秒s)
                 才会被唤醒继续运行。wait()必须在已获得Lock前提下才能调用,否则会触发RuntimeError。 
notify(n=1): 通知其他线程,那些挂起的线程接到这个通知之后会开始运行,
		默认是通知一个正等待该condition的线程,最多则唤醒n个等待的线程。notify()必须在已获得Lock前提
		下才能调用,否则会触发RuntimeError。notify()不会主动释放Lock。
notifyAll(): 通知所有线程,这些线程都将进入锁定池尝试获得锁定。
             调用这个方法不会释放锁定。使用前线程必须已获得锁定,否则将抛出异常。
import threading
import time

con = threading.Condition()
num = 0  #鱼丸数量

# 生产者
def produce():
    # 锁定线程
    global num
    con.acquire()
    while True:
        print("开始添加!!!")
        num += 1
        print("火锅里面鱼丸个数:%s" % str(num))
        time.sleep(1)
        if num >= 5:
            print("火锅里面里面鱼丸数量已经到达5个,无法添加了!")
            # 唤醒等待的线程
            con.notify()  # 唤醒小伙伴开吃啦
            # 等待通知
            con.wait()
    # 释放锁
    con.release()

# 消费者
def  consume():
    con.acquire()
    global num
    while True:
        print("开始吃啦!!!")
        num -= 1
        print("火锅里面剩余鱼丸数量:%s" % str(num))
        time.sleep(2)
        if num <= 0:
            print("锅底没货了,赶紧加鱼丸吧!")
            con.notify()  # 唤醒其它线程
            # 等待通知
            con.wait()
    con.release()
if __name__ == "__main__":
    p = threading.Thread(target=produce)
    c = threading.Thread(target=consume)
    p.start()
    c.start()
    p.join()
    c.join()

定时线程
import time
import threading
def run():
    print("啦啦,啦啦,我是卖报的小行家")
if __name__ == "__main__":
    # 进程启动5秒后在执行
    th = threading.Timer(5, run)
    th.start()
    th.join()

信号量
import time
from threading import Thread, Semaphore, current_thread

def task(s):
    # s.acquire()  # 减1
    with s:
        for i in range(5):
            print('{}扫地....{}'.format(current_thread().name, i))
            time.sleep(1)
    # s.release()  # 加1


if __name__ == '__main__':
    s = Semaphore(4)
    for i in range(10):
        t = Thread(target=task, args=(s,))
        t.start()
        
# 线程的信号量:Semaphore ,其实也是控制线程个数
  信号量的实现方式:
  在内部有一个counter计数器,每当我们 s.acquire()一次,计数器就进行减1处理
  每当 s.release()一次,计数器就进行加1处理,当计数器为0的时候其他线程的就处于
  等待的状态counter的值就是同一时间可以开启线程的个数
  建议使用with    
死锁
死锁:两把锁
    申请锁的顺序使用不当
开发过程中使用线程,在线程间共享多个资源的时候,
如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。
尽管死锁很少发生,但一旦发生就会造成应用的停止响应,程序不做任何事情。

避免死锁:
1. 重构代码
2. 使用timeout参数
八、协程
import time
import gevent 
from gevent import monkey

monkey.patch_all()  #猴子补丁  
#把标准库中的thread/socket等给替换掉.这样我们在后面使用socket的时候可以跟平常一样使用,无需修改任何代码,但是它变成非阻塞的了
def eat():    
	for i in range(5):        
        print('坤坤喜欢吃肉饼...')        
        time.sleep(0.1)
def listen_music():    
	for i in range(5):        
		print('坤坤喜欢听麻婆豆腐..', i)        
		time.sleep(0.1)
if __name__ == '__main__':   
    g1 = gevent.spawn(eat)    
    g2 = gevent.spawn(listen_music)    
    g1.join()    
    g2.join()    
    print('----over---')
    
#  gevent中用到的主要模式是greenlet,它是以C扩展模块形式接入Python的轻量级协程。 greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。
五、python性能之困
  • 计算密集型

    • CPU长时间满负荷运行,如图像处理、圆周率计算等-
    • 计算密集型:用C语言补充
    • Profiletimeit 工具用来测试代码效率
  • I/O密集型 :网络IO、文件IO、设备IO等

    • 多线程/多进程/协程
    • 阻塞 >非阻塞
    • 同步 >异步

1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值