Chemmy's Blog

chengming0916@outlook.com

  1. WinForm程序

1)第一种方法,使用委托:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

private delegate void SetTextCallback(string text);
private void SetText(string text)
{
// InvokeRequired需要比较调用线程ID和创建线程ID
// 如果它们不相同则返回true
if (this.txt_Name.InvokeRequired)
{
SetTextCallback d = new SetTextCallback(SetText);
this.Invoke(d, new object[] { text });
}
else {
this.txt_Name.Text = text;
}
}

2)第二种方法,使用匿名委托

1
2
3
4
5
6
7
8
9
10
11
12
private void SetText(Object obj)
{
if (this.InvokeRequired)
{
this.Invoke(new MethodInvoker(delegate {
this.txt_Name.Text = obj;
}));
}
else {
this.txt_Name.Text = obj;
}
}

这里说一下BeginInvoke和Invoke和区别:BeginInvoke会立即返回,Invoke会等执行完后再返回。

  1. WPF程序

1)可以使用Dispatcher线程模型来修改

如果是窗体本身可使用类似如下的代码:

this.lblState.Dispatcher.Invoke(new Action(delegate
{
this.lblState.Content = “状态:” + this._statusText;
}));

那么假如是在一个公共类中弹出一个窗口、播放声音等呢?这里我们可以使用:System.Windows.Application.Current.Dispatcher,如下所示

复制代码

System.Windows.Application.Current.Dispatcher.Invoke(new Action(() =>
{
if (path.EndsWith(“.mp3”) || path.EndsWith(“.wma”) || path.EndsWith(“.wav”))
{
_player.Open(new Uri(path));
_player.Play();
}
}));

关键问题:多个视频同时播放,以上几种方法不足以解决,多个视频播放中主界面卡死和播放显示刷新不了的问题。

目前笔者的解决方法是

 pinturebox.CreateGraphics().DrawImage(imgSrc.Bitmap, new System.Drawing.Rectangle(0, 0, pinturebox.Width, pinturebox.Height));

EmguCV中的Capture类可以完成视频文件的读取,并捕捉每一帧,可以利用Capture类完成实现WinForm中视频检测跟踪环境的搭建。本文只实现最简陋的WinForm + EmguCV上的avi文件读取和播放框架,复杂的检测和跟踪算法在之后添加进去。

        这里使用WinForm实现视频的播放,主要是PictureBox类,它是支持基于事件的异步模式的典型组件,不使用EmguCV自带的UI控件等。

效果图

图1.效果图

        直接在UI线程中完成视频的播放的话整个程序只有一个线程,由于程序只能同步执行,播放视频的时候UI将停止响应用户的输入,造成界面的假死。所以视频的播放需要实现异步模式。主要有三种方法:第一是使用异步委托;第二种是使用BackgroundWorker组件;最后一种就是使用多线程(不使用CheckForIllegalCrossThreadCalls =false的危险做法)。

        Windows窗体控件,唯一可以从创建它的线程之外的线程中调用的是Invoke()、BegionInvoke()、EndInvoke()方法和InvokeRequired属性。其中BegionInvoke()、EndInvoke()方法是Invoke()方法的异步版本。这些方法会切换到创建控件的线程上,以调用赋予一个委托参数的方法,该委托参数可以传递给这些方法。

        (一)   使用多线程
        首先定义监控的类及其对应的事件参数类和异常类:
        判断是否继续执行的布尔型成员会被调用线程改变,因此声名为volatile,不进行优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
