同步编程是对于单线程来说的,就像我们编写的控制台程序,以main方法为入口,顺序执行我们编写的代码。

异步编程是对于多线程来说的,通过创建不同线程来实现多个任务的并行执行。

线程

多线程的意义在于一个应用程序中,有多个执行部分可以同时执行;对于比较耗时的操作(例如io,数据库操作),或者等待响应(如WCF通信)的操作,可以单独开启后台线程来执行,这样主线程就不会阻塞,可以继续往下执行;等到后台线程执行完毕,再通知主线程,然后做出对应操作!

什么是主线程

每一个Windows进程都恰好包含一个用作程序入口点的主线程。进程的入口点创建的第一个线程被称为主线程。.Net执行程序(控制台、Windows Form、Wpf等)使用Main()方法作为程序入口点。当调用该方法时,主线程被创建。

什么是工作者线程

由主线程创建的线程,可以称为工作者线程,用来去执行某项具体的任务。

什么是前台线程

默认情况下,使用Thread.Start()方法创建的线程都是前台线程。前台线程能阻止应用程序的终结,只有所有的前台线程执行完毕,CLR才能关闭应用程序(即卸载承载的应用程序域)。前台线程也属于工作者线程。

什么是后台线程

后台线程不会影响应用程序的终结,当所有前台线程执行完毕后,后台线程无论是否执行完毕,都会被终结。一般后台线程用来做些无关紧要的任务(比如邮箱每隔一段时间就去检查下邮件,天气应用每隔一段时间去更新天气)。后台线程也属于工作者线程。

在C#中开启新线程比较简单


static void Main(string[] args)
{
Console.WriteLine("主线程开始!");
//创建前台工作线程
Thread t1 = new Thread(Task1);
t1.Start();
//创建后台工作线程
Thread t2= new Thread(new ParameterizedThreadStart(Task2));
t2.IsBackground = true;//设置为后台线程
t2.Start("传参");
}
private static void Task1()
{
Thread.Sleep(1000);//模拟耗时操作,睡眠1s
Console.WriteLine("前台线程被调用!");
}
private static void Task2(object data)
{
Thread.Sleep(2000);//模拟耗时操作,睡眠2s
Console.WriteLine("后台线程被调用!" + data);
}

线程池

试想一下,如果有大量的任务需要处理,例如网站后台对于HTTP请求的处理,那是不是要对每一个请求创建一个后台线程呢?显然不合适,这会占用大量内存,而且频繁地创建的过程也会严重影响速度,那怎么办呢?线程池就是为了解决这一问题,把创建的线程存起来,形成一个线程池(里面有多个线程),当要处理任务时,若线程池中有空闲线程(前一个任务执行完成后,线程不会被回收,会被设置为空闲状态),则直接调用线程池中的线程执行(例asp.net处理机制中的Application对象)

线程池是为突然大量爆发的线程设计的,通过有限的几个固定线程为大量的操作服务,减少了创建和销毁线程所需的时间,从而提高效率,这也是线程池的主要好处。

ThreadPool适用于并发运行若干个任务且运行时间不长且互不干扰的场景。

还有一点需要注意,通过线程池创建的任务是后台任务。


for (int i = 0; i < 100; i++)
{
//将方法排入队列以便执行,此方法在线程池中线程变的可以时执行
ThreadPool.QueueUserWorkItem(m =>
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId.ToString());
});
}
Console.Read(); //虽然执行了100次,但并没有创建100个线程。
//如果去掉最后一句Console.Read(),会发现程序仅输出【主线程开始!】就直接退出,从而确定ThreadPool创建的线程都是后台线程。

信号量(Semaphore)

信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施, 它负责协调各个线程, 以保证它们能够正确、合理的使用公共资源。

以一个停车场是运作为例。为了简单起见,假设停车场只有三个车位,一开始三个车位都是空的。这是如果同时来了五辆车,看门人允许其中三辆不受阻碍的进入,然后放下车拦,剩下的车则必须在入口等待,此后来的车也都不得不在入口处等待。这时,有一辆车离开停车场,看门人得知后,打开车拦,放入一辆,如果又离开两辆,则又可以放入两辆,如此往复。   在这个停车场系统中,车位是公共资源,每辆车好比一个线程,看门人起的就是信号量的作用。   更进一步,信号量的特性如下:信号量是一个非负整数(车位数),所有通过它的线程(车辆)都会将该整数减一(通过它当然是为了使用资源),当该整数值为零时,所有试图通过它的线程都将处于等待状态。在信号量上我们定义两种操作: Wait(等待) 和 Release(释放)。 当一个线程调用Wait等待)操作时,它要么通过然后将信号量减一,要么一自等下去,直到信号量大于一或超时。Release(释放)实际上是在信号量上执行加操作,对应于车辆离开停车场,该操作之所以叫做“释放”是应为加操作实际上是释放了由信号量守护的资源。

