主要介绍使用 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() :进程 ID
  • current_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 就会进入锁住状态,如果此时另一个线程想要获得这个锁,该线程就会变为阻塞状态,因为每次只能有一个线程能够获得锁,直到拥有锁的线程调用 lockrelease() 方法释放锁之后,线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行状态。

这种情况比较容易被发现,还有一种情况不太容易被发现,调用其他加锁函数,也可能造成死锁。

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 ,那么 acquirerelease 必须成对出现,即调用了 iacquire ,必须调用 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. 线程间通信

PythonQueue 模块中提供了以下几种队列类:

  • 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)— 线程锁、线程池、线程数量、互斥锁、死锁、线程同步相关推荐

  1. 问:为什么python中有了全局解释器锁GIL,还要有互斥锁?

    首先我们在进行对比之前,我们要知道什么是全局解释器锁,和什么是互斥锁,他们分别是用来做什么的才能解决这个问题. 首先介绍全局解释解释器锁GIL,Python代码的执行由Python 虚拟机(也叫解释器 ...

  2. AUTOSAR-自旋锁(spinlock)与互斥锁

    AUTOSAR多核OS为实现核间资源互斥,保证数据一致性,设计了自旋锁机制,该机制适用于核间资源互斥.对于多核概念,需要一种新的机制来支持不同内核上任务的互斥.这种新机制不应在同一内核上的 TASK ...

  3. golang map 锁_go 安全map 实现, 互斥锁和读写锁

    ###互斥锁 其中Mutex为互斥锁,Lock()加锁,Unlock()解锁,使用Lock()加锁后,便不能再次对其进行加锁,直到利用Unlock()解锁对其解锁后,才能再次加锁.适用于读写不确定场景 ...

  4. python 多线程和协程结合_一文讲透 “进程、线程、协程”

    本文从操作系统原理出发结合代码实践讲解了以下内容: 什么是进程,线程和协程? 它们之间的关系是什么? 为什么说Python中的多线程是伪多线程? 不同的应用场景该如何选择技术方案? ... 什么是进程 ...

  5. 10月13日学习内容整理:线程,创建线程(threading模块),守护线程,GIL(全局解释器互斥锁)...

    一.线程 1.概念:一条流水线的工作过程 2.和进程的区别和关系 (1)关系 >进程是资源单位,线程是执行单位,cpu真正执行的是线程 >一个进程至少有一个线程 >多线程针对的是一个 ...

  6. 进程互斥锁,队列,IPC进程间通信,生产者与消费者,线程,线程对象的属性,先行互斥锁...

    进程互斥锁: 让并发变成串行, 牺牲了执行效率, 保证了数据安全.在程序并发执行时,需要修改数据时使用. 队列 队列:先进先出 ​ 相当于内存中产生一个队列空间,先进先出, ​ 可以存放多个数据,但数 ...

  7. linux 信号量锁 内核,Linux内核信号量互斥锁应用

    主要介绍了Linux 内核关于信号量,互斥锁等的应用 内核同步机制-信号量/互斥锁/读-写信号量 sema ,mutex ,rwsem 信号量 通用信号量 用户类进程之间使用信号量(semaphore ...

  8. python并发编程--进程、线程、协程、锁、池、队列

    文章目录 操作系统的概念 进程 multiprocessing模块 守护进程 使用多进程实现一个并发的socket的server 锁 生产者消费者模型 数据共享 线程threading模块 守护线程和 ...

  9. python线程池阻塞队列_福利又来啦!python多线程进阶篇

    使用Python中的线程模块,能够同时运行程序的不同部分,并简化设计.如果你已经入门Python,并且想用线程来提升程序运行速度的话,希望这篇教程会对你有所帮助. 通过阅读本文,你将了解到:什么是死锁 ...

  10. python多线程同步与互斥_python多线程编程(3): 使用互斥锁同步线程

    问题的提出 上一节的例子中,每个线程互相独立,相互之间没有任何关系.现在假设这样一个例子:有一个全局的计数num,每个线程获取这个全局的计数,根据num进行一些处理,然后将num加1.很容易写出这样的 ...

最新文章

  1. Python学习之序列
  2. Linux命令中的rpm安装命令
  3. [转]sqlserver转换为Mysql工具使用
  4. SiftGPU:编译SiftGPU出现问题-无法解析的外部符号 glutInit
  5. 阿里云前端周刊 - 第 14 期
  6. Spring集成Mybatis配置映射文件方法详解
  7. 交易所行情报盘程序配置
  8. 深度linux内核升级,深度操作系统 2020.11.11 更新发布:内核升级
  9. pytorch1.4+tensorboard不显示graph计算图的问题
  10. matlab2c使用c++实现matlab函数系列教程-polyder函数
  11. LIBSVM在Matlab下的使用
  12. 工作后,成长速度是如何产生差异的?
  13. Luogu1091 合唱队形
  14. ssh中关于antion取jsp传递的值接收不到
  15. php框架laravel win10,composer 安装Laravel (win10)
  16. 爬取网页表格到Excel ?别再复制粘贴了,太慢!
  17. 客户案例|围观!卡耐基梅隆大学用上中国造?要玩转自主导航机器人领域?
  18. oracle10g windows7
  19. Vue实现待办事件列表
  20. js将汉字转为相应的拼音

热门文章

  1. Windows中配置java变量环境
  2. 预见未来丨机器学习:未来十年研究热点
  3. tf.variance_scaling_initializer() tensorflow学习:参数初始化
  4. 同步与异步,阻塞与非阻塞的区别
  5. 理解 Word2Vec 之 Skip-Gram 模型
  6. 传统编译器与神经网络编译器
  7. deeplearning搜索空间
  8. CodeGen按钮循环
  9. 边缘的人工智能可以满足许多需求
  10. 什么样的技术将在后大流行的世界里兴起