/// <summary>
/// 红外检测子。
/// </summary>
public class ThermalSurveillant
{
#region Private Fields

/// <summary>
/// 是否停止线程,此变量供多个线程访问。
/// </summary>
private volatile bool shouldStop = false;

#endregion
#region Public Properties

#endregion
#region Public Events

/// <summary>
/// 帧刷新事件。
/// </summary>
public EventHandler<FrameRefreshEventArgs> FrameRefresh;

/// <summary>
/// 播放完成。
/// </summary>
public EventHandler<CompletedEventArgs> Completed;

#endregion
#region Protected Methods

/// <summary>
/// 处理帧刷新事件。
/// </summary>
/// <param name="e"></param>
protected virtual void OnFrameRefresh(FrameRefreshEventArgs e)
{
if (this.FrameRefresh != null)
{
this.FrameRefresh(this, e);
}
}

/// <summary>
/// 处理视频读完事件。
/// </summary>
/// <param name="e"></param>
protected virtual void OnCompleted(CompletedEventArgs e)
{
if (this.Completed != null)
{
this.Completed(this, e);
}
}

#endregion
#region Public Methods

/// <summary>
/// 视频监控。
/// </summary>
/// <param name="capture">捕捉。</param>
public void DoSurveillance(Object oCapture)
{
Capture capture = oCapture as Capture;
int id = 1;
if (capture == null)
{
throw new InvalidCaptureObjectException("传递的Capture类型无效。");
}
while (!shouldStop)
{
Image<Bgr, byte> frame = capture.QueryFrame();
if (frame != null)
{
FrameRefreshEventArgs e = new FrameRefreshEventArgs(frame.ToBitmap(), id++);
// 触发刷新事件
this.OnFrameRefresh(e);
}
else
{
break;
}
}
// 触发完成事件
this.OnCompleted(new CompletedEventArgs(id));
}

/// <summary>
/// 请求停止线程。
/// </summary>
public void Cancel()
{
this.shouldStop = true;
}

#endregion
}

        UI线程中启动播放线程:

声明:

1
2
3
4
5
6
7
8
9
10
11
12
/// <summary>
/// 监控线程。
/// </summary>
private Thread threadSurveillance = null;
/// <summary>
/// 捕获视频帧。
/// </summary>
private Capture captureSurveillance;
/// <summary>
/// 监控子。
/// </summary>
private ThermalSurveillant surveillant = new ThermalSurveillant();

读入视频文件:

1
2
3
4
5
captureSurveillance = new Capture(this.videoFilePath);
captureSurveillance.SetCaptureProperty(CAP_PROP.CV_CAP_PROP_FRAME_WIDTH, this.width);
captureSurveillance.SetCaptureProperty(CAP_PROP.CV_CAP_PROP_FRAME_HEIGHT, this.height);
Image<Bgr, byte> frame = captureSurveillance.QueryFrame();
this.pictureBox.Image = frame.ToBitmap();

播放视频文件:

        UI线程中响应监控类的事件:

定义异步调用的委托:

添加事件委托:

1
2
this.surveillant.FrameRefresh += OnRefreshFrame;
this.surveillant.Completed += OnCompleted;

        以下方法中都是由监控线程中的事件委托方法,应该使用BeginInvoke方法,这样可以优雅的结束线程,如果使用Invoke方法,则调用方式为同步调用,此时如果使用Thread.Join()方法终止线程将引发死锁(正常播放没有问题),Thread.Join()方法的使用使调用线程阻塞等待当前线程完成,在这里即UI线程阻塞等待监控线程完成,而监控线程中又触发UI线程中pictureBox的刷新,使用Invoke方法就造成了监控线程等待UI线程刷新结果,而UI线程已经阻塞,形成了死锁。死锁时只能用Thread.Abort()方法才能结束线程。或者直接强制结束应用程序。

        使用BeginInvoke方法时为异步调用,监控线程不等待刷新结果直接继续执行,可以正常结束。结束后UI才进行刷新,不会造成死锁。

图2.线程关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/// <summary>
/// 刷新UI线程的pixtureBox的方法。
/// </summary>
/// <param name="frame">要刷新的帧。</param>
private void RefreshFrame(Bitmap frame)
{
this.pictureBox.Image = frame;
// 这里一定不能刷新!2012年8月2日1:50:16
//this.pictureBox.Refresh();
}


/// <summary>
/// 响应pictureBox刷新。
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnRefreshFrame(object sender, FrameRefreshEventArgs e)
{
// 判断是否需要跨线程调用
if (this.pictureBox.InvokeRequired == true)
{
FrameRefreshDelegate fresh = this.RefreshFrame;
this.BeginInvoke(fresh, e.Frame);
}
else
{
this.RefreshFrame(e.Frame);
}
}