类似互斥锁,但它可以允许多个线程同时访问一个共享资源。通过使用一个计数器来控制对共享资源的访问,如果计数器大于0,就允许访问,如果等于0,就拒绝访问。计数器累计的是“许可证”的数目,为了访问某个资源。线程必须从信号量获取一个许可证。

Semaphore负责协调线程,可以限制对某一资源访问的线程数量


using System.Threading static SemaphoreSlim semLim = new SemaphoreSlim(3); //3表示最多只能有三个线程同时访问
static void Main(string[] args)
{
for (int i = 0; i < 10; i++)
{
new Thread(SemaphoreTest).Start();
}
Console.Read();
}
static void SemaphoreTest()
{
semLim.Wait();
Console.WriteLine("线程" + Thread.CurrentThread.ManagedThreadId.ToString() + "开始执行");
Thread.Sleep(2000);
Console.WriteLine("线程" + Thread.CurrentThread.ManagedThreadId.ToString() + "执行完毕");
semLim.Release();
}

可以看到,刚开始只有三个线程在执行,当一个线程执行完毕并释放之后,才会有新的线程来执行方法!除了SemaphoreSlim类,还可以使用Semaphore类,感觉更加灵活

Semaphore

Public Semaphore(int initialCount,int maximumCount)

initialCount指信号量许可证的初始值,maximumCount为最大值.获取许可证使用WaitOne(),不需要时释放使用 public int Release()或者public int Release(int releaseCount)


public class MyThread
{
public Thread thrd;
//创建一个可授权2个许可证的信号量,且初始值为2
static Semaphore sem = new Semaphore(2, 2);
public MyThread(string name)
{
thrd = new Thread(this.run);
thrd.Name = name;
thrd.Start();
}
void run()
{
Console.WriteLine(thrd.Name + "正在等待一个许可证……");
//申请一个许可证
sem.WaitOne();
Console.WriteLine(thrd.Name + "申请到许可证……");
for (int i = 0; i < 4; i++)
{
Console.WriteLine(thrd.Name + ": " + i);
Thread.Sleep(1000);
}
Console.WriteLine(thrd.Name + " 释放许可证……");
//释放
sem.Release();
}
} class Program
{
public static void Main()
{
mythread mythrd1 = new mythread("Thrd #1");
mythread mythrd2 = new mythread("Thrd #2");
mythread mythrd3 = new mythread("Thrd #3");
mythread mythrd4 = new mythread("Thrd #4");
mythrd1.thrd.Join();
mythrd2.thrd.Join();
mythrd3.thrd.Join();
mythrd4.thrd.Join();
}
}

Task

Task是.NET4.0加入的,跟线程池ThreadPool的功能类似,用Task开启新任务时,会从线程池中调用线程,而Thread每次实例化都会创建一个新的线程,简化了我们进行异步编程的方式,而不用直接与线程和线程池打交道。用Task类可以轻松地在次线程中调用方法。

System.Threading.Tasks中的类型被称为任务并行库(TPL)。TPL使用CLR线程池(说明使用TPL创建的线程都是后台线程)自动将应用程序的工作动态分配到可用的CPU中。


Console.WriteLine("主线程启动");
//Task.Run启动一个线程
//Task启动的是后台线程,要在主线程中等待后台线程执行完毕,可以调用Wait方法
//Task task = Task.Factory.StartNew(() => { Thread.Sleep(1500); Console.WriteLine("task启动"); });
Task task = Task.Run(() => {
Thread.Sleep(1500);
Console.WriteLine("task启动");
});
Thread.Sleep(300);
task.Wait();
Console.WriteLine("主线程结束");

开启新任务的方法:Task.Run()或者Task.Factory.StartNew(),开启的是后台线程。要在主线程中等待后台线程执行完毕,可以使用Wait方法(会以同步的方式来执行)。不用Wait则会以异步的方式来执行。

