[.Net线程处理系列]专题二:线程池中的工作者线程
目录:
一、上节补充
二、CLR线程池基础
三、通过线程池的工作者线程实现异步
四、使用委托实现异步
五、任务
六、小结
一、上节补充
对于Thread类还有几个常用方法需要说明的。
1.1 Suspend和Resume方法
这两个方法在.net Framework 1.0 的时候就支持的方法,他们分别可以挂起线程和恢复挂起的线程。但在.net Framework 2.0以后的版本中这两个方法都过时了,MSDN的解释是这样:
警告:
不要使用 Suspend 和 Resume 方法来同步线程的活动。您无法知道挂起线程时它正在执行什么代码。如果您在安全权限评估期间挂起持有锁的线程,则 AppDomain中的其他线程可能被阻止。如果您在线程正在执行类构造函数时挂起它,则 AppDomain中尝试使用该类的其他线程将被阻止。这样很容易发生死锁。
对于这个解释可能有点抽象吧,让我们来看看一段代码可能会清晰点:
- class Program
- {
- static void Main(string[] args)
- {
- // 创建一个线程来测试
- Thread thread1 = new Thread(TestMethod);
- thread1.Name = "Thread1";
- thread1.Start();
- Thread.Sleep(2000);
- Console.WriteLine("Main Thread is running");
- int b = 0;
- int a = 3 / b;
- Console.WriteLine(a);
- thread1.Resume();
- Console.Read();
- }
- private static void TestMethod()
- {
- Console.WriteLine("Thread: {0} has been suspended!", Thread.CurrentThread.Name);
- //将当前线程挂起
- Thread.CurrentThread.Suspend();
- Console.WriteLine("Thread: {0} has been resumed!", Thread.CurrentThread.Name);
- }
- }
上面一段代码还存在一个隐患,请看下面一小段代码:
- class Program
- {
- static void Main(string[] args)
- {
- // 创建一个线程来测试
- Thread thread1 = new Thread(TestMethod);
- thread1.Name = "Thread1";
- thread1.Start();
- Console.WriteLine("Main Thread is running");
- thread1.Resume();
- Console.Read();
- }
- private static void TestMethod()
- {
- Console.WriteLine("Thread: {0} has been suspended!", Thread.CurrentThread.Name);
- Thread.Sleep(1000);
- //将当前线程挂起
- Thread.CurrentThread.Suspend();
- Console.WriteLine("Thread: {0} has been resumed!", Thread.CurrentThread.Name);
- }
- }
当主线程跑(运行)的太快,做完自己的事情去唤醒thread1时,此时thread1还没有挂起而起唤醒thread1,此时就会出现异常了。并且上面使用的Suspend和Resume方法,编译器已经出现警告了,提示这两个方法已经过时, 所以在我们平时使用中应该尽量避免。
1.2 Abort和 Interrupt方法
Abort方法和Interrupt都是用来终止线程的,但是两者还是有区别的。
1、他们抛出的异常不一样,Abort 方法抛出的异常是ThreadAbortException, Interrupt抛出的异常为ThreadInterruptedException
2、调用interrupt方法的线程之后可以被唤醒,然而调用Abort方法的线程就直接被终止不能被唤醒的。
下面一段代码是演示Abort方法的使用
- using System;
- using System.Threading;
- namespace ConsoleApplication1
- {
- class Program
- {
- static void Main(string[] args)
- {
- Thread abortThread = new Thread(AbortMethod);
- abortThread.Name = "Abort Thread";
- abortThread.Start();
- Thread.Sleep(1000);
- try
- {
- abortThread.Abort();
- }
- catch
- {
- Console.WriteLine("{0} Exception happen in Main Thread", Thread.CurrentThread.Name);
- Console.WriteLine("{0} Status is:{1} In Main Thread ", Thread.CurrentThread.Name, Thread.CurrentThread.ThreadState);
- }
- finally
- {
- Console.WriteLine("{0} Status is:{1} In Main Thread ", abortThread.Name, abortThread.ThreadState);
- }
- abortThread.Join();
- Console.WriteLine("{0} Status is:{1} ", abortThread.Name, abortThread.ThreadState);
- Console.Read();
- }
- private static void AbortMethod()
- {
- try
- {
- Thread.Sleep(5000);
- }
- catch(Exception e)
- {
- Console.WriteLine(e.GetType().Name);
- Console.WriteLine("{0} Exception happen In Abort Thread", Thread.CurrentThread.Name);
- Console.WriteLine("{0} Status is:{1} In Abort Thread ", Thread.CurrentThread.Name, Thread.CurrentThread.ThreadState);
- }
- finally
- {
- Console.WriteLine("{0} Status is:{1} In Abort Thread", Thread.CurrentThread.Name, Thread.CurrentThread.ThreadState);
- }
- }
- }
从运行结果可以看出,调用Abort方法的线程引发的异常类型为ThreadAbortException, 以及异常只会在 调用Abort方法的线程中发生,而不会在主线程中抛出,并且调用Abort方法后线程的状态不是立即改变为Aborted状态,而是从AbortRequested->Aborted。
Interrupt方法:
- using System;
- using System.Threading;
- namespace ConsoleApplication1
- {
- class Program
- {
- static void Main(string[] args)
- { Thread interruptThread = new Thread(AbortMethod);
- interruptThread.Name = "Interrupt Thread";
- interruptThread.Start();
- interruptThread.Interrupt();
- interruptThread.Join();
- Console.WriteLine("{0} Status is:{1} ", interruptThread.Name, interruptThread.ThreadState);
- Console.Read();
- }
- private static void AbortMethod()
- {
- try
- {
- Thread.Sleep(5000);
- }
- catch(Exception e)
- {
- Console.WriteLine(e.GetType().Name);
- Console.WriteLine("{0} Exception happen In Interrupt Thread", Thread.CurrentThread.Name);
- Console.WriteLine("{0} Status is:{1} In Interrupt Thread ", Thread.CurrentThread.Name, Thread.CurrentThread.ThreadState);
- }
- finally
- {
- Console.WriteLine("{0} Status is:{1} In Interrupt Thread", Thread.CurrentThread.Name, Thread.CurrentThread.ThreadState);
- }
- }
- }
- }
从结果中可以得到,调用Interrupt方法抛出的异常为:ThreadInterruptException, 以及当调用Interrupt方法后线程的状态应该是中断的, 但是从运行结果看此时的线程因为了Join,Sleep方法而唤醒了线程,为了进一步解释调用Interrupt方法的线程可以被唤醒, 我们可以在线程执行的方法中运用循环,如果线程可以唤醒,则输出结果中就一定会有循环的部分,然而调用Abort方法线程就直接终止,就不会有循环的部分,下面代码相信大家看后肯定会更加理解两个方法的区别的:
- using System;
- using System.Threading;
- namespace ConsoleApplication2
- {
- class Program
- {
- static void Main(string[] args)
- {
- Thread thread1 = new Thread(TestMethod);
- thread1.Start();
- Thread.Sleep(100);
- thread1.Interrupt();
- Thread.Sleep(3000);
- Console.WriteLine("after finnally block, the Thread1 status is:{0}", thread1.ThreadState);
- Console.Read();
- }
- private static void TestMethod()
- {
- for (int i = 0; i < 4; i++)
- {
- try
- {
- Thread.Sleep(2000);
- Console.WriteLine("Thread is Running");
- }
- catch (Exception e)
- {
- if (e != null)
- {
- Console.WriteLine("Exception {0} throw ", e.GetType().Name);
- }
- }
- finally
- {
- Console.WriteLine("Current Thread status is:{0} ", Thread.CurrentThread.ThreadState);
- }
- }
- }
- }
- }
如果把上面的 thread1.Interrupt();改为 thread1.Abort(); 运行结果为:
二、线程池基础
首先,创建和销毁线程是一个要耗费大量时间的过程,另外,太多的线程也会浪费内存资源,所以通过Thread类来创建过多的线程反而有损于性能,为了改善这样的问题 ,.net中就引入了线程池。
线程池形象的表示就是存放应用程序中使用的线程的一个集合(就是放线程的地方,这样线程都放在一个地方就好管理了)。CLR初始化时,线程池中是没有线程的,在内部, 线程池维护了一个操作请求队列,当应用程序想执行一个异步操作时,就调用一个方法,就将一个任务放到线程池的队列中,线程池中代码从队列中提取任务,将这个任务委派给一个线程池线程去执行,当线程池线程完成任务时,线程不会被销毁,而是返回到线程池中,等待响应另一个请求。由于线程不被销毁, 这样就可以避免因为创建线程所产生的性能损失。
注意:通过线程池创建的线程默认为后台线程,优先级默认为Normal.
三、通过线程池的工作者线程实现异步
3.1 创建工作者线程的方法
public static bool QueueUserWorkItem (WaitCallback callBack);
public static bool QueueUserWorkItem(WaitCallback callback, Object state);
这两个方法向线程池的队列添加一个工作项(work item)以及一个可选的状态数据。然后,这两个方法就会立即返回。
工作项其实就是由callback参数标识的一个方法,该方法将由线程池线程执行。同时写的回调方法必须匹配System.Threading.WaitCallback委托类型,定义为:
public delegate void WaitCallback(Object state);
下面演示如何通过线程池线程来实现异步调用:
- using System;
- using System.Threading;
- namespace ThreadPoolUse
- {
- class Program
- {
- static void Main(string[] args)
- {
- // 设置线程池中处于活动的线程的最大数目
- // 设置线程池中工作者线程数量为1000,I/O线程数量为1000
- ThreadPool.SetMaxThreads(1000, 1000);
- Console.WriteLine("Main Thread: queue an asynchronous method");
- PrintMessage("Main Thread Start");
- // 把工作项添加到队列中,此时线程池会用工作者线程去执行回调方法
- ThreadPool.QueueUserWorkItem(asyncMethod);
- Console.Read();
- }
- // 方法必须匹配WaitCallback委托
- private static void asyncMethod(object state)
- {
- Thread.Sleep(1000);
- PrintMessage("Asynchoronous Method");
- Console.WriteLine("Asynchoronous thread has worked ");
- }
- // 打印线程池信息
- private static void PrintMessage(String data)
- {
- int workthreadnumber;
- int iothreadnumber;
- // 获得线程池中可用的线程,把获得的可用工作者线程数量赋给workthreadnumber变量
- // 获得的可用I/O线程数量给iothreadnumber变量
- ThreadPool.GetAvailableThreads(out workthreadnumber, out iothreadnumber);
- Console.WriteLine("{0}\n CurrentThreadId is {1}\n CurrentThread is background :{2}\n WorkerThreadNumber is:{3}\n IOThreadNumbers is: {4}\n",
- data,
- Thread.CurrentThread.ManagedThreadId,
- Thread.CurrentThread.IsBackground.ToString(),
- workthreadnumber.ToString(),
- iothreadnumber.ToString());
- }
- }
- }
从结果中可以看出,线程池中的可用的工作者线程少了一个,用去执行回调方法了。
ThreadPool.QueueUserWorkItem(WaitCallback callback,Object state) 方法可以把object对象作为参数传送到回调函数中,使用和ThreadPool.QueueUserWorkItem(WaitCallback callback)的使用和类似,这里就不列出了。
3.2 协作式取消
.net Framework提供了取消操作的模式, 这个模式是协作式的。为了取消一个操作,首先必须创建一个System.Threading.CancellationTokenSource对象。
下面代码演示了协作式取消的使用,主要实现当用户在控制台敲下回车键后就停止数数方法。
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- using System.Threading;
- namespace ConsoleApplication3
- {
- class Program
- {
- static void Main(string[] args)
- {
- ThreadPool.SetMaxThreads(1000, 1000);
- Console.WriteLine("Main thread run");
- PrintMessage("Start");
- Run();
- Console.ReadKey();
- }
- private static void Run()
- {
- CancellationTokenSource cts = new CancellationTokenSource();
- // 这里用Lambda表达式的方式和使用委托的效果一样的,只是用了Lambda后可以少定义一个方法。
- // 这在这里就是让大家明白怎么lambda表达式如何由委托转变的
- ThreadPool.QueueUserWorkItem(o => Count(cts.Token, 1000));
- ThreadPool.QueueUserWorkItem(callback, cts.Token);
- Console.WriteLine("Press Enter key to cancel the operation\n");
- Console.ReadLine();
- // 传达取消请求
- cts.Cancel();
- }
- private static void callback(object state)
- {
- Thread.Sleep(1000);
- PrintMessage("Asynchoronous Method Start");
- CancellationToken token =(CancellationToken)state;
- Count(token, 1000);
- }
- // 执行的操作,当受到取消请求时停止数数
- private static void Count(CancellationToken token,int countto)
- {
- for (int i = 0; i < countto; i++)
- {
- if (token.IsCancellationRequested)
- {
- Console.WriteLine("Count is canceled");
- break;
- }
- Console.WriteLine(i);
- Thread.Sleep(300);
- }
- Console.WriteLine("Cout has done");
- }
- // 打印线程池信息
- private static void PrintMessage(String data)
- {
- int workthreadnumber;
- int iothreadnumber;
- // 获得线程池中可用的线程,把获得的可用工作者线程数量赋给workthreadnumber变量
- // 获得的可用I/O线程数量给iothreadnumber变量
- ThreadPool.GetAvailableThreads(out workthreadnumber, out iothreadnumber);
- Console.WriteLine("{0}\n CurrentThreadId is {1}\n CurrentThread is background :{2}\n WorkerThreadNumber is:{3}\n IOThreadNumbers is: {4}\n",
- data,
- Thread.CurrentThread.ManagedThreadId,
- Thread.CurrentThread.IsBackground.ToString(),
- workthreadnumber.ToString(),
- iothreadnumber.ToString());
- }
- }
- }
四、使用委托实现异步
通过调用ThreadPool的QueueUserWorkItem方法来来启动工作者线程非常方便,但委托WaitCallback指向的是带有一个参数的无返回值的方法,如果我们实际操作中需要有返回值,或者需要带有多个参数, 这时通过这样的方式就难以实现, 为了解决这样的问题,我们可以通过委托来建立工作这线程,
下面代码演示了使用委托如何实现异步:
- using System;
- using System.Threading;
- namespace Delegate
- {
- class Program
- {
- // 使用委托的实现的方式是使用了异步变成模型APM(Asynchronous Programming Model)
- // 自定义委托
- private delegate string MyTestdelegate();
- static void Main(string[] args)
- {
- ThreadPool.SetMaxThreads(1000, 1000);
- PrintMessage("Main Thread Start");
- //实例化委托
- MyTestdelegate testdelegate = new MyTestdelegate(asyncMethod);
- // 异步调用委托
- IAsyncResult result = testdelegate.BeginInvoke(null, null);
- // 获取结果并打印出来
- string returndata = testdelegate.EndInvoke(result);
- Console.WriteLine(returndata);
- Console.ReadLine();
- }
- private static string asyncMethod()
- {
- Thread.Sleep(1000);
- PrintMessage("Asynchoronous Method");
- return "Method has completed";
- }
- // 打印线程池信息
- private static void PrintMessage(String data)
- {
- int workthreadnumber;
- int iothreadnumber;
- // 获得线程池中可用的线程,把获得的可用工作者线程数量赋给workthreadnumber变量
- // 获得的可用I/O线程数量给iothreadnumber变量
- ThreadPool.GetAvailableThreads(out workthreadnumber, out iothreadnumber);
- Console.WriteLine("{0}\n CurrentThreadId is {1}\n CurrentThread is background :{2}\n WorkerThreadNumber is:{3}\n IOThreadNumbers is: {4}\n",
- data,
- Thread.CurrentThread.ManagedThreadId,
- Thread.CurrentThread.IsBackground.ToString(),
- workthreadnumber.ToString(),
- iothreadnumber.ToString());
- }
- }
- }
五、任务
同样 任务的引入也是为了解决通过ThreadPool.QueueUserWorkItem中限制的问题,
下面代码演示通过任务来实现异步:
5.1 使用任务来实现异步
- using System;
- using System.Threading;
- using System.Threading.Tasks;
- namespace TaskUse
- {
- class Program
- {
- static void Main(string[] args)
- {
- ThreadPool.SetMaxThreads(1000, 1000);
- PrintMessage("Main Thread Start");
- // 调用构造函数创建Task对象,
- Task<int> task = new Task<int>(n => asyncMethod((int)n), 10);
- // 启动任务
- task.Start();
- // 等待任务完成
- task.Wait();
- Console.WriteLine("The Method result is: "+task.Result);
- Console.ReadLine();
- }
- private static int asyncMethod(int n)
- {
- Thread.Sleep(1000);
- PrintMessage("Asynchoronous Method");
- int sum = 0;
- for (int i = 1; i < n; i++)
- {
- // 如果n太大,使用checked使下面代码抛出异常
- checked
- {
- sum += i;
- }
- }
- return sum;
- }
- // 打印线程池信息
- private static void PrintMessage(String data)
- {
- int workthreadnumber;
- int iothreadnumber;
- // 获得线程池中可用的线程,把获得的可用工作者线程数量赋给workthreadnumber变量
- // 获得的可用I/O线程数量给iothreadnumber变量
- ThreadPool.GetAvailableThreads(out workthreadnumber, out iothreadnumber);
- Console.WriteLine("{0}\n CurrentThreadId is {1}\n CurrentThread is background :{2}\n WorkerThreadNumber is:{3}\n IOThreadNumbers is: {4}\n",
- data,
- Thread.CurrentThread.ManagedThreadId,
- Thread.CurrentThread.IsBackground.ToString(),
- workthreadnumber.ToString(),
- iothreadnumber.ToString());
- }
- }
- }
5.2 取消任务
如果要取消任务, 同样可以使用一个CancellationTokenSource对象来取消一个Task.
下面代码演示了如何来取消一个任务:
- using System;
- using System.Threading;
- using System.Threading.Tasks;
- namespace TaskUse
- {
- class Program
- {
- static void Main(string[] args)
- {
- ThreadPool.SetMaxThreads(1000, 1000);
- PrintMessage("Main Thread Start");
- CancellationTokenSource cts = new CancellationTokenSource();
- // 调用构造函数创建Task对象,将一个CancellationToken传给Task构造器从而使Task和CancellationToken关联起来
- Task<int> task = new Task<int>(n => asyncMethod(cts.Token, (int)n), 10);
- // 启动任务
- task.Start();
- // 延迟取消任务
- Thread.Sleep(3000);
- // 取消任务
- cts.Cancel();
- Console.WriteLine("The Method result is: " + task.Result);
- Console.ReadLine();
- }
- private static int asyncMethod(CancellationToken ct, int n)
- {
- Thread.Sleep(1000);
- PrintMessage("Asynchoronous Method");
- int sum = 0;
- try
- {
- for (int i = 1; i < n; i++)
- {
- // 当CancellationTokenSource对象调用Cancel方法时,
- // 就会引起OperationCanceledException异常
- // 通过调用CancellationToken的ThrowIfCancellationRequested方法来定时检查操作是否已经取消,
- // 这个方法和CancellationToken的IsCancellationRequested属性类似
- ct.ThrowIfCancellationRequested();
- Thread.Sleep(500);
- // 如果n太大,使用checked使下面代码抛出异常
- checked
- {
- sum += i;
- }
- }
- }
- catch (Exception e)
- {
- Console.WriteLine("Exception is:" + e.GetType().Name);
- Console.WriteLine("Operation is Canceled");
- }
- return sum;
- }
- // 打印线程池信息
- private static void PrintMessage(String data)
- {
- int workthreadnumber;
- int iothreadnumber;
- // 获得线程池中可用的线程,把获得的可用工作者线程数量赋给workthreadnumber变量
- // 获得的可用I/O线程数量给iothreadnumber变量
- ThreadPool.GetAvailableThreads(out workthreadnumber, out iothreadnumber);
- Console.WriteLine("{0}\n CurrentThreadId is {1}\n CurrentThread is background :{2}\n WorkerThreadNumber is:{3}\n IOThreadNumbers is: {4}\n",
- data,
- Thread.CurrentThread.ManagedThreadId,
- Thread.CurrentThread.IsBackground.ToString(),
- workthreadnumber.ToString(),
- iothreadnumber.ToString());
- }
- }
- }
5.3 任务工厂
同样可以通过任务工厂TaskFactory类型来实现异步操作。
- using System;
- using System.Threading;
- using System.Threading.Tasks;
- namespace TaskFactory
- {
- class Program
- {
- static void Main(string[] args)
- {
- ThreadPool.SetMaxThreads(1000, 1000);
- Task.Factory.StartNew(() => PrintMessage("Main Thread"));
- Console.Read();
- }
- // 打印线程池信息
- private static void PrintMessage(String data)
- {
- int workthreadnumber;
- int iothreadnumber;
- // 获得线程池中可用的线程,把获得的可用工作者线程数量赋给workthreadnumber变量
- // 获得的可用I/O线程数量给iothreadnumber变量
- ThreadPool.GetAvailableThreads(out workthreadnumber, out iothreadnumber);
- Console.WriteLine("{0}\n CurrentThreadId is {1}\n CurrentThread is background :{2}\n WorkerThreadNumber is:{3}\n IOThreadNumbers is: {4}\n",
- data,
- Thread.CurrentThread.ManagedThreadId,
- Thread.CurrentThread.IsBackground.ToString(),
- workthreadnumber.ToString(),
- iothreadnumber.ToString());
- }
- }
- }
六、小结
讲到这里CLR的工作者线程大致讲完了,希望也篇文章可以让大家对线程又有进一步的理解。在后面的一篇线程系列将谈谈CLR线程池的I/O线程。
[.Net线程处理系列]专题二:线程池中的工作者线程相关推荐
- 突然就懵了!面试官问我:线程池中多余的线程是如何回收的?
点击关注公众号,Java干货及时送达 最近阅读了JDK线程池ThreadPoolExecutor的源码,对线程池执行任务的流程有了大体了解,实际上这个流程也十分通俗易懂,就不再赘述了,别人写的比我好多 ...
- C#如何判断线程池中所有的线程是否已经完成(转)
其 实很简单用ThreadPool.RegisterWaitForSingleObject方法注册一个定时检查线程池的方法,在检查线程的方法内调用 ThreadPool.GetAvailableThr ...
- [C#]获得线程池中活动的线程数
在C#中的线程池ThreadPool没有获得线程池中的活动线程数量的属性或者是方法,但是有一两个 方法可以帮助获得活动线程数. 这两个方法分别是:GetMaxThreads和GetAvailableT ...
- [.NET领域驱动设计实战系列]专题二:结合领域驱动设计的面向服务架构来搭建网上书店...
原文:[.NET领域驱动设计实战系列]专题二:结合领域驱动设计的面向服务架构来搭建网上书店 一.前言 在前面专题一中,我已经介绍了我写这系列文章的初衷了.由于dax.net中的DDD框架和Bytear ...
- Java线程池线程突然没了_70%人答不全!线程池中的一个线程异常了会被怎么处理?...
#线程池中的一个线程异常了会被怎么处理? 估计很多人会是以下三点答案(me too): 1.抛异常出来并打印在控制台上 2.其他线程任务不受影响 3.异常线程会被回收 但是这里我先提前说一下以上三点不 ...
- 线程池中运行的线程,当等待队列未满的情况下,一定不大于核心线程数吗
通过<线程池内运行的线程抛异常,线程池会怎么办>了解到当线程执行的任务出现异常时,会将当前线程移出线程池,并新增一个线程到线程池中,我们先来回顾一下线程池的运行原理: 从原理图中可以看到只 ...
- [.Net线程处理系列]专题五:线程同步——事件构造
引言: 其实这部分内容应该是属于专题四,因为这篇也是讲关于线程同步的,但是由于考虑到用户的阅读习惯问题,因为文章太长了,很多人不是很愿意看包括我也是这样的,同时也有和我说可以把代码弄成折叠的,这样就不 ...
- [C# 线程处理系列]专题四:线程同步
目录: 一.线程同步概述 二.线程同步的使用 三 .总结 一.线程同步概述 前面的文章都是讲创建多线程来实现让我们能够更好的响应应用程序,然而当我们创建了多个线程时,就存在多个线程同时访问一个共享的资 ...
- Android-JNI开发系列《二》-在jni层的线程中回调到java层
人间观察 忽有故人心上头,回首山河已是秋. 马上国庆+中秋了--- 今天我们看一个比较常见的场景: 当我们处理一个密集型计算数据(比如音视频的软编解码处理,bitmap的特效处理等),这时候就需要用c ...
最新文章
- maven学习(中)- 私服nexus搭建
- firefox扩展开发(二):用XUL创建窗口控件
- ABAP性能优化之使用 “for all entries”
- 对jquery的conflict方法的解读
- 数据结构学习笔记(一)——《大话数据结构》
- 一些比较有用的SQL操作,2011最新整理
- Sharding-JDBC水平分表(环境搭建)_Sharding-Sphere,Sharding-JDBC分布式_分库分表工作笔记007
- 【React-2】组件开发初步探索
- 在线JSON转Excel工具
- 如何接入易班第三方登录
- 玄武中专计算机动漫与游戏制作,正青春,不散场——玄武中专信息技术系举办2016级计算机动漫与游戏制作专业“3+3”学生毕业作品展开幕式...
- 量子计算机游戏,第3章 量子计算机中的游戏
- [数学基础知识] Cramér‘s V 相关系数和Python算法实现
- C PRIMER PLUS第七章11题
- 安装类Excel开发工具设计器
- MingW下载与安装
- 3D动画旋转rotateY的用法(23)
- python爬虫开发微课版pdf_Python爬虫开发实战教程(微课版)
- Python 的reload()方法
- KMP讲解(自制动图)