/// <summary>
/// 响应Label刷新信息。
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnCompleted(object sender, CompletedEventArgs e)
{
// 判断是否需要跨线程调用
CompletedDelegate fresh = this.RefreshStatus;
string message = "视频结束,共 " + e.FrameCount + " 帧。";
this.BeginInvoke(fresh, message);
}
  

关闭时需要中止播放线程之后再退出:

/// <summary>
/// 关闭窗体时发生。
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnFormClosed(object sender, FormClosedEventArgs e)
{
// 检测子算法请求终止
surveillant.Cancel();

// 阻塞调用线程直到检测子线程终止
if (threadSurveillance != null)
{
if (threadSurveillance.IsAlive == true)
{
threadSurveillance.Join();
}
}
}

        (二)   使用异步委托

        创建线程的一个更简单的方法是定义一个委托,并异步调用它。委托是方法的类型安全的引用。Delegate类还支持异步地调用方法。在后台,Delegate类会创建一个执行任务的线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// asynchronous by using a delegate
PlayVideoDelegate play = this.PlayVideoFile;
IAsyncResult status = play.BeginInvoke(null, null);

/// <summary>
/// 播放视频文件。
/// </summary>
private void PlayVideoFile()
{
while (true)
{
Image<Bgr, byte> frame = capture.QueryFrame();
if (frame != null)
{
Image<Gray, byte> grayFrame = frame.Convert<Gray, byte>();
grayFrame.Resize(this.width, this.height, INTER.CV_INTER_CUBIC);
RefreshPictureBoxDelegate fresh = this.RefreshPictureBox;
try
{
this.BeginInvoke(fresh, grayFrame.ToBitmap());
}
catch (ObjectDisposedException ex)
{
Thread.CurrentThread.Abort();
}
}
else
{
break;
}
}
}

/// <summary>
/// 刷新UI线程的pixtureBox的方法。
/// </summary>
/// <param name="frame">要刷新的帧。</param>
private void RefreshPictureBox(Bitmap frame)
{
this.pictureBox.Image = frame;
}

        (三)   使用BackgroundWorker组件

        BackgroundWorker类是异步事件的一种实现方案,异步组件可以选择性的支持取消操作,并提供进度信息。RunWorkerAsync()方法启动异步调用。CancelAsync()方法取消。

图3.BackgroundWorker组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/// <summary>
/// 播放视频文件。
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void detectItemPlay_Click(object sender, EventArgs e)
{
if (this.videoFilePath != null)
{
// run async
this.backgroundWorker.RunWorkerAsync(capture);
}
}

/// <summary>
/// 异步调用。
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnDoWork(object sender, DoWorkEventArgs e)
{
Emgu.CV.Capture capture = e.Argument as Emgu.CV.Capture;
while (!e.Cancel)
{
Image<Bgr, byte> frame = capture.QueryFrame();
if (frame != null)
{
Image<Gray, byte> grayFrame = frame.Convert<Gray, byte>();
grayFrame.Resize(this.width, this.height, INTER.CV_INTER_CUBIC);
if (this.backgroundWorker.CancellationPending == true)
{
e.Cancel = true;
break;
}
else
{
if (this.pictureBox.InvokeRequired == true)
{
RefreshPictureBoxDelegate fresh = this.RefreshPictureBox;
this.BeginInvoke(fresh, grayFrame.ToBitmap());
}
else
{
this.RefreshPictureBox(grayFrame.ToBitmap());
}
}
}
else
{
break;
}
}
}

/// <summary>
/// 关闭窗体时发生。
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void MainForm_FormClosed(object sender, FormClosedEventArgs e)
{
if (this.backgroundWorker.IsBusy)
{
this.backgroundWorker.CancelAsync();
}
}

在 WPF 中总会修改 Button 的 Style,比如一个自定义的 Close 按钮。刚入门的可能会用一张 PNG 格式的图片来做这个按钮的 Icon,但这个是不优雅的。而且你要改的时候还得去操作文件,想想都痛苦。