比较一下Task和Thread:


static void Main(string[] args)
{
for (int i = 0; i < 5; i++)
{
new Thread(Run1).Start();
}
for (int i = 0; i < 5; i++)
{
Task.Run(() => { Run2(); });
}
}
static void Run1()
{
Console.WriteLine("Thread Id =" + Thread.CurrentThread.ManagedThreadId);
/*
Thread Id=10
Thread Id=11
Thread Id=12
Thread Id=13
Thread Id=14
*/
}
static void Run2()
{
Console.WriteLine("Task调用的Thread Id =" + Thread.CurrentThread.ManagedThreadId); /*
Task调用的Thread Id =15
Task调用的Thread Id =6
Task调用的Thread Id =6
Task调用的Thread Id =17
Task调用的Thread Id =15
*/
}

可以看出来,直接用Thread会开启5个线程,用Task(用了线程池)开启了3个!

Task<TResult>

Task<TResult>就是有返回值的Task,TResult就是返回值类型。


Console.WriteLine("主线程开始");
//返回值类型为string
Task<string> task = Task<string>.Run(() => {
Thread.Sleep(2000);
return Thread.CurrentThread.ManagedThreadId.ToString();
});
//会等到task执行完毕才会输出;
Console.WriteLine(task.Result);
Console.WriteLine("主线程结束"); /*
主线程开始
10
主线程结束
*/

通过task.Result可以取到返回值,若取值的时候,后台线程还没执行完,则会等待其执行完毕!Task任务可以通过CancellationTokenSource类来取消,感觉用得不多

Task线程取消

Task 提供了线程取消的操作,使用起来也是非常的简单。它本质上是靠异常来打断线程的执行,并且把Task的状态置为Cancel状态

要实现线程的取消需要一下步骤。

  • 首先要在线程中加入取消检查点(ThrowIfCancellationRequested),这里是在工作函数的起始处和while循环的运行中。
  • 在Main函数中定义CancellationTokenSource对象并把它作为参数传递给工作Task。
  • 在运行时调用CancellationTokenSource.Cancel(),触发线程的取消。
  • 在Main函数中捕获取消异常(OperationCanceledException),线程终止,取消成功。

public void run_with_cancel(System.Threading.CancellationToken ct)
{
System.Console.WriteLine("ThreadWork1 run { ");
ct.ThrowIfCancellationRequested();
for (int i = 0; i < 10; i++)
{
System.Console.WriteLine("ThreadWork1 : " + i);
System.Threading.Thread.Sleep(200);
ct.ThrowIfCancellationRequested();
}
System.Console.WriteLine("ThreadWork1 run } ");
} public void run_with_cancel(System.Threading.CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
System.Console.WriteLine("ThreadWork2 run { ");
for (int i = 0; i < 10; i++)
{
System.Console.WriteLine("ThreadWork2 : " + i * i);
System.Threading.Thread.Sleep(300);
ct.ThrowIfCancellationRequested();
}
System.Console.WriteLine("ThreadWork2 run } ");
}

static void StartT1(System.Threading.CancellationToken ct)
{
ThreadWork1 work1 = new ThreadWork1();
work1.run_with_cancel(ct);
} static void StartT2(System.Threading.CancellationToken ct)
{
ThreadWork2 work2 = new ThreadWork2();
work2.run_with_cancel(ct);
}

System.Threading.CancellationTokenSource cts =new System.Threading.CancellationTokenSource();
System.Threading.CancellationToken ct = cts.Token;
Task t1 = new Task(() => StartT1(ct)); //传入Token
Task t2 = new Task(() => StartT2(ct)); //传入Token
t1.Start();
t2.Start();
System.Threading.Thread.Sleep(2000);
cts.Cancel(); //触发取消
try{
Console.WriteLine("Main wait t1 t2 end {");
if (!Task.WaitAll(new Task[] { t1, t2 }, 5000))
{
Console.WriteLine("Worker1 and Worker2 NOT complete within 5 seconds");
Console.WriteLine("Worker1 Status: " + t1.Status);
Console.WriteLine("Worker2 Status: " + t2.Status);
}
else
{
Console.WriteLine("Worker1 and Worker2 complete within 5 seconds");
}
}
catch (AggregateException agg_ex)
{
foreach (Exception ex in agg_ex.InnerExceptions)
{
Console.WriteLine("Agg Exceptions: " + ex.ToString());
Console.WriteLine("");
}
}

