ActiveRecoverySwitcher






4.89/5 (4投票s)
如何切换 N 个线程 - 主服务和备用服务。展示一种实现备用服务切换的方法。这可以轻松地转换为负载均衡服务。
引言
这是一个原型/概念验证项目,展示了如何在某些资源不可用时直观地切换资源。它专门设计用于在 N 个自动识别系统 (AIS) 实时数据流 - TCP 流之间进行切换,但为了方便实现此演示,我们将不使用这些实时数据流,而是使用文件来模拟 TCP 流。
背景
请注意,执行此演示不需要具备 AIS 的工作知识,但如果您想了解一些背景信息,请访问以下链接。
AIS 消息通过 TCP 流从不同的 AIS 提供商 (http://www.aishub.net/, http://marinetraffic.com/) 发送。AIS 消息存储有关船只 ID、速度、目的地等信息,更多信息请访问以下链接。
- http://www.it-digin.com/blog/?p=20
- http://www.maritec.co.za/aisoverview.php
- http://www.bosunsmate.org/ais/
概念
其思想是,如果文件存在,则表示服务可用,反之亦然。这些服务也应具有优先级,可以在数据结构中指定。在此演示中,文件也已通过按顺序命名的方式进行了优先级排序,从“TextFile0.txt”(最高优先级)到“TextFileN.txt”(最低优先级)。
当“TextFile0.txt”可用时,我们无需使用下一个可用的服务“ThreadFile1.txt”(即优先级次高的线程)。但如果“TextFile0.txt”不可用,则检查下一个,依此类推,直到达到集合中可能的最大服务(文件)。
如果我们当前正在使用(第四个服务)“TextFile3.txt”,则表示有三个其他优先级更高的服务可能会在我们仍然使用“TextFile3.txt”时恢复。这就是为什么我们需要定期检查其他服务是否可用 = 其他文件是否存在。
测试软件
要测试软件,只需运行应用程序并开始更改文件名(请查看项目测试文件的 Files 文件夹)。当您更改文件“TextFile0.txt”时,应用程序会监听下一个服务(文件)“TextFile1.txt”。您会注意到,过一段时间后,会再次检查 TextFile0.txt 是否存在(服务是否已恢复)。
请注意,“App.config” - 存储服务数量 = 文件数量 = 线程数量的配置。
始终有一个主线程 - 控制台应用程序的主线程。
要点 - 源代码解释
为了检查之前的文件/线程是否存在,我使用了 Timers。但因为每个计时器对应一个线程,所以我扩展了 Timer 类,并添加了 ID - 这是线程的 ID,以及一些附加字段(这些只是为了说明目的)。public class ServiceTimer : Timer
public class ServiceTimer : Timer
{
/// <summary>
/// Service ID == TimerID (every timer is associated with a service)
/// </summary>
/// <value>
/// The unique identifier.
/// </value>
public int Id { get; set; }
/// <summary>
/// Gets or sets the name of the timer. (debuggingh purposes)
/// </summary>
/// <value>
/// The name.
/// </value>
public string Name { get; set; }
/// <summary>
/// Gets or sets the description. (debuggingh purposes)
/// </summary>
/// <value>
/// The description.
/// </value>
public string Description { get; set; }
Timers 由“ServiceTimers.cs”类管理。它包含一个 Timer 的字典集合。Tick 方法用于指定一个委托,该委托将处理特定“id”服务的 Elapsed 事件。
public class ServiceTimers
{
public delegate void OnElapsedTime(object sender, ElapsedEventArgs e);
public OnElapsedTime OnElapsed { get; set; }
private Dictionary<string, ServiceTimer> timers = new Dictionary<string, ServiceTimer>();
public void Add(ServiceTimer serviceTimer, OnElapsedTime onElapsed)
{
OnElapsed = onElapsed;
timers.Add(serviceTimer.Name, serviceTimer);
}
public void Tick(int id)
{
foreach (var t in timers.Values)
{
if (t.Id == id)
{
t.Elapsed += new ElapsedEventHandler(OnElapsed);
break;
}
}
}
}
现在我们来看主类:“ActiveSwitcher.cs”。
它有几个大小等于服务数量 - 可用文件数量的数组。
bool[] exit = new bool[ServicesCount];
- 仅在停止线程之前设置为 true。bool[] works = new bool[ServicesCount];
- 如果文件 - 服务正常工作,则设置为 true,如果文件不存在,则设置为 false。object[] lockers = new object[ServicesCount];
- 锁定对象集合 - 当我们需要在关键部分锁定代码时。CheckResource(int resourceID);
- 此委托用于,因为最好在类外部检查资源。其思想是,我们希望 ActiveSwitcher 像一个黑盒子一样工作 - 只需指定服务数量、检查它们的方式以及检查的计时器间隔。
在应用程序的 Main()
方法中 - 在 Program.cs 中,此委托被分配给 Exist 方法 - 此方法只是检查指定路径中的文件是否存在。
在 Init()
方法中,上述所有数组都已初始化。如代码注释所示,只有主线程被标记为 works[0] = true;
。我们需要这样做来开始检查服务。
private void Init()
{
for (int i = 0; i < ServicesCount; i++)
{
exit[i] = false;
lockers[i] = new object();
works[i] = (i == 0); //only the main thread is marked that it works
}
}
StartThreads()
:当我们启动一个服务时,还需要指定备用服务的 ID。实现方式是,当我们达到最后一个可能的服务(文件)时,将备用服务指定为前一个服务。如有必要,可以在此处应用其他逻辑。
private void StartThreads()
{
for (int i = 0; i < ServicesCount; i++)
{
if (i == ServicesCount - 1)
{
new Thread(Work).Start(new CurrentAndNextService(i, i - 1));
}
else
{
new Thread(Work).Start(new CurrentAndNextService(i, i + 1));
}
}
}
InitTimers()
:初始化与线程对应的所有计时器。主线程计时器 - id=0 设置为 100 ms,所有其他计时器设置为 2000ms = 2 秒。初始化后,为所有线程调用 Tick 事件。
private void InitTimers()
{
for (var i = 0; i < ServicesCount; i++)
{
_testServiceTimers.Add(i == 0 ? new ServiceTimer(i, 100) :
new ServiceTimer(i, 2000), OnElapsedTime);
// the main thread is released every 100 ms the other times elapsed on every 2000 ms
}
for (var i = 0; i < ServicesCount; i++)
{
_testServiceTimers.Tick(i);
}
}
StopExecutionOfAllLockers()
:将所有线程的 exit[]
值设置为 true - 这意味着线程已准备好退出。在模拟中,这在主线程的第 9997 次执行时完成。如果这是一个 Windows 服务应用程序,我们可以在用户调用停止事件时调用它。
public void StopExecutionOfAllLockers()
{
for (var i = 0; i < lockers.Length; i++)
{
lock (lockers[i])
{
exit[i] = true;
Monitor.Pulse(lockers[i]);
}
}
}
PulseFirstWorkingLocker()
:释放标记为工作(works[i]=true
)的线程。更改 works 值以供其他(优先级较低的)线程检查服务是否已恢复的计时器 - 工作正常(请检查 ActiveSwitcher.cs 中的 OnElapsedTime
事件)。
public void PulseFirstWorkingLocker()
{
for (var i = 0; i < works.Length; i++)
{
lock (MainLocker)
{
if (works[i])
{
lock (lockers[i])
{
Monitor.Pulse(lockers[i]);
break;
}
}
}
}
}
CurrentAndNextService
类顾名思义,包含当前服务和下一个服务的 ID - 这在 StartThreads()
方法中使用。
public class CurrentAndNextService
{
/// <summary>
/// Gets or sets the current service identifier.
/// </summary>
/// <value>
/// The current service identifier.
/// </value>
public int CurrentServiceId { get; set; }
/// <summary>
/// Gets or sets the next service identifier.
/// </summary>
/// <value>
/// The next service identifier.
/// </value>
public int NextServiceId { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="CurrentAndNextService"/> class.
/// </summary>
/// <param name="currentServiceId">The current service identifier.</param>
/// <param name="nextServiceId">The next service identifier.</param>
public CurrentAndNextService(int currentServiceId, int nextServiceId)
{
CurrentServiceId = currentServiceId;
NextServiceId = nextServiceId;
}
}
我将服务 ID 传递给“Work”方法的输入参数。该方法作为新线程启动。因此,服务 ID 以 object 类型传递。似乎这是创建新线程时唯一合适的类型。在 Work 方法内部,有一个无限循环,只有当线程被标记为 exit[]= true; 时才会停止。否则,它只会将线程的 ID 打印到控制台。
private void Work(object currentSercice)
{
var currentAndNext = (CurrentAndNextService)currentSercice;
//use the locker of the current service
lock (lockers[currentAndNext.CurrentServiceId])
{
while (true)
{
Monitor.Wait(lockers[currentAndNext.CurrentServiceId]);
if (!exit[currentAndNext.CurrentServiceId])
{
Console.Write((currentAndNext.CurrentServiceId).ToString(CultureInfo.InvariantCulture));
//check if the current service is working correctly
if (Check(currentAndNext.CurrentServiceId))
{
works[currentAndNext.CurrentServiceId] = true;
}
else
{
works[currentAndNext.CurrentServiceId] = false;
lock (lockers[currentAndNext.NextServiceId])
{
works[currentAndNext.NextServiceId] = true;
}
}
}
else
{
//if exit is true then go out of the while loop
//exit[] becomes true when the service is signaled to stop
Console.WriteLine("\nlastly called t" + currentAndNext.CurrentServiceId + "\n");
break;
}
}
}
}
已完成工作的评估
Concurrency Visualizer 是 Visual Studio 包的一部分。
为了测试目的,我将使应用程序在 100 次调用后停止。
线程启动时 - 模拟的第一部分
线程停止时 - 模拟的最后一部分
我们可以从可视化中得出什么结论?
大部分时间用于同步:95%。睡眠占 5%。我猜 DirectX GPU Engine 与控制台应用程序有关。查看可视化效果和百分比,我没有发现任何异常或意外之处。这里应该注意的是,当不同的文件存在时,进程切换是不同的。如果更多服务 - 文件不存在,会有更多的检查。