但是很多人苦于不知道去哪里获取 Path,当然网上已经有不少使用 Photoshop 获取图片的 Path ,但如果图片的质量不好,获取的 Path 歪歪曲曲的也不好看,更何况在这之前你还得会使用 Photoshop。

现在分享一个我经常使用的解决方案,阿里巴巴矢量图,这上面可以说有海量的图标可以用到。

流程:

  1,进入 阿里巴巴矢量图 并搜索你想要的图标

  2,下载 Icon 时使用 SVG 下载

  3,用记事本或文本编辑器打开,标签 Path 下的 d 属性就是 Path 的 Data 数据(很多复杂一点的 Icon 可能是多个 Data 组成,使用时只要用空格把几个 Data 隔开就行)

  例子:

  <svg t=“1491032725422” class=“icon” style=“” viewBox=“0 0 1024 1024” version=“1.1” xmlns=“http://www.w3.org/2000/svg“ p-id=“2372” xmlns:xlink=“http://www.w3.org/1999/xlink“ width=“248” height=“248”>
  <defs>
    <style type=“text/css”></style>
  </defs>
  <path d=“M503.2868 510.9903m-349.4226 0a341.233 341.233 0 1 0 698.8452 0 341.233 341.233 0 1 0-698.8452 0Z” p-id=“2373”></path>
  <path d=“M106.1386 263.9677a110 100 0 1 1 121.6696 248.2668Z” p-id=“2374”></path>
</svg>

  在WPF中使用时:

<Path Data=“M503.2868 510.9903m-349.4226 0a341.233 341.233 0 1 0 698.8452 0 341.233 341.233 0 1 0-698.8452 0Z M106.1386 263.9677a110 100 0 1 1 121.6696 248.2668Z”/>

Data 也可以作为资源放在独立的资源字典里,使用的 Geometry 标签

<Geometry x:Key=“logo”>M503.2868 510.9903m-349.4226 0a341.233 341.233 0 1 0 698.8452 0 341.233 341.233 0 1 0-698.8452 0Z M106.1386 263.9677a110 100 0 1 1 121.6696 248.2668Z</Geometry>

XAML:

<Path Data=“{StaticResource logo}” Fill=“White” Stretch=“Fill” Stroke=“White” StrokeThickness=“1.5” />

介绍

在实际项目使用中quartz.net中,都希望有一个管理界面可以动态添加job,而避免每次都要上线发布。 

也看到有园子的同学问过。这里就介绍下实现动态添加job的几种方式, 也是二次开发的核心模块。

阅读目录:

  1. 传统方式
  2. 框架反射方式
  3. 进程方式
  4. URL方式
  5. 框架配置方式

传统方式

 继承IJob,实现业务逻辑,添加到scheduler。

public class MonitorJob : IJob
{ public void Execute(IJobExecutionContext context)
{ //do something
Console.WriteLine(“test”);
}
} //var job = JobBuilder.Create() // .WithIdentity(“test”, “value”) // .Build(); //var trigger = (ICronTrigger) TriggerBuilder.Create() // .WithIdentity(“test”, “value”) // .WithCronSchedule(“0 0/5 * * * ?”) // .Build(); //scheduler.ScheduleJob(job, trigger);

也可以使用CrystalQuartz远程管理暂停取消。之前的博客CrystalQuartz远程管理(二)

框架反射方式

这种方式需要定义一套接口框架。 比如:

interface IcustomJob
{ void Excute(string context); void Failed(string error); void Complete(string msg);
}

1:当我们写job时同一实现这个框架接口,类库形式。

2:写完后编译成DLL,上传到我们的作业执行节点。

3:在执行节点中,通过反射拿到DLL的job信息。

4:然后构建quartz的job,添加到scheduler。

这种方式缺点: 耦合性太高,开发量较大。 优点:集中式管理。

系统结构如图:

进程方式

这个方式和windows任务计划类似。

1:使用方编写自己的job,无需实现任何接口,可执行应用程序形式。