async/await

async/await是C#4.5中推出的,async关键字用来指定某个方法、Lambda表达式或匿名方法自动以异步的方式来调用,先上用法


static void Main(string[] args)
{
Console.WriteLine("-------主线程启动-------");
Task<int> task = GetStrLengthAsync();
Console.WriteLine("主线程继续执行");
Console.WriteLine("Task返回的值" + task.Result);
Console.WriteLine("-------主线程结束-------");
} static async Task<int> GetStrLengthAsync()
{
Console.WriteLine("GetStrLengthAsync方法开始执行");
//此处返回的<string>中的字符串类型,而不是Task<string>
string str = await GetString();
Console.WriteLine("GetStrLengthAsync方法执行结束");
return str.Length;
} static Task<string> GetString()
{
   Console.WriteLine("GetString方法开始执行")
return Task<string>.Run(() =>
{
Thread.Sleep(2000);
return "GetString的返回值";
});
} /*
-------主线程启动-------
GetStrLengthAsync方法开始执行
GetString方法开始执行
主线程继续执行
GetStrLengthAsync方法执行结束
Task返回的值13
-------主线程结束-------
*/

async用来修饰方法,表明这个方法是异步的,声明的方法的返回类型必须为:void,Task或Task<TResult>。方法的执行结果或者任何异常都将直接反映在返回类型中

await必须用来修饰Task或Task<TResult>,而且只能出现在已经用async关键字修饰的异步方法中。通常情况下,async/await成对出现才有意义

可以看出来,main函数调用GetStrLengthAsync方法后,在await之前,都是同步执行的,直到遇到await关键字,main函数才返回继续执行

在遇到await关键字后,没有继续执行GetStrLengthAsync方法后面的操作,也没有马上反回到main函数中,而是执行了GetString的第一行,以此可以判断await这里并没有开启新的线程去执行GetString方法,而是以同步的方式让GetString方法执行,等到执行到GetString方法中的Task<string>.Run()的时候才由Task开启了后台线程!

被async标记的方法,意味着可以在方法内部使用await,这样该方法将会在一个await point(等待点)处被挂起,并且在等待的实例完成后该方法被异步唤醒。【注意:await point(等待点)处被挂起,并不是说在代码中使用await SomeMethodAsync()处就挂起,而是在进入SomeMethodAsync()真正执行异步任务时被挂起,切记

不是被async标记的方法,就会被异步执行,刚开始都是同步开始执行。换句话说,方法被async标记不会影响方法是同步还是异步的方式完成运行。事实上,async使得方法能被分解成几个部分,一部分同步运行,一些部分可以异步的运行(而这些部分正是使用await显示编码的部分),从而使得该方法可以异步的完成。调用async标记的方法,刚开始是同步执行的,只有当执行到await标记的方法中的异步任务时,才会挂起。

那么await的作用是什么呢?

await关键字告诉编译器在async标记的方法中插入一个可能的挂起/唤醒点。 逻辑上,这意味着当你写await someMethod();时,编译器将生成代码来检查someMethod()代表的操作是否已经完成。如果已经完成,则从await标记的唤醒点处继续开始同步执行;如果没有完成,将为等待的someMethod()生成一个continue委托,当someMethod()代表的操作完成的时候调用continue委托。这个continue委托将控制权重新返回到async方法对应的await唤醒点处。返回到await唤醒点处后,不管等待的someMethod()是否已经经完成,任何结果都可从Task中提取,或者如果someMethod()操作失败,发生的任何异常随Task一起返回或返回给SynchronizationContext。

可以从字面上理解,上面提到task.wait可以让主线程等待后台线程执行完毕,await和wait类似,同样是等待,等待Task<string>.Run()开始的后台线程执行完毕,不同的是await不会阻塞主线程,只会让GetStrLengthAsync方法暂停执行。

IAsyncResult

IAsyncResult自.NET1.1起就有了,包含可异步操作的方法的类需要实现它,Task类就实现了该接口

在不借助于Task的情况下怎么实现异步呢?


