英创信息技术嵌入式系统通讯线程的C#编程方法

描述

在使用英创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自带的远程线程查看工具进行程序调试。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分