2:将程序发送到执行节点,由执行节点起进程调用job程序。

执行节点调用,示例如下:

public class ConsoleJob:IJob
{ public void Execute(IJobExecutionContext context)
{
JobDataMap dataMap = context.JobDetail.JobDataMap; string content = dataMap.GetString(“jobData”); var jd = new JavaScriptSerializer().Deserialize(content);

        Process p \= new Process();
        p.StartInfo.UseShellExecute \= true;
        p.StartInfo.FileName \= jd.Path;
        p.StartInfo.Arguments \= jd.Parameters;   //空格分割
        p.StartInfo.WindowStyle = ProcessWindowStyle.Minimized;
        p.Start();
    }
}

这种方式相对来说: 耦合性中等,执行节点和job相互不关心,没有依赖,开发量较小。

系统结构如图:

URL方式

URL方式和第三种类似,不过调用的不在是执行程序,而是URL。

1: 使用方在网页或服务中,实现业务逻辑。

2: 然后将Url,交给执行节点post或get执行。

执行节点调用,示例如下:

public class HttpJob : IJob
{ public void Execute(IJobExecutionContext context)
{ var dataMap = context.JobDetail.JobDataMap; var content = dataMap.GetString(“jobData”); var jd = new JavaScriptSerializer().Deserialize(content); if (jd.Parameters == null)
jd.Parameters = string.Empty; if (jd.Timeout == 0)
jd.Timeout = 5*60; var result = RequestHelper.Post(jd.Url, jd.ContentType, jd.Timeout, jd.Parameters, jd.heads);
}
}

这种方式耦合比较低,使用方不需要单独写应用程序了,和平常业务开发一样。

执行节点的职权,仅仅作为一个触发器。

有2点需要注意的是:

1:请求URL时,注意双方约定token加密,防止非执行节点执行调用。

2:使用方,如果有耗时操作,建议异步执行。 

系统结构如图:

框架配置方式

1:使用方直接使用quartz.net框架,实现自己的job。从管理方拉取执行节点配置,然后自行管理执行节点。

2:使用方也可以暴露端口给管理方,以实现监控,修改配置。

这种形式,耦合性最低。是把管理方当成一个配置中心。 ps:几乎和传统方式+CrystalQuartz一样了。

通过context.JobDetail.JobDataMap,可以保存job的需要的信息。

本篇介绍主流的几种实现方案,供大家参考使用。

介绍

在实际使用quartz.net中,持久化能保证实例重启后job不丢失、 集群能均衡服务器压力和解决单点问题。

quartz.net在这两方面配置都比较简单。

持久化

quartz.net的持久化,是把job、trigger一些信息存储到数据库里面,以解决内存存储重启丢失。

下载sql脚本

           https://github.com/quartznet/quartznet/blob/master/database/tables/tables\_sqlServer.sql

创建个数据库,并执行脚本

  QRTZ_BLOB_TRIGGERS  以Blob 类型存储的触发器。

  QRTZ_CALENDARS   存放日历信息, quartz.net可以指定一个日历时间范围。

  QRTZ_CRON_TRIGGERS  cron表达式触发器。

  QRTZ_JOB_DETAILS      job详细信息。

  QRTZ_LOCKS       集群实现同步机制的行锁表

  QRTZ_SCHEDULER_STATE   实例信息,集群下多使用。

quartz.net 配置

//===持久化==== //存储类型
properties[“quartz.jobStore.type”] = “Quartz.Impl.AdoJobStore.JobStoreTX, Quartz”; //表明前缀
properties[“quartz.jobStore.tablePrefix”] = “QRTZ_“; //驱动类型
properties[“quartz.jobStore.driverDelegateType”] = “Quartz.Impl.AdoJobStore.SqlServerDelegate, Quartz”; //数据源名称
properties[“quartz.jobStore.dataSource”] = “myDS”; //连接字符串
properties[“quartz.dataSource.myDS.connectionString”] = @”Data Source=(local);Initial Catalog=JobScheduler;User ID=sa;Password=123465”; //sqlserver版本
properties[“quartz.dataSource.myDS.provider”] = “SqlServer-20”;