class Program
{
static void Main(string[] args)
{
Console.WriteLine("主程序开始--------------------");
int threadId;
AsyncDemo ad = new AsyncDemo();
AsyncMethodCaller caller = new AsyncMethodCaller(ad.TestMethod); IAsyncResult result = caller.BeginInvoke(3000,out threadId, null, null);
Thread.Sleep(0);
Console.WriteLine("主线程线程 {0} 正在运行.",Thread.CurrentThread.ManagedThreadId)
//会阻塞线程,直到后台线程执行完毕之后,才会往下执行
result.AsyncWaitHandle.WaitOne();
Console.WriteLine("主程序在做一些事情!!!");
//获取异步执行的结果
string returnValue = caller.EndInvoke(out threadId, result);
//释放资源
result.AsyncWaitHandle.Close();
Console.WriteLine("主程序结束--------------------");
Console.Read();
}
}
public class AsyncDemo
{
//供后台线程执行的方法
public string TestMethod(int callDuration, out int threadId)
{
Console.WriteLine("测试方法开始执行.");
Thread.Sleep(callDuration);
threadId = Thread.CurrentThread.ManagedThreadId;
return String.Format("测试方法执行的时间 {0}.", callDuration.ToString());
}
}
public delegate string AsyncMethodCaller(int callDuration, out int threadId); /*
主程序开始--------------------
主线程线程9正在运行.
测试方法开始执行.
测试方法执行完毕.
主程序在做一些事情!!!
主程序结束--------------------
*/

和Task的用法差异不是很大!result.AsyncWaitHandle.WaitOne()就类似Task的Wait。

Parallel(数据并行)

数据并行是指使用Parallel.For()或Parallel.ForEach()方法以并行方式对数组或集合中的数据进行迭代。


