在web应用中,单个进程或者机器的响应速度有限,类似大量数据导入导出的操作的数量如果不加限制,会导致服务器cpu被吃满,导致其他一些很简单的请求无法及时响应的问题。针对这个限制提出了如下要求。
1. 先到达的请求先执行: 先入先出原则
2. 只能同时执行若干请求:避免cpu被吃满
3. 异步执行:如果长时间执行会长期占用iis的工作线程
基于上述的要求我设计了一个队列。这个队列我们需要稍微提一个组件,ParallelExtensionsExtras
这是微软提供的一个线程的扩展,具体的自行搜索下相关资料,这里开始的时候我并没有用这个组件,而是自己对task的封装,但是实际上task还是利用的线程池,线程池默认的线程数10个,并不能满足某些场景对多个现成的要求,于是在漫长的搜索过程中才发现了这个组建。
这里我主要用到两个对TaskScheduler的扩展
QueuedTaskScheduler:对task进行排队执行,执行时在Thread环境中,并且可以控制线程的数量,类似自定义的线程池。
ThreadPerTaskScheduler: 顾名思义就是在线程中执行每个task。
针对两种区别在于,QueuedTaskScheduler 的线程是可复用的,在线程数量固定的情况下推荐使用。
ThreadPerTaskScheduler 只创建线程而不进行销毁,每次执行一个task都是使用一个new一个thread执行。适用于在某些线程数量动态变化的情况。
下面是实现代码:
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Schedulers;
using Coralcode.Framework.Data;
using Coralcode.Framework.Extensions;
using ThreadState = System.Threading.ThreadState;
namespace Coralcode.Framework.Task
{
/// <summary>
/// 线程队列
/// 注意如果使用ScheduleType.Defaut 存在线程池数量限制
/// </summary>
public class TaskQueue
{
public static readonly TaskQueue Instance = new TaskQueue(1, ScheduleType.Thread);
/// <summary>
/// 获取队列
/// </summary>
/// <param name="maxCount">最大并发数</param>
/// <param name="type">执行计划</param>
/// <returns></returns>
public static TaskQueue GetQueue(int maxCount,ScheduleType type=ScheduleType.Thread)
{
return new TaskQueue(maxCount, type);
}
/// <summary>
/// 获取队列
/// </summary>
/// <param name="maxCount">最大并发数</param>
/// <param name="name">队列名称</param>
/// <param name="type">执行计划</param>
/// <returns></returns>
public static TaskQueue GetQueue(int maxCount, string name, ScheduleType type = ScheduleType.Thread)
{
return new TaskQueue(maxCount, name, type);
}
/// <summary>
/// 获取队列
/// </summary>
/// <param name="maxCount">最大并发数</param>
/// <param name="name">队列名称</param>
/// <param name="token">取消令牌</param>
/// <param name="type">执行计划</param>
public static TaskQueue GetQueue(int maxCount, string name, CancellationToken token, ScheduleType type = ScheduleType.Thread)
{
return new TaskQueue(maxCount, name, token, type);
}
/// <summary>
/// 获取队列
/// </summary>
/// <param name="maxCount">最大并发数</param>
/// <param name="token">取消令牌</param>
/// <param name="type">执行计划</param>
public static TaskQueue GetQueue(int maxCount, CancellationToken token, ScheduleType type = ScheduleType.Thread)
{
return new TaskQueue(maxCount, IdentityGenerator.NewGuidString(), token, type);
}
private ConcurrentQueue<System.Threading.Tasks.Task> _tasks;
private readonly int _limitedTaskCount;
private int _runningTaskCount;
private Thread _mainExcuteThread;
private CancellationToken _token;
private TaskScheduler _scheduler;
private TaskQueue(int maxCount, ScheduleType type) :
this(maxCount, IdentityGenerator.NewGuidString(), type)
{
}
private TaskQueue(int maxCount, string name, ScheduleType type) :
this(maxCount, name, CancellationToken.None, type)
{
}
private TaskQueue(int maxCount, string name, CancellationToken token, ScheduleType type)
{
_limitedTaskCount = maxCount;
_tasks = new ConcurrentQueue<System.Threading.Tasks.Task>();
Name = name;
_token = token;
switch (type)
{
case ScheduleType.Default:
_scheduler = new QueuedTaskScheduler(maxCount);
break;
case ScheduleType.Thread:
_scheduler = new ThreadPerTaskScheduler();
break;
default:
throw new ArgumentOutOfRangeException(nameof(type), type, null);
}
}
/// <summary>
/// 执行不带返回结果的方法
/// </summary>
/// <param name="func"></param>
/// <returns></returns>
public System.Threading.Tasks.Task Execute(Action func)
{
var task = new System.Threading.Tasks.Task(func, _token);
_tasks.Enqueue(task);
if (_mainExcuteThread == null || _mainExcuteThread.ThreadState.HasFlag(ThreadState.Stopped))
{
_mainExcuteThread?.DisableComObjectEagerCleanup();
_mainExcuteThread = new Thread(NotifyThreadPendingWork);
_mainExcuteThread.Start();
}
return task;
}
/// <summary>
/// 执行带返回结果的方法
/// </summary>
/// <typeparam name="TResult"></typeparam>
/// <param name="func"></param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.Synchronized)]
public Task<TResult> Execute<TResult>(Func<TResult> func)
{
var task = new Task<TResult>(func, _token);
_tasks.Enqueue(task);
if (_mainExcuteThread == null || _mainExcuteThread.ThreadState.HasFlag(ThreadState.Stopped))
{
_mainExcuteThread?.DisableComObjectEagerCleanup();
_mainExcuteThread = new Thread(NotifyThreadPendingWork);
_mainExcuteThread.Priority = ThreadPriority.Highest;
_mainExcuteThread.Start();
}
return task;
}
private void NotifyThreadPendingWork()
{
try
{
while (true)
{
if (_token.IsCancellationRequested)
{
_tasks = new ConcurrentQueue<System.Threading.Tasks.Task>();
break;
}
System.Threading.Tasks.Task task;
if (!_tasks.TryDequeue(out task))
break;
task.Start(_scheduler);
Interlocked.Increment(ref _runningTaskCount);
task.ContinueWith(item =>
{
Interlocked.Decrement(ref _runningTaskCount);
});
//Debug.WriteLine("队列{0},允许执行 {1} 条,等待线程为 {2} ,执行中 {3} 条,时间为 {4} ", _name, _limitedTaskCount, _tasks.Count, _runningTaskCount, DateTime.Now);
while (_runningTaskCount >= _limitedTaskCount)
{
Thread.Sleep(500);
}
}
}
finally
{
_runningTaskCount = 0;
}
}
/// <summary>
/// 线程队列的名字
/// </summary>
public string Name { get; }
/// <summary>
/// 根据返回的task的id获取到当前task排队的位置
/// </summary>
/// <param name="taskId"></param>
/// <returns>返回-1表示正在执行,或者task没有加进去,返回大于等于0则表示其顺序</returns>
public int GetCurrentTaskIndex(int taskId)
{
lock (_tasks)
{
return _tasks.IndexOf(item => item.Id == taskId);
}
}
/// <summary>
/// 等待执行的线程数量
/// </summary>
public int WaitingTaskCount => _tasks.Count;
/// <summary>
/// 正在执行的线程数量
/// </summary>
public int RunningTaskCount => _runningTaskCount;
/// <summary>
/// 并发数
/// </summary>
public int LimitedTaskCount => _limitedTaskCount;
}
/// <summary>
///
/// </summary>
public enum ScheduleType {
/// <summary>
/// 线程池,默认方式
/// </summary>
Default,
/// <summary>
/// 自定义线程,ThreadPerTaskScheduler
/// </summary>
Thread
}
}
重点部分说明
1. ConcurrentQueue 本身在处理多线程环境所以采用线程安全的队列。
2. ContinueWith 在任务执行完毕之后需要对执行的数量-1。
3. NotifyThreadPendingWork 这里就是启动另外一个主线程来对任务进行分发。
4. 主线程分发来满足异步的需求。
5. CancellationToken 提供取消执行的功能。
6. 可以采用默认也可以自己实例化,而默认是一个线程,即考虑常用情况,也提供扩展的功能。
测试代码
写出来这个队列的部分可能只用了1小时,但是写测试代码和调试用了差不多半天时间才搞定。
线程的测试在单元测试中一直是难以控制的,在这个case中多个线程并发的情况下,实时获取排队数量,执行中数量也是个很难测试的部分。
下面是测试代码:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Coralcode.Framework.Task;
using Coralcode.Framework.Utils;
using iTextSharp.text;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace FrameworkTest.Task
{
[TestClass]
public class TaskQueueTest
{
[TestMethod]
public void TwoTimesExcuteWithFreeBetweenTwoTimsTest()
{
var wantExecuteTaskCount = 10;
var maxCanExcuteTaskCount = 1;
var queue = TaskQueue.GetQueue(maxCanExcuteTaskCount);
var addList = new List<int>();
var executedList = new List<int>();
for (int i = 0; i < wantExecuteTaskCount; i++)
{
addList.Add(i);
}
/*
* 测试原理:
* 添加一批任务,等待执行完成以后在执行一次
* 第二次也能执行完毕,无报错的话认为,
* 队列在执行完毕后自动暂停,等有新任务进来的时候可以重新启动
*/
addList.ForEach(index =>
{
var list = executedList;
queue.Execute(() =>
{
var item = index / maxCanExcuteTaskCount;
list.Add(item);
return true;
});
});
while (addList.Count != executedList.Count)
{
Thread.Sleep(1000);
}
Assert.AreEqual(queue.WaitingTaskCount, 0);
executedList.Clear();
for (int i = 0; i < wantExecuteTaskCount; i++)
{
addList.Add(i);
}
addList.ForEach(index =>
{
var list = executedList;
queue.Execute(() =>
{
var item = index / maxCanExcuteTaskCount;
list.Add(item);
return true;
});
});
while (addList.Count != executedList.Count)
{
Thread.Sleep(1000);
}
Assert.AreEqual(queue.WaitingTaskCount, 0);
}
[TestMethod]
public void ExecuteAsAddSequenceTest()
{
var wantExecuteTaskCount = 10;
var maxCanExcuteTaskCount = 1;
var queue = TaskQueue.GetQueue(maxCanExcuteTaskCount);
var addList = new List<int>();
var executedList = new List<int>();
for (int i = 0; i < wantExecuteTaskCount; i++)
{
addList.Add(i);
}
/*
* 测试原理:
* 按需添加,然后记录执行,结果也必须是顺序的
* 如果一次执行多条,那么多条被认为同一个批次
* 同一个批次的顺序应该是一致的
*/
addList.ForEach(index =>
{
var list = executedList;
queue.Execute(() =>
{
var item = index / maxCanExcuteTaskCount;
list.Add(item);
return true;
});
});
while (addList.Count != executedList.Count)
{
Thread.Sleep(1000);
}
addList = addList.Select(item => item / maxCanExcuteTaskCount).Distinct().ToList();
executedList = executedList.Distinct().ToList();
for (int i = 0; i < addList.Count; i++)
{
Assert.AreEqual(addList[i], executedList[i]);
}
}
[TestMethod]
public void WaitCountTest()
{
var wantExecuteTaskCount = 10;
var maxCanExecuteTaskCount = 2;
var queue = TaskQueue.GetQueue(maxCanExecuteTaskCount);
var executedOrExecutingCount = 0;
/*
* 测试原理:
* 记录执行了的数量
* 队列中等待的数量 =最开始放入的数量-队列运行执行的最大线程数-已经执行的数量
* 当所有的线程都在被执行时,队列中等待数量为0,但是还未执行完的话,直接用0来判断
*/
var taskList = new List<System.Threading.Tasks.Task>();
for (int i = 0; i < wantExecuteTaskCount; i++)
{
var task = queue.Execute(() =>
{
Thread.Sleep(2000);
Debug.WriteLine("已执行的数量" + executedOrExecutingCount);
Interlocked.Increment(ref executedOrExecutingCount);
});
taskList.Add(task);
}
// ReSharper disable once LoopVariableIsNeverChangedInsideLoop
while (executedOrExecutingCount < wantExecuteTaskCount)
{
Thread.Sleep(700);
var queueWaitCount = queue.WaitingTaskCount;
var wantWaitCount = wantExecuteTaskCount - maxCanExecuteTaskCount - executedOrExecutingCount;
Debug.WriteLine("************");
Debug.WriteLine("队列等待执行的数量" + queueWaitCount);
Debug.WriteLine("队列期望等待执行的数量" + wantWaitCount);
Debug.WriteLine("************");
if (wantWaitCount < 0)
Assert.AreEqual(queueWaitCount, 0);
else
Assert.AreEqual(queueWaitCount, wantWaitCount);
}
}
[TestMethod]
public void WaitTest()
{
//System.Threading.Tasks.Task.Factory.StartNew(() =>
//{
// try
// {
// var str = HttpUtil.Get<string>("http://172.16.2.3:10004",
// "api/Acl/PermissionAuthorityApi/GetPermissionCountByApp?app=1");
// }
// catch (Exception e)
// {
// Console.WriteLine(e);
// throw;
// }
//});
var wantExecuteTaskCount = 10;
var maxCanExcuteTaskCount = 1;
var queue = TaskQueue.GetQueue(maxCanExcuteTaskCount);
var addList = new List<int>();
var executedList = new List<int>();
for (int i = 0; i < wantExecuteTaskCount; i++)
{
addList.Add(i);
}
var tasks = new List<System.Threading.Tasks.Task>();
/*
* 测试原理:
* 执行异步请求看最后是否成功执行
*/
addList.ForEach(index =>
{
var list = executedList;
var task = queue.Execute(() =>
{
try
{
var str = HttpUtil.Get<string>("http://172.16.2.3:10004",
"api/Acl/PermissionAuthorityApi/GetPermissionCountByApp?app=1");
}
catch (Exception e)
{
Console.WriteLine(e);
}
var item = index / maxCanExcuteTaskCount;
list.Add(item);
});
tasks.Add(task);
});
System.Threading.Tasks.Task.WaitAll(tasks.ToArray());
//System.Threading.Tasks.Task.WaitAll(tasks.ToArray());
addList = addList.Select(item => item / maxCanExcuteTaskCount).Distinct().ToList();
executedList = executedList.Distinct().ToList();
for (int i = 0; i < addList.Count; i++)
{
Assert.AreEqual(addList[i], executedList[i]);
}
}
[TestMethod]
public void ExceptionTestTest()
{
var wantExecuteTaskCount = 10;
var maxCanExecuteTaskCount = 2;
var queue = TaskQueue.GetQueue(maxCanExecuteTaskCount);
var executedOrExecutingCount = 0;
/*
* 发现部分情况下队列里面的异常检测不到
* 这里做了测试
*/
queue.Execute(() =>
{
try
{
throw new Exception("测试异常");
}
catch (Exception ex)
{
Assert.IsNotNull(ex);
}
});
}
[TestMethod]
public void GetCurrentTaskIndexTeset()
{
var wantExecuteTaskCount = 10;
var maxCount = 10;
var waitSeconds = 1000;
var queue = TaskQueue.GetQueue(maxCount);
List< System.Threading.Tasks.Task> executeingTasks = new List<System.Threading.Tasks.Task>();
/*
* 测试方法:
* 假设并发为10,那么首先塞进去10个线程执行,
* 那么后面的线程都在等待,
* 在放进去n个,则后面的正在排队,返回的位置和放进去的位置一致
* 而之前放进去的10个都在执行,返回-1
*/
for (int i = 0; i < maxCount; i++)
{
var task= queue.Execute(() =>
{
try
{
Thread.Sleep(waitSeconds * 1000);
}
catch (Exception e)
{
Console.WriteLine(e);
}
});
executeingTasks.Add(task);
}
Dictionary<int,System.Threading.Tasks.Task> tasks = new Dictionary<int, System.Threading.Tasks.Task>();
for (int i = 0; i < wantExecuteTaskCount; i++)
{
var task = queue.Execute(() =>
{
try
{
Thread.Sleep(waitSeconds*1000);
}
catch (Exception e)
{
Console.WriteLine(e);
}
});
tasks.Add(i,task);
}
foreach (var task in executeingTasks)
{
Assert.AreEqual(queue.GetCurrentTaskIndex(task.Id), -1);
}
foreach (var task in tasks)
{
Assert.AreEqual(queue.GetCurrentTaskIndex(task.Value.Id),task.Key);
}
}
}
}
具体的代码中都有每个测试用例的设计思考。
注意事项
注意事项就是我在这里踩过的坑
Session丢失
在web应用中session的存储实际以线程为基础,类似ThreadLocal实现的线程隔离和静态使用。
这里再执行之前需要预先取出来要使用的对象然后再在action/func中使用
DbContext
1. 在ef中如果获取一个dbcontext在另外一个线程中保存就会报错。
2. 如果一个从ef取出来的对象从一个线程传递到另外一个线程,修改提交就会报错。
3. 如果主线程和队列中线程同时操作例如一个读一个写,或者同时写,此时也会报错
Ioc的问题
如果ioc采用prethread或者preresolve这两种方式来管理生命周期,理论上在http请求结束的时候都要对线程中使用的对象进行释放,
因为队列为异步那么当请求结束时候线程却还在执行,此时就会出现空指针的问题,或者ef中对象已经释放。
最佳实践
/// <summary>
/// 调用示例
///
/// </summary>
/// <typeparam name="TService"></typeparam>
public static void AsyncExecuteServiceAction<TService>(this TService service,string actionName,params object[] parameters)
where TService:CoralService
{
TaskQueue.Instance.Execute(() =>
{
using (var newservice = UnityService.Resolve<TService>())
{
newservice.InitContext(service.AppContext,service.UserContext,service.SessionContext,service.PageContext);
newservice.ExecuteMethod<object>(actionName, parameters);
}
});
}
1. service.AppContext,service.UserContext,service.SessionContext,service.PageContext 这是四种不同级别的缓存,理解为session
2. using 的目的就是为了释放对象
3. 反射参数的方式只是为了方便调用
总结
1. 代码不难,直接拷贝就可以使用,单元测试也是一样
2. TPL我觉得是在.net技术中很好用的一部分,需要熟悉
3. 线程中有很多对象,锁等问题,这个要随着经验不断的挖坑填坑来增长经验
展望
1. 前面说过两种线程计划,我这里放入prethread是在某个工具中有压力测试的部分,需要以最快的速度来创建线程,所以没有采用复用的方式,具体后面有一个接口测试工具的系列文章来介绍。
2. 这里说的线程队列,和前面动态类型序列化是后面一个作业调度系统的基础,这部分还在设计和实现中,预计月底可以在文章中和大家见面。
3. 其实上一篇和这一篇都是在为作业系统做铺垫。
4. 下一篇将介绍一个定时执行任务的设计。
5. 虽然我一直觉得设计是最重要的,实现其次,但是要落地还是要依赖于实现,所以这些基本的组件和帮助类还是需要的。