启动客户端

var properties = JobsManager.GetProperties(); var schedulerFactory = new StdSchedulerFactory(properties);
scheduler = schedulerFactory.GetScheduler();
scheduler.Start(); //var job = JobBuilder.Create() // .WithIdentity(“test”, “value”) // .Build(); //var trigger = (ICronTrigger) TriggerBuilder.Create() // .WithIdentity(“test”, “value”) // .WithCronSchedule(“0 0/5 * * * ?”) // .Build(); //scheduler.ScheduleJob(job, trigger);

补充

     1: 持久化后,job只有添加一次了(数据库已经有了),所以不能再执行端写添加job的行为。这时候需要一个管理工具,动态添加操作。

     2: quartz.net 支持sql server、sqlite、mysql、oracle、mongodb(非官方版)。

部署图:

 

如图quartz.net 的集群模式是依赖数据库表的,所以要持久化配置。  集群节点之间是不通信的,这样分布式的架构,很方便进行水平扩展。

1: 除了线程池数量,instanceId可以不同外,各个节点的配置必须是一样的。

2:集群中节点的系统时间一致。  

3:多线程、集群中。quartz.net 利用数据库锁来保证job不会重复执行。

     源码在DBSemaphore.cs、UpdateLockRowSemaphore.cs、StdRowLockSemaphore.cs

4:集群化后,某节点失效后,剩余的节点能保证job继续执行下去。

实例配置后启动。

//cluster
properties[“quartz.jobStore.clustered”] = “true”;
properties[“quartz.scheduler.instanceId”] = “AUTO”;

简单管理界面:

介绍

前面介绍Quartz.Net的基本用法,但在实际应用中,往往有更多的特性需求,比如记录job执行的执行历史,发邮件等。

阅读目录

  1. Quartz.Net插件
  2. TriggerListener,JobListener
  3. Cron表达式
  4. Quartz.Net线程池
  5. 总结

Quartz.Net插件

     Quartz.net 自身提供了一个插件接口(ISchedulerPlugin)用来增加附加功能,看下官方定义:

1

2

3

4

5

6

7

8

public interface ISchedulerPlugin

{

void Initialize(``string pluginName, IScheduler sched);

void Shutdown();

void Start();

}

 继承接口,实现自己的插件。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

public class MyPlugin : ISchedulerPlugin

{

public void  Initialize(``string pluginName, IScheduler sched)

{

Console.WriteLine(``"实例化"``);

}

public  void Start()

{

Console.WriteLine(``"启动"``);

}

public  void Shutdown()

{

Console.WriteLine(``"关闭"``);

}

}

  主函数里面配置要实现的插件,注释部分,一句话搞定。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

static void Main(``string``[] args)

{

var properties = new NameValueCollection();

properties[``"quartz.plugin.MyPlugin.type"``] = "QuartzDemo3.MyPlugin,QuartzDemo3"``;

var schedulerFactory = new StdSchedulerFactory(properties);

var scheduler = schedulerFactory.GetScheduler();

var job = JobBuilder.Create<HelloJob>()

.WithIdentity(``"myJob"``, "group1"``)

.Build();

var trigger = TriggerBuilder.Create()

.WithIdentity(``"mytrigger"``, "group1"``)

.WithCronSchedule(``"/2 * * ? * *"``)

.Build();

scheduler.ScheduleJob(job, trigger);

scheduler.Start();

Thread.Sleep(6000);

scheduler.Shutdown(``true``);

Console.ReadLine();

}

运行结果如下:

 

TriggerListener,JobListener

这2个是对触发器和job本身的行为监听器,这样更好方便跟踪Job的状态及运行情况。  

ITriggerListener是官方定义的接口,这里我们直接继承实现。 

