Python 多线程总结(2)— 线程锁、线程池、线程数量、互斥锁、死锁、线程同步
主要介绍使用 threading
模块创建线程的 3 种方式,分别为:
- 创建
Thread
实例函数 - 创建
Thread
实例可调用的类对象 - 使用
Thread
派生子类的方式
多线程是提高效率的一种有效方式,但是由于 CPython
解释器中存在 GIL
锁,因此 CPython
中的多线程只能使用单核。也就是说 Python
的多线程是宏观的多线程,而微观上实际依旧是单线程。
线程和进程之间有很多相似的地方,它们都是一个独立的任务。但是相比进程,线程要小的多。我们运行线程需要在进程中进行,而且线程和线程之间是共享内存的。相比进程的数据隔离,线程的安全性要更差一些。
1. Thread 实例-函数
使用 threading
模块创建一个 Thread
的实例,传递给它一个函数。
import threading
import timeloops = [4, 2]def loop(nloop, nsec):print 'start loop', nloop, 'at:', time.ctime()time.sleep(nsec)print 'end loop', nloop, 'at:', time.ctime()def main():print 'start main at:', time.ctime()threads = []nloops = range(len(loops))
# 实例化Thread即调用Thread()与调用start_new_thread()最大区别是:新的线程不会立即开始for i in nloops:t = threading.Thread(target=loop, args=(i, loops[i]))threads.append(t)# t.daemon = True python主程序只有在没有非守护线程的时候才会退出,设置# 线程是否随主线程退出而退出,默认为False# 所有线程创建之后,一起调用start()函数启动,而不是创建一个调用一个for i in nloops:threads[i].start() # join()会等到线程结束,或者在给了timeout参数的时候,等到超时为止
# join() 的作用是让主线程等待直到该线程执行完for i in nloops:threads[i].join() print 'end main at:', time.ctime()if __name__ == "__main__":main()
代码输出如下:
'''
start main at: Sat Jul 21 22:27:35 2018
start loop 0 at: Sat Jul 21 22:27:35 2018
start loop 1 at: Sat Jul 21 22:27:35 2018
end loop 1 at: Sat Jul 21 22:27:37 2018
end loop 0 at: Sat Jul 21 22:27:39 2018
end main at: Sat Jul 21 22:27:39 2018
'''
2. Thread 实例-可调用的类对象
创建一个 Thread
的实例,传递给它一个可调用的类对象
import threading
import timeloops = [4, 2]class ThreadFun(object):def __init__(self, func, args, name=''):self.name = nameself.func = funcself.args = argsdef __call__(self):apply(self.func, self.args)def loop(nloop, nsec):print 'start loop', nloop, 'at:', time.ctime()time.sleep(nsec)print 'end loop', nloop, 'at:', time.ctime()def main():print 'main is start at:', time.ctime()threads = []nloops = range(len(loops))for i in nloops:t = threading.Thread(target=ThreadFun(loop, (i, loops[i]), loop.__name__))# 该类在调用函数方面更加通用,并不局限于loop()函数threads.append(t)for i in nloops:threads[i].start()for i in nloops:threads[i].join()print 'main is end at:', time.ctime()if __name__ == "__main__":main()
3. Thread 派生子类
从 Thread
派生出一个子类,创建一个这个子类的实例
import threading
import timeloops = [4, 2]class MyThread(threading.Thread):def __init__(self, func, args, name=''):# super().__init__(name=name) # # 线程的名字threading.Thread.__init__(self)self.name = nameself.func = funcself.args = args# run 等同于之前 target 指定的函数def run(self):apply(self.func, self.args)def test(self):print("this is test")def loop(nloop, nsec):print 'start loop', nloop, 'at:', time.ctime()time.sleep(nsec)print 'end loop', nloop, 'at:', time.ctime()def main():print 'main is start at:', time.ctime()threads = []nloops = range(len(loops))for i in nloops:# 类似于 Thread(target=函数名) , 只会创建出一个线程t = MyThread(loop, [i, loops[i]], loop.__name__) threads.append(t)# MyThread 类中没有 start 方法,继承的父类 调用 start 方法,会自动调用 run 方法for i in nloops:threads[i].start()for i in nloops:threads[i].join()print 'main is end at:', time.ctime()t.test() # 这种方式不是多线程的方式!!!要在 run 方法里面调用 test 方法,才是多任务的方式
if __name__ == "__main__":main()
大家在用面向对象的方式,要注意类中除了 run
方法外,其他的方法,通过类的实例化去调用并不是多线程的方式。
除了用函数的方式,我们还可以用面向对象的方式来创建线程。这就需要我们手动继承 Thread
类,而且还需要实现其中的 run
方法,代码如下:
import time
from threading import Threadclass MyThread(Thread):def __init__(self):super().__init__()def run(self):time.sleep(1)print("我在运行")t = MyThread()
t.start()
print("我是主线程")
4. 使用线程锁来解决资源竞争
import threadinglock = threading.Lock()
some_var = 0class IncrementThread(threading.Thread):def run(self):global some_varlock.acquire() #read_value = some_varprint "some_var in %s is %d" % (self.name, read_value)some_var = read_value + 1print "some_var in %s after increment is %d" % (self.name, some_var)lock.release()def use_increment_thread():threads = []for i in range(50):t = IncrementThread()threads.append(t)t.start()for t in threads:t.join()print "After 50 modifications, some_var should have become 50"print "After 50 modifications, some_var is %d" % (some_var,)if __name__ == "__main__":use_increment_thread()
这里需要注意一点,我们两个函数/进程使用的是同一把锁,如果我们使用不同的锁还是会出现数据不安全的问题。
5. 线程池
池是用来保证计算机硬件安全的情况下,最大限度地利用计算机,它降低了程序的运行效率,但是保证了计算机硬件的安全,从而让你写的程序能够正常运行。
- 同步:提交任务之后原地等待任务的返回结果,期间不做任何事
- 异步:提交任务之后不等待任务的返回结果,执行继续往下执行
ThreadPoolExecutor
让线程的使用更加简单方便,减小了线程创建/销毁的资源损耗,无需考虑线程间的复杂同步,方便主线程与子线程的交互。
from concurrent.futures import ThreadPoolExecutor
import timedef get_html(times):time.sleep(times)print("get page {} success".format(times))return timesexecutor = ThreadPoolExecutor(max_workers=2)
task1 = executor.submit(get_html,(2))
task2 = executor.submit(get_html,(3))#done方法用来判断某个人物是否完成
print(task1.done())
time.sleep(5)
print(task2.done())
print(task1.cancel()
#result方法可以获取task返回值
print(task1.result())
线程池是从 Python 3.2 才被加入标准库中的 concurrent.futures
模块,相比 threading
模块,该模块通过 submit
返回的是一个 future
对象,通过它可以获取某一个线程的任务执行状态或返回值,另外futures
可以让多线程和多进程的编码接口一致,
from concurrent.futures import ThreadPoolExecutor
import time# 括号内可以传数字 不传的话默认会开设当前计算机 cpu 个数进程
pool = ThreadPoolExecutor(5) # 池子里面固定只有五个线程
"""
池子造出来之后 里面会固定存在五个线程
这个五个线程不会出现重复创建和销毁的过程
"""
def task(n):print(n)time.sleep(2)return n**n# pool.submit(task, 1) # 朝池子中提交任务 异步提交
# print("主")def call_back(n): # 回调处理数据的函数print('call_back>>>:',n.result()) # obj.result() 拿到的就是异步提交的任务的返回结果t_list = []
for i in range(10):res = pool.submit(task, i)# print(res.result()) # result 方法 同步提交# res = pool.submit(task, i).add_done_callback(call_back)# 将 res 返回的结果 <Future at 0x100f97b38 state=running>,交给回电函数 call_back 处理# 即 res 做实参传给 call_back 函数t_list.append(res)# 等待线程池中所有的任务执行完毕之后再继续往下执行
pool.shutdown() # 关闭线程池 等待线程池中所有的任务运行完毕
for t in t_list:print(">>>", t.result())
因为开启线程需要消耗一些时间,所以有时候我们会使用线程池来减少开启线程花费的时间。线程池的操作定义在 concurrent.futures.ThreadPoolExecutor
类中,下面我们来看看线程池如何使用:
import time
import threading
from concurrent.futures import ThreadPoolExecutordef func1():print(threading.current_thread().name, 'is running')def func2():for i in range(3):time.sleep(1)print(threading.current_thread().name, 'is running')pool = ThreadPoolExecutor(max_workers=2)
t1 = pool.submit(func2)
t2 = pool.submit(func1)
在代码中我们创建了一个容量为 2 的线程池,我们调用 pool.submit
函数就能使用线程池中的线程了。
总结
- 池子一旦造出来后,固定了线程或进程。
- 线程不会再变更,所有的任务都是这些线程处理。 这些线程不会再出现重复创建和销毁的过程。
- 任务的提交是异步的,异步提交任务的返回结果,应该通过回调机制来获取。
- 回调机制就相当于,把任务交给一个员工完成,它完成后主动找你汇报完成结果。
6. 查看线程数量
查看线程数量是通过 threading.enumerate()
方法来查看的。
import threading
import timedef test1():for i in range(5):print("--test1--%d"%i)time.sleep(1)def test2():for i in range(5):print("--test2--%d"%i)time.sleep(1)def main():t1 = threading.Thread(target=test1, name="t1")t2 = threading.Thread(target=test2)t1.start()t2.start()# 获取当前程序所有的线程print(threading.enumerate())if __name__ == "__main__":main()
输出结果:
--test1--0
--test2--0
[<_MainThread(MainThread, started 140076707002112)>, <Thread(t1, started 140076670510848)>, <Thread(Thread-1, started 140076662118144)>]
--test1--1
--test2--1
--test1--2
--test2--2
--test2--3
--test1--3
--test2--4
--test1--4
如果多次运行,会发现打印的顺序并不是一致的。因为线程的运行时没有先后顺序的,谁先抢到资源就先执行谁。
7. 线程其它方法
import os
import threading
from threading import active_count, current_thread
import timedef task():print("hello")print(os.getpid())print(current_thread().name)time.sleep(1)if __name__ == '__main__':t1 = threading.Thread(target=task, name="t1")t2 = threading.Thread(target=task, name="t2")t1.start()t1.join() # 等待线程执行结果后,主线程继续执行t2.start()print(os.getpid()) # 进程 IDprint(current_thread().name) # 获取线程名字print(active_count()) # 统计当前正在活跃的线程数量
join()
:等待线程执行结果后,主线程继续执行os.getpid()
:进程 IDcurrent_thread().name
:获取线程名字active_count()
:统计当前正在活跃的线程数量
8. 多个线程同时修改全局变量
import threading
import timenum = 0def test1(nums):global numfor i in range(nums):num += 1print("test1----num=%d" % num)def test2(nums):global numfor i in range(nums):num += 1print("test2----num=%d" % num)def main():t1 = threading.Thread(target=test1, args=(1000000,))t2 = threading.Thread(target=test2, args=(1000000,))t1.start()t2.start()time.sleep(5)print("main-----num=%d" % num)if __name__ == "__main__":main()
输出结果如下,当参数 args 变小时不会出现下面这种问题。
test1----num=1177810
test2----num=1476426
main-----num=1476426
当我们的线程 1 到 CPU 中执行代码 num+=1
的时候,其实这一句代码要被拆分为 3 个步骤来执行:
- 第一步:获取 num 的值
- 第二步:把获取的值 +1 操作
- 第三步:把第二步获取的值存储到 num 中
我们在 CPU
中执行这三步的时候,并不能保证这三部一定会执行结束,再去执行线程 2 中的代码。
因为这是多线程的,所以 CPU
在处理两个线程的时候,是采用雨露均沾的方式,可能在线程一刚刚将 num
值 +1
还没来得及将新值赋给 num
时,就开始处理线程二了,因此当线程二执行完全部的 num+=1
的操作后,可能又会开始对线程一的未完成的操作,而此时的操作停留在了完成运算未赋值的那一步,因此在完成对 num
的赋值后,就会覆盖掉之前线程二对 num
的 +1
操作。
那我们应该怎么解决这个问题?这就要用到我们接下来的知识——锁。
9. 互斥锁
当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制。线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。
互斥锁为资源引入一个状态——锁定/非锁定。
某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能改变,直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
基本使用:
#创建锁
mutex = threading.Lock()#锁定(加锁)
mutex.acquire()#解锁
mutex.release()
互斥锁解决资源竞争
import threading
import timenum = 0
# 创建一个互斥锁,默认是没有上锁的
mutex = threading.Lock()def test1(nums):global nummutex.acquire()for i in range(nums):num += 1mutex.release()print("test1----num=%d"%num)def test2(nums):global nummutex.acquire()for i in range(nums):num += 1mutex.release()print("test1----num=%d" % num)def main():t1 = threading.Thread(target=test1,args=(1000000,))t2 = threading.Thread(target=test2,args=(1000000,))t1.start()t2.start()time.sleep(2)print("main-----num=%d" % num)if __name__ == "__main__":main()
此时输出的结果是没有问题的。互斥锁也会引发一个问题,就是死锁。
10. 死锁
当多个线程几乎同一 时间的去修改某个共享数据的时候就需要我们进行同步控制,线程同步能够保证多个线程安全的访问竞争资源,我们最简单的就是引入互斥锁 Lock、递归锁 RLock。这两种类型的锁有一点细微的区别,
像下面这种情况,就容易出现死锁。互相锁住了对方,又在等对方释放资源。
import threading #Lock对象
lock = threading.Lock()
#A 线程
lock.acquire(a)
lock.acquire(b)#B 线程
lock.acquire(b)
lock.acquire(a)
当线程调用 lock
对象的 acquire()
方法时,lock
就会进入锁住状态,如果此时另一个线程想要获得这个锁,该线程就会变为阻塞状态,因为每次只能有一个线程能够获得锁,直到拥有锁的线程调用 lock
的 release()
方法释放锁之后,线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行状态。
这种情况比较容易被发现,还有一种情况不太容易被发现,调用其他加锁函数,也可能造成死锁。
def add(lock):global totalfor i in range(100000):lock.acquire()task()total += 1lock.release()def task():lock.acquire()# do somethinglock.release()
避免死锁:
- 程序设计上尽量避免
- 添加超时时间
import threading #RLock对象
rLock = threading.RLock()
rLock.acquire()
#在同一线程内,程序不会堵塞。
rLock.acquire()
rLock.release()
rLock.release()
RLock
允许在同一线程中被多次 acquire
,如果出现 Rlock
,那么 acquire
和 release
必须成对出现,即调用了 i
次 acquire
,必须调用 i
次的 release
才能真正释放所占用的锁。
11. 线程同步
11.1 condition 条件变量
condition
(条件变量):condition
有两把锁,一把底层锁会在线程底层调用 wait
后释放。我们每次调用 wait
时候回分配一把锁放到 condition
的等待队列中等待 notify
方法的唤醒。
import threading
class factory(threading.Thread):def __init__(self,cond):super(factory,self).__init__(name="口罩生产厂家")self.cond = conddef run(self):with self.cond:self.cond.wait()print("{}:生产了10万个口罩,快来拿".format(self.name))self.cond.notify()self.cond.wait()print("{}:又生产了100万个口罩发往武汉".format(self.name))self.cond.notify()self.cond.wait()print("{}:加油,武汉!".format(self.name))self.cond.notify()class wuhan(threading.Thread):def __init__(self,cond):super(wuhan,self).__init__(name="武汉志愿者")self.cond = conddef run(self):with self.cond:print("{}:能帮我们生产一批口罩吗?".format(self.name))self.cond.notify()self.cond.wait()print("{}:谢谢你们".format(self.name))self.cond.notify()self.cond.wait()print("{}:一起加油".format(self.name))self.cond.notify()self.cond.wait()if __name__=="__main__":lock = threading.Condition()factory = factory(lock)wuhan = wuhan(lock)factory.start()wuhan.start()
上面的代码,大家看到我用到 with
语句,这是因为 Condition
源码中实现了 __enter__
和 __exit__
,类中实现了这两个方法,就可以用 with
语句。而且 __enter__
调用了 acquire() 方法,在 __exit__
方法中调用了 release()
方法。
def __enter__(self):return self._lock.__enter__()def __exit__(self, *args):return self._lock.__exit__(*args)
11.2 semaphore 信号对象
semaphore
(信号对象):用于控制进入数量的锁,Semaphore
对象管理着一个计数器,当我们每次调用 acquire()
方法的时候会进行递减,而每个 release()
方法调用递增,计数器永远不会低于零,当 acquire()
发现计数器为零的时候线程阻塞等待其他线程调用 release()
,具体如一下示例:
import threading
import timeclass HtmlSpider(threading.Thread):def __init__(self, url, sem):super().__init__()self.url = urlself.sem = semdef run(self):time.sleep(2)print("got html text success")self.sem.release()class UrlProducer(threading.Thread):def __init__(self, sem):super().__init__()self.sem = semdef run(self):for i in range(20):self.sem.acquire()html_thread = HtmlSpider("https://baidu.com/{}".format(i), self.sem)html_thread.start()if __name__ == "__main__":sem = threading.Semaphore(3)url_producer = UrlProducer(sem)url_producer.start()
12. 线程间通信
Python
的 Queue
模块中提供了以下几种队列类:
FIFO
(先入先出) 队列Queue
LIFO
(后入先出)队列LifoQueue
- 优先级队列
Priority Queue
一般我们可以使用队列来实现线程同步,在开发中 FIFO
队列我们使用的比较多,下面我将用一个例子说明:
from threading import Thread
from time import sleep
from queue import Queue#生产者
def Producer():count =0while True:if queue.qsize()<1000:for i in range(100):count +=1msg = "生产商品"+str(count)queue.put(msg)print(msg)sleep(0.5)#消费者
def Consumer():while True:if queue.qsize()>100:for i in range(3):msg = "消费者消费了"+queue.get()print(msg)sleep(1)if __name__=="__main__":#定义一个队列queue = Queue();#初始化商品
for i in range(500):queue.put("初始商品"+str(i))#生产商品for i in range(4):p = Thread(target=Producer)p.start()#消费商品for i in range(10):c = Thread(target=Consumer)c.start()
队列对象(Queue、LifoQueue 或者 PriorityQueue)提供下列描述的公共方法。
Queue.qsize()
返回队列的大致大小。注意,qsize()> 0 不保证后续的 get() 不被阻塞,qsize() < maxsize 也不保证 put() 不被阻塞。Queue.empty()
如果队列为空,返回 True,否则返回 False。如果 empty() 返回 True,不保证后续调用的 put() 不被阻塞。类似的,如果 empty() 返回 False,也不保证后续调用的 get() 不被阻塞。Queue.full()
如果队列是满的返回 True,否则返回 False。如果 full() 返回 True 不保证后续调用的 get() 不被阻塞。类似的,如果 full() 返回 False 也不保证后续调用的 put() 不被阻塞。
Queue.put(item, block=True, timeout=None)
将 item 放入队列。如果可选参数 block 是 true 并且 timeout 是 None(默认),则在必要时阻塞至有空闲插槽可用。如果 timeout 是个正数,将最多阻塞 timeout 秒,如果在这段时间没有可用的空闲插槽,将引发 Full 异常。反之(block 是 false),如果空闲插槽立即可用,则把 item 放入队列,否则引发 Full 异常(在这种情况下,timeout 将被忽略)。Queue.put_nowait (item)
相当于 put(item, False)。Queue.get(block=True, timeout=None)
从队列中移除并返回一个项目。如果可选参数 block 是 true 并且 timeout 是 None(默认值),则在必要时阻塞至项目可得到。如果 timeout 是个正数,将最多阻塞 timeout 秒,如果在这段时间内项目不能得到,将引发 Empty 异常。反之(block 是 false),如果一个项目立即可得到,则返回一个项目,否则引发 Empty 异常(这种情况下,timeout 将被忽略)。
POSIX 系统 3.0 之前,以及所有版本的 Windows 系统中,如果 block 是 true 并且 timeout 是 None,这个操作将进入基础锁的不间断等待。这意味着,没有异常能发生,尤其是 SIGINT 将不会触发 KeyboardInterrupt 异常。
Queue.get_nowait()
相当于 get(False)。提供了两个方法,用于支持跟踪排队的任务是否被守护的消费者线程完整的处理。Queue.task_done()
表示前面排队的任务已经被完成。被队列的消费者线程使用。每个 get() 被用于获取一个任务,后续调用 task_done() 告诉队列,该任务的处理已经完成。
如果 join() 当前正在阻塞,在所有条目都被处理后,将解除阻塞(意味着每个 put() 进队列的条目的 task_done() 都被收到)。 如果被调用的次数多于放入队列中的项目数量,将引发 ValueError 异常。
Queue.join()
阻塞至队列中所有的元素都被接收和处理完毕。
在多线程通信中,Queue 扮演者重要的角色,一般添加数据到队列使用 put() 方法,在队列中取数据使用 get() 方法,后面针对 Queue 还会做进一步的讲解
其它参考
https://segmentfault.com/a/1190000014306740
一篇带你熟练使用多线程与原理
Python 多线程总结(2)— 线程锁、线程池、线程数量、互斥锁、死锁、线程同步相关推荐
- 问:为什么python中有了全局解释器锁GIL,还要有互斥锁?
首先我们在进行对比之前,我们要知道什么是全局解释器锁,和什么是互斥锁,他们分别是用来做什么的才能解决这个问题. 首先介绍全局解释解释器锁GIL,Python代码的执行由Python 虚拟机(也叫解释器 ...
- AUTOSAR-自旋锁(spinlock)与互斥锁
AUTOSAR多核OS为实现核间资源互斥,保证数据一致性,设计了自旋锁机制,该机制适用于核间资源互斥.对于多核概念,需要一种新的机制来支持不同内核上任务的互斥.这种新机制不应在同一内核上的 TASK ...
- golang map 锁_go 安全map 实现, 互斥锁和读写锁
###互斥锁 其中Mutex为互斥锁,Lock()加锁,Unlock()解锁,使用Lock()加锁后,便不能再次对其进行加锁,直到利用Unlock()解锁对其解锁后,才能再次加锁.适用于读写不确定场景 ...
- python 多线程和协程结合_一文讲透 “进程、线程、协程”
本文从操作系统原理出发结合代码实践讲解了以下内容: 什么是进程,线程和协程? 它们之间的关系是什么? 为什么说Python中的多线程是伪多线程? 不同的应用场景该如何选择技术方案? ... 什么是进程 ...
- 10月13日学习内容整理:线程,创建线程(threading模块),守护线程,GIL(全局解释器互斥锁)...
一.线程 1.概念:一条流水线的工作过程 2.和进程的区别和关系 (1)关系 >进程是资源单位,线程是执行单位,cpu真正执行的是线程 >一个进程至少有一个线程 >多线程针对的是一个 ...
- 进程互斥锁,队列,IPC进程间通信,生产者与消费者,线程,线程对象的属性,先行互斥锁...
进程互斥锁: 让并发变成串行, 牺牲了执行效率, 保证了数据安全.在程序并发执行时,需要修改数据时使用. 队列 队列:先进先出 相当于内存中产生一个队列空间,先进先出, 可以存放多个数据,但数 ...
- linux 信号量锁 内核,Linux内核信号量互斥锁应用
主要介绍了Linux 内核关于信号量,互斥锁等的应用 内核同步机制-信号量/互斥锁/读-写信号量 sema ,mutex ,rwsem 信号量 通用信号量 用户类进程之间使用信号量(semaphore ...
- python并发编程--进程、线程、协程、锁、池、队列
文章目录 操作系统的概念 进程 multiprocessing模块 守护进程 使用多进程实现一个并发的socket的server 锁 生产者消费者模型 数据共享 线程threading模块 守护线程和 ...
- python线程池阻塞队列_福利又来啦!python多线程进阶篇
使用Python中的线程模块,能够同时运行程序的不同部分,并简化设计.如果你已经入门Python,并且想用线程来提升程序运行速度的话,希望这篇教程会对你有所帮助. 通过阅读本文,你将了解到:什么是死锁 ...
- python多线程同步与互斥_python多线程编程(3): 使用互斥锁同步线程
问题的提出 上一节的例子中,每个线程互相独立,相互之间没有任何关系.现在假设这样一个例子:有一个全局的计数num,每个线程获取这个全局的计数,根据num进行一些处理,然后将num加1.很容易写出这样的 ...
最新文章
- Python学习之序列
- Linux命令中的rpm安装命令
- [转]sqlserver转换为Mysql工具使用
- SiftGPU:编译SiftGPU出现问题-无法解析的外部符号 glutInit
- 阿里云前端周刊 - 第 14 期
- Spring集成Mybatis配置映射文件方法详解
- 交易所行情报盘程序配置
- 深度linux内核升级,深度操作系统 2020.11.11 更新发布:内核升级
- pytorch1.4+tensorboard不显示graph计算图的问题
- matlab2c使用c++实现matlab函数系列教程-polyder函数
- LIBSVM在Matlab下的使用
- 工作后,成长速度是如何产生差异的?
- Luogu1091 合唱队形
- ssh中关于antion取jsp传递的值接收不到
- php框架laravel win10,composer 安装Laravel (win10)
- 爬取网页表格到Excel ?别再复制粘贴了,太慢!
- 客户案例|围观!卡耐基梅隆大学用上中国造?要玩转自主导航机器人领域?
- oracle10g windows7
- Vue实现待办事件列表
- js将汉字转为相应的拼音