在使用英创ARM9系列嵌入式主板的COM口,CAN口,网口时,一般会使用到timer或线程来实现数据的接收。使用timer控件较为方便,通过InterVal值来设定调用间隔,但是灵活性不如线程。并且timer的Tick函数是并在主线程中,如果Tick函数中运算数据过于复杂,会导致主线程运行变慢,可能导致窗口卡死。使用C#中的线程类,可以非常方便的解决这个问题,线程卡死,不会影响到主线程的运算,就不会导致窗口卡死的状况发生。
本文将介绍如何使用C#来创建和关闭线程,并在此基础上,利用WinCE系统的消息机制实现通讯数据的实时收发,代替常规的定时查询方法,从而降低了CPU负载,使嵌入式设备的整体性能得以提高。
1、线程的应用实例
以下是一个简单的多线程代码:
using System;
using System.Threading;
namespace thread
{
class Program
{
static void Main(string[] args)
{
Thread t = new Thread(excute);
t.Start();
while (true) Console.Write('1'); // 主线程循环输出1
}
static void excute()
{
while (true) Console.Write('2'); // 线程t循环输出2
}
}
}
输出例子(并不唯一):12121212121212121212121212121212121212121212121212...
2、线程的使用方法
首先需要添加thread类的引用
using System.Threading;
初始一个线程类,并设定它的执行函数,该函数可以是静态函数,也可以是别的类的成员函数
Thread t = new Thread(excute);
执行start,线程即启动并运行它的执行函数,函数运行完毕后,线程自动退出
t.Start();
3、线程的数据同步
观察以下代码:
using System;
using System.Threading;
namespace thread
{
class Program
{
static int i;
static void Main(string[] args)
{
Thread t = new Thread(excute);
t.Start();
excute();
}
static void excute()
{
for (i = 0; i < 5; i++)
{
Console.Write('{0}', i);
}
}
}
}
这个程序的输出无法确定,可能是:001234。
这是因为在一个线程在使用一个变量时,另外一个线程也可能同时在使用。如果希望一个线程在使用某个变量时,禁止其他线程的使用,就需要用到线程锁lock。
修改代码为:
using System;
using System.Threading;
namespace thread
{
class Program
{
static readonly object locker = new object();
static int i;
static void Main(string[] args)
{
Thread t = new Thread(excute);
t.Start();
excute();
}
static void excute()
{
lock (locker)
{
for (i = 0; i < 5; i++)
{
Console.Write('{0}', i);
}
}
}
}
}
程序输出:0123401234。
注意lock的使用,见MSDN的说明:
1、不要锁定this,即禁止lock(this)
2、不要锁定类型,如lock (typeof (MyType))
3、不要锁定字符串,如lock('myLock')
4、最佳做法是定义private或 private static对象来锁定
锁定本身是很快,一个锁在堵塞的情况,任务切换带来的开销很低,使用锁可以有效避免一些数据错误,提高程序稳定性。
4、线程的结束
使用abort可以提前释放被阻塞的线程,使用join可以等待线程的结束:
using System;
using System.Threading;
namespace thread
{
class Program
{
static int i;
static void Main(string[] args)
{
Thread t = new Thread(excute);
t.Start();
// t.Abort();
t.Join();
for (i = 6; i < 10; i++)
{
Console.Write('{0}', i);
}
}
static void excute()
{
for (i = 0; i < 5; i++)
{
Console.Write('{0}', i);
}
}
}
}
程序输出:0123456789。
如果取消Abort的注释,程序的输出可能是:6789。
在主线程中关闭副线程一般步骤为,终止副线程,再等待确认该线程退出,在主程序退出的时间同样需要执行检测副线程的关闭:
t.Abort();
t.Join();
5、带参数的线程
有时候希望在添加的线程中传入指定的参数。
最简单的办法是把类封装在类中,让线程的执行函数为类的成员函数,然后通过设置类的成员变量,执行函数访问成员变量这样的办法来实现指定执行函数参数的功能,例程如下:
using System;
using System.Threading;
namespace thread
{
class ThreadClass
{
public int x;
public void excute()
{
while (true) { Console.WriteLine('{0}', x); }
}
}
class Program
{
static void Main(string[] args)
{
ThreadClass TClass1 = new ThreadClass();
TClass1.x = 1;
ThreadClass TClass2 = new ThreadClass();
TClass2.x = 2;
Thread t1 = new Thread(TClass1.excute);
Thread t2 = new Thread(TClass2.excute);
t1.Start();
t2.Start();
}
}
}
还有一个另外的办法,使用ParameterizedThreadStart。
C#提供2种委托,ThreadStart和ParameterizedThreadStart,ParameterizedThreadStart允许传入一个参数Object,可以将所需参数打包后调用。
注意:wince使用的是.net精简库,不包含ParameterizedThreadStart,如果在wince下编程,请使用第一种方法。
6、线程的挂起和唤醒
当线程创建后,就将占用一定的CPU时间,可以使用Sleep函数让线程放弃一定时间片,进入休眠状态,在休眠状态下,线程将不再占用CPU时间,如:
Thread.Sleep(0); // 释放CPU时间片
Thread.Sleep(1000); // 休眠1000毫秒
Thread.Sleep(Timeout.Infinite); // 休眠直到被唤醒
使用线程的Interrupt方法可以强行唤醒休眠中的线程,注意,wince的.net精简库里,Thread类没有Interrupt方法,所以在嵌入式设备中开发时不要无限休眠线程,即Sleep(-1)。
7、线程的消息事件响应
有的时候需要在线程中轮询执行一个函数,如通信接口的接收函数。使用轮循的方式将非常浪费CPU时间。
private void BeginReceive() // 客户机状态下接收数据线程
{
while (!threadStop)
{
// 线程接收函数
}
}
在接收线程中加入适当休眠可以提高CPU效率,这里Sleep的x越大,CPU效率越高,但是可能造成数据处理的延时。
private void BeginReceive() // 客户机状态下接收数据线程
{
while (!threadStop)
{
// 线程接收函数
Thread.Sleep(x); // 轮询休眠
}
}
为了避免通讯数据接收的延时,线程还可采用等待数据接收事件的方式,线程在平时挂起,直到有数据接收的事件产生。
C#提供一套事件类,可以让线程进入等待状态,直到该事件到来,线程在等待时不会消耗CPU资源。
using System;
using System.Threading;
namespace thread
{
class Program
{
static AutoResetEvent evt;
static void Main(string[] args)
{
evt = new AutoResetEvent(false);
Thread t = new Thread(excute);
t.Start();
Thread.Sleep(10000);
evt.Set();
}
static void excute()
{
for ( ; ; )
{
evt.WaitOne();
Console.Write('event');
}
}
}
}
设定一个事件
static AutoResetEvent evt;
在线程等待该事件的时候挂起
evt.WaitOne();
直到该事件Set产生,线程才继续执行下面的代码:
evt.Set();
还可以设置等待的时间长短,当有事件产生,WaitOne函数立刻返回true,如果等待时间超过设置时间,WaitOne也会返回,返回值false。
using System;
using System.Threading;
namespace thread
{
class Program
{
static AutoResetEvent evt;
static void Main(string[] args)
{
evt = new AutoResetEvent(false);
Thread t = new Thread(excute);
t.Start();
evt.Set();
Thread.Sleep(1000);
evt.Set();
Thread.Sleep(10000);
evt.Set();
}
static void excute()
{
bool b;
for (; ; )
{
b = evt.WaitOne(1000, false);
Console.Write('{0}', b.ToString);
}
}
}
}
注意:WaitOne第二个参数一般设置为false。
但是使用C#的事件类可能有一定局限性,它需要在同一进程里,有一些情况无法满足需要。这时候可以使用系统的API函数来解决这个问题,参看以下代码。
using System;
using System.Threading;
using System.Runtime.InteropServices;
namespace thread
{
class Program
{
[DllImport('coredll.dll', EntryPoint = 'WaitForSingleObject')]
private static extern int WaitForSingleObject(int hHandle, int dwMilliseconds);
[DllImport('coredll.dll', EntryPoint = 'CreateEvent')]
private static extern int CreateEvent(int lpEventAttributes, int bManualReset, int bInitialState, int lpName);
[DllImport('coredll.dll', EntryPoint = 'EventModify')]
private static extern bool EventModify(int h, int i);
[DllImport('coredll.dll', EntryPoint = 'WaitForMultipleObjects')]
private static extern int WaitForMultipleObjects(uint nCount, int[] lpHandles, int bWaitAll, int dwMilliseconds);
[DllImport('coredll.dll', EntryPoint = 'CloseHandle')]
private static extern int CloseHandle(int hObject);
static int hEvt;
static void Main(string[] args)
{
hEvt = CreateEvent(0, 1, 0, 0); // CreateEvent(NULL,TRUE,FALSE,NULL)
EventModify(hEvt, 2); // ResetEvent(hEvt);
Thread t = new Thread(excute);
t.Start();
Thread.Sleep(1000);
EventModify(hEvt, 3); // SetEvent(hEvt);
Thread.Sleep(10000);
EventModify(hEvt, 3); // SetEvent(hEvt);
CloseHandle(hEvt);
}
static void excute()
{
int i;
for (; ; )
{
i = WaitForSingleObject(hEvt, -1); // 无限等待
// i = WaitForSingleObject(hEvt, 1000); // 等待1秒
EventModify(hEvt, 2); // ResetEvent(hEvt);
Console.Write('event');
}
}
}
}
这里使用了API函数,所以需要添加引用
using System.Runtime.InteropServices;
通过CreateEvent创建一个事件,并获得该事件句柄。这里参数一般使用(NULL,TRUE,FALSE,NULL),即(0, 1, 0, 0)
通过EventModify(hEvt, 2)将该事件的信号设置为无信号,该函数第一个参数为设置的事件句柄,第二个参数为2表示ResetEvent,第二个参数为3表示SetEvent。
hEvt = CreateEvent(0, 1, 0, 0); // CreateEvent(NULL,TRUE,FALSE,NULL)
EventModify(hEvt, 2); // ResetEvent(hEvt);
在线程中调用WaitForSingleObject函数等待事件,第一个参数为等待的事件句柄,第二个参数为等待的时间,如果为INFINITE即-1,表示一直等待,直到收到事件消息。该函数返回0表示接收到消息,返回0x102表示未接收到消息等待超时
i = WaitForSingleObject(hEvt, -1); // 无限等待
当主线程执行SetEvent即EventModify(hEvt, 3)时,挂起的副线程将被激活
EventModify(hEvt, 3); // SetEvent(hEvt);
在接收到信号的处理代码里,需要重新将事件设置为未激活状态,否则WaitForSingleObject函数将判定事件为激活状态,不再发生等待
EventModify(hEvt, 2); // ResetEvent(hEvt);
在程序结束处,记得用CloseHandle关闭创建的事件
CloseHandle(hEvt);
使用API函数的事件响应与使用C#的事件类作用相同,因为使用了句柄做事件的标志,就可以与C的代码进行交互,以英创ARM9系列嵌入式主板EM9161的CAN口数据接收线程为例。
设定一个线程用于CAN口的接收,创建一个事件用于通知线程关闭
private Thread revThread;
hCloseEvent = CreateEvent(0, 1, 0, 0); // CreateEvent(NULL,TRUE,FALSE,NULL)
打开CAN口后,通过COM组件接口函数获得CAN的消息事件句柄
hEvent = CAN.CAN_GetRxEvent(hCAN);
hErr = CAN.CAN_GetErrorEvent(hCAN);
设定一个接收线程专门处理CAN口接收。
revThread = new Thread(new ThreadStart(BeginReceive));
threadStop = false;
revThread.Start(); // 启动waitforMessage线程
在接收函数中,执行等待,直到有CAN口接收消息到来,或是接收到线程关闭的事件。
private void BeginReceive() // 客户机状态下接收数据线程
{
int[] handles = new int[2];
handles[0] = hCloseEvent;
handles[1] = hEvent;
int i;
bool bResult;
string revstr;
while (!threadStop)
{
// WaitForSingleObject(hEvent, 200);
i = WaitForMultipleObjects(2, handles, 0, -1); // handles里的两个事件hEvent和hCloseEvent
// ….其他的处理代码
}
}
这里使用了WaitForMultipleObjects来同时等待2个事件,第一个参数为等待的事件数。第二个参数为各事件的数组。第三个参数为FALSE即0表示当任何一个事件产生,该函数即返回,第三个参数为TRUE即1表示只有当所有事件都产生,该函数才返回。最后个参数为等待的时间。返回值为0x102表示超时,返回0-X表示接收的事件在数组中的位置,同时接收多个事件,返回的第一个事件在数组中的位置。
更详细的完整代码,请参考英创ARM9系列嵌入式主板EM9161的CAN事件接口例程。
8、等待线程
C#使用Thread类的Join函数来等待一个线程
using System;
using System.Threading;
namespace thread
{
class Program
{
static int i;
static void Main(string[] args)
{
Thread t = new Thread(excute);
t.Start();
for (i = 0; i < 10; i++)
{
Console.Write('2');
}
t.Join();
// t.Join(1000);
for (; ; )
{
Console.Write('2');
}
}
static void excute()
{
for (; ;)
{
Console.Write('1');
}
}
}
}
该函数不带参数表示一直等待到线程结束,带参数表示等待的时间,返回true表示线程已结束,返回false表示线程还在运行,只是超时返回。
在主函数关闭前,应使用Join函数来确保各支线程已完全关闭,否则会导致进程无法完全关闭。
9、其他
在关闭程序进程时,请确保关闭所有创建的线程,否则进程将无法完全关闭,并一直占用系统资源。在英创ARM9系列嵌入式主板程序开发中,可以结合VS自带的远程线程查看工具进行程序调试。
全部0条评论
快来发表一下你的评论吧 !