public class MyTriggerListener : ITriggerListener
{ private string name; public void TriggerComplete(ITrigger trigger, IJobExecutionContext context, SchedulerInstruction triggerInstructionCode)
{
Console.WriteLine(“job完成时调用”);
} public void TriggerFired(ITrigger trigger, IJobExecutionContext context)
{
Console.WriteLine(“job执行时调用”);
} public void TriggerMisfired(ITrigger trigger)
{
Console.WriteLine(“错过触发时调用(例:线程不够用的情况下)”);
} public bool VetoJobExecution(ITrigger trigger, IJobExecutionContext context)
{ //Trigger触发后,job执行时调用本方法。true即否决,job后面不执行。
return false;
} public string Name { get { return name; } set { name = value; } }
}

主函数添加:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

scheduler.ListenerManager.AddTriggerListener(myJobListener, KeyMatcher<TriggerKey>.KeyEquals(``new TriggerKey(``"mytrigger"``, "group1"``)));

////添加监听器到指定分类的所有监听器。

////添加监听器到指定分类的所有监听器。

////添加监听器到指定的2个分组。

////添加监听器到所有的触发器上。

scheduler.Start();

JobListener同理,这里不多做描述。

Cron表达式

quartz中的cron表达式和Linux下的很类似,比如 “/5 * * ? * * *“ 这样的7位表达式,最后一位年非必选。

表达式从左到右,依此是秒、分、时、月第几天、月、周几、年。下面表格是要遵守的规范:

字段名

允许的值

允许的特殊字符

Seconds

0-59

, - * /

Minutes

0-59

, - * /

Hours

0-23

, - * /

Day of month

1-31

, - * ? / L W

Month

1-12 or JAN-DEC

, - * /

Day of week

1-7 or SUN-SAT

, - * ? / L #

Year

空, 1970-2099

, - * /

特殊字符

解释

,

或的意思。例:分钟位 5,10  即第5分钟或10分都触发。 

/

a/b。 a:代表起始时间,b频率时间。 例; 分钟位  3/5,  从第三分钟开始,每5分钟执行一次。

*

频率。 即每一次波动。    例;分钟位 *  即表示每分钟 

-

区间。  例: 分钟位   5-10 即5到10分期间。 

?

任意值 。   即每一次波动。只能用在DayofMonth和DayofWeek,二者冲突。指定一个另一个一个要用?

L

表示最后。 只能用在DayofMonth和DayofWeek,4L即最后一个星期三

W

工作日。  表示最后。 只能用在DayofWeek

4#2。 只能用DayofMonth。 某月的第二个星期三  

实例介绍

”0 0 10,14,16 * * ?”    每天10点,14点,16点 触发。

“0 0/5 14,18 * * ?”    每天14点或18点中,每5分钟触发 。

“0 4/15 14-18 * * ?”       每天14点到18点期间,  从第四分钟触发,每15分钟一次。

“0 15 10 ? * 6L”        每月的最后一个星期五上午10:15触发。

Quartz.Net线程池

线程池数量设置:

properties[“quartz.threadPool.threadCount”] = “5”;

这个线程池的设置,是指同时间,调度器能执行Job的最大数量。

quartz是用每个线程跑一个job。上面的设置可以解释是job并发时能执行5个job,剩下的job如果触发时间恰好到了,当前job会进入暂停状态,直到有可用的线程。

如果在指定的时间范围依旧没有可用线程,会触发misfired时间。

quartz 提供了IThreadPool接口,也可以用自定义线程池来实现。

配置如下:

properties[“quartz.threadPool.type”] = “Quartz.Simpl.SimpleThreadPool, Quartz”;

一般来说作业调度很少并发触发大量job,如果有上百个JOB,可在服务器承受范围内适量增加线程数量。     

总结

官方有LoggingTriggerHistoryPlugin,LoggingJobHistoryPlugin  已实现的,触发器和job历史记录的插件。

Quartz.Plugin 命名空间下有官方实现的其他一些插件,也可以自己增加扩展。

quartz中监听器还有SchedulerListener,使用方法基本一样。 

本文基于自用经验和官方文档代码来写的,部分是直接翻译的。 

Quartz.Net官方教程http://www.quartz-scheduler.net/documentation/quartz-2.x/tutorial/index.html

0%