Stopwatch watch1 = new Stopwatch();
watch1.Start();
for (int i = 1; i <= 10; i++)
{
Console.Write(i + ",");
Thread.Sleep(1000);
}
watch1.Stop();
Console.WriteLine(watch1.Elapsed); Stopwatch watch2 = new Stopwatch();
watch2.Start(); //会调用线程池中的线程
Parallel.For(1, 11, i =>
{
Console.WriteLine(i + ",线程ID:" + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(1000);
});
watch2.Stop();
Console.WriteLine(watch2.Elapsed);

List<int> list = new List<int>() { 1, 2, 3, 4, 5, 6, 6, 7, 8, 9 };
Parallel.ForEach<int>(list, n =>
{
Console.WriteLine(n);
Thread.Sleep(1000);
});

Action[] actions = new Action[] {
new Action(()=>{
Console.WriteLine("方法1");
}),
new Action(()=>{
Console.WriteLine("方法2");
})
};
Parallel.Invoke(actions);

PLINQ(并行LINQ查询)

为并行运行而设计的LINQ查询为PLINQ。System.Linq命名空间的ParallelEnumerable中包含了一些扩展方法来支持PINQ查询。


int[] modThreeIsZero = (from num in source.AsParallel()
where num % 3 == 0
orderby num descending
select num).ToArray();

异步的回调

为了简洁(偷懒),文中所有Task<TResult>的返回值都是直接用task.result获取,这样如果后台任务没有执行完毕的话,主线程会等待其执行完毕。这样的话就和同步一样了,一般情况下不会这么用。简单演示一下Task回调函数的使用:


Console.WriteLine("主线程开始");
Task<string> task = Task<string>.Run(() => {
Thread.Sleep(2000);
return Thread.CurrentThread.ManagedThreadId.ToString();
});
//会等到任务执行完之后执行
task.GetAwaiter().OnCompleted(() =>
{
Console.WriteLine(task.Result);
});
Console.WriteLine("主线程结束");
Console.Read(); /*
主线程开始
主线程结束
10
*/

OnCompleted中的代码会在任务执行完成之后执行!另外task.ContinueWith()也是一个重要的方法:


Console.WriteLine("主线程开始");
Task<string> task = Task<string>.Run(() => {
Thread.Sleep(2000);
return Thread.CurrentThread.ManagedThreadId.ToString();
}); task.GetAwaiter().OnCompleted(() =>
{
Console.WriteLine(task.Result);
});
task.ContinueWith(m=>{Console.WriteLine("第一个任务结束啦!我是第二个任务");});
Console.WriteLine("主线程结束");
Console.Read(); /*
主线程开始
主线程结束
10
第一个任务结束啦!我是第二个任务
*/

ContinueWith()方法可以让该后台线程继续执行新的任务,控制Task简单的任务执行顺序的方式。

用ContinueWith和Wait函数组合使用便可以控制任务的运行顺序。


class ThreadWork1
{
public ThreadWork1()
{ } public List<string> run()
{
List<string> RetList = new List<string>();
System.Console.WriteLine("ThreadWork1 run { ");
System.Console.WriteLine("ThreadWork1 running ... ... ");
for (int i = 0; i < 100; i++)
{
RetList.Add("ThreadWork1 : " + i);
//System.Console.WriteLine("ThreadWork1 : " + i);
}
System.Console.WriteLine("ThreadWork1 run } "); return RetList;
}
} class ThreadWork2
{
public ThreadWork2()
{ } public List<string> run()
{
List<string> RetList = new List<string>(); System.Console.WriteLine("ThreadWork2 run { ");
System.Console.WriteLine("ThreadWork2 running ... ... ");
for (int i = 0; i < 100; i++)
{
RetList.Add("ThreadWork2 : " + i);
//System.Console.WriteLine("ThreadWork2 : " + i * i);
}
System.Console.WriteLine("ThreadWork2 run } ");
return RetList;
}
} class Program
{
static void StartT0()
{
System.Console.WriteLine("Hello I am T0 Task, sleep 3 seconds. when I am ready others GO!");
for (int i = 0; i < 3; i++)
{
Console.WriteLine("StartT0 sleeping ... ... " + i);
System.Threading.Thread.Sleep(1000);
}
} static List<string> StartT1()
{
ThreadWork1 work1 = new ThreadWork1();
return work1.run();
} static List<string> StartT2()
{
ThreadWork2 work2 = new ThreadWork2();
return work2.run();
}
static void Main(string[] args)
{
Console.WriteLine("Sample 3-4 Main {");
// The sequence of the task is:
// T0 (Wait 3s) --> |
// | --> T1 (Cacluate) |
// | --> T2 (Cacluate) |
// | --> T3 (Print) var t0 = Task.Factory.StartNew(() => StartT0());
var t1 = t0.ContinueWith((t) => StartT1());
var t2 = t0.ContinueWith((t) => StartT2()); Console.WriteLine("Main wait t1 t2 end {");
Task.WaitAll(t1, t2);
Console.WriteLine("Main wait t1 t2 end }"); var t3 = Task.Factory.StartNew(() =>
{
Console.WriteLine("============= T1 Result =============");
for (int i = 0; i < t1.Result.Count; i++)
{
Console.WriteLine(t1.Result[i]);
}
Console.WriteLine("============= ========= =============\n\n"); Console.WriteLine("============= T2 Result =============");
for (int i = 0; i < t2.Result.Count; i++)
{
Console.WriteLine(t2.Result[i]);
}
Console.WriteLine("============= ========= =============\n\n");
}, TaskCreationOptions.LongRunning); Console.WriteLine("Sample 3-4 Main }"); Console.ReadKey();
}
}

最新文章

  1. [转载]VIM 教程:Learn Vim Progressively
  2. 8天掌握EF的Code First开发系列之3 管理数据库创建,填充种子数据以及LINQ操作详解
  3. uestc 1073 秋实大哥与线段树 Label:线段树
  4. 转 XenServer、XenCenter安装测试
  5. poj 1695 动态规划
  6. Linux下修改网卡IP、DNS和网关
  7. Mac RTX
  8. rips中如何使用PHP虚拟机自带函数--token_get_all
  9. SpringBoot使用Nacos配置中心
  10. Hello Json(c#)
  11. Lombok使用简介
  12. 二分求幂/快速幂取模运算——root(N,k)
  13. 启动与关闭WebService
  14. 吴裕雄 15-MySQL LIKE 子句
  15. IO multiplexing 与 非阻塞网络编程
  16. curl教程2
  17. Codeforces 1091 Good Bye 2018
  18. Django FBV和CBV -
  19. ANT发送邮件需要的3个JAR包
  20. 由已打开的文件读取数据---read

热门文章

  1. (转) 通过UUID在vSphere虚拟机内外识别硬盘
  2. Transact-SQL语法速查手册
  3. Thread Safety in Java(java中的线程安全)
  4. jtag引脚
  5. POJ 1595 Prime Cuts (ZOJ 1312) 素数打表
  6. 【习题 5-6 UVA-1595】Symmetry
  7. Xvisor ARM32 启动分析
  8. ZOJ 1494 Climbing Worm 数学水题
  9. java 返回图片到页面
  10. [Vue] Create Vue.js Layout and Navigation with Nuxt.js