关于C#跨线程操作和Pinvoke的一些总结

最近由于需求,买了一个Xtion Live Pro,因此必须使用OpenNI进行体感开发了。很久没关注了,现在才发现OpenNI已经更新到了2.1beta,接口有非常大的调整,因此过去的很多C#Wrapper都不能用了,而且目前网上还没有针对新版本的Wrapper。用C++开发虽然灵活强大,但还是希望借助WPF或WinForm的优势进行软件开发,因此打算自己实现一个Wrapper,同时希望这个Wrapper的接口和Kinect SDK的接口尽可能相似,能用于任何.Net项目。

这里记录一下实现过程中学到的和需要注意的东西。


一、P/Invoke:

在C#与C/C++编写的Dll进行互操作时,Pinvoke是避免不了的。在调用C/C++函数时,一些参数是指针,在C#当中可以使用以下一些方法:

1、使用ref关键字:

[DllImport("OpenNI2.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern OniStatus oniDeviceGetInfo(IntPtr device, ref OniDeviceInfo pInfo);
如上面的pInfo参数,需要一个指向OniDeviceInfo结构的指针,ref关键字的作用是传递引用,本质上就是指针。

使用时,在传递的参数前加上ref即可,如下:

OniDeviceInfo info = new OniDeviceInfo();
oniDeviceGetInfo(_deviceHandle, ref Info);


2、使用StructureToPtr方法:

如果参数是一个结构体指针(像上面那样),还可以使用Marshal类当中的StructureToPtr方法,该方法需要在非托管内存区分配一块内存,并将托管数据复制到非托管区,并返回一个指向非托管区的指针,当然,使用完之后要释放这块非托管内存。

VideoMode mode = new VideoMode {Fps = 30};
IntPtr pMode = Marshal.AllocHGlobal(Marshal.SizeOf(mode)); //分配非托管内存

Marshal.StructureToPtr(mode, pMode, false);
oniStreamSetProperty(_streamHandle, 3, pMode, Marshal.SizeOf(mode));

Marshal.FreeHGlobal(pMode); //释放非托管内存

3、使用unsafe和fixed:

当然最直接也是最接近原函数接口的方法就是使用真正的指针。

unsafe
{
    VideoMode* pMode = &mode;

    OniStatus oniStatus = oniStreamSetProperty(_streamHandle, 3, pMode, Marshal.SizeOf(mode));

}
如果要传递的是类的指针,还需要用fixed关键字将类固定。

unsafe
{
    fixed (NiteUserTrackerCallbacks* pHandler = &_callbackHandler) //固定对象
    {
        niteRegisterUserTrackerCallbacks(_streamHandle, pHandler, IntPtr.Zero);
    }
}

二、多线程访问:
等待新的帧到来时,为了不阻塞UI线程,常常开启一个或多个线程进行轮询或等待操作,因此难免会多个线程访问同一数据,造成访问冲突。常用两种方法解决这个问题。

1、使用lock语句:

lock语句需要一个对象作为锁(任何对象都可以,哪怕是一个Object类),所有需要同步的线程,在访问数据前都要使用lock语句锁住相同的锁。

lock (_locker)
{
    if (_streamHandle != IntPtr.Zero)
    {
        NiteWrapper.niteShutdownUserTracker(_streamHandle);
        _streamHandle = IntPtr.Zero;
    }

    IsOpened = false;
}
如上,lock语句中的代码同一时刻只能有一个线程执行(前提是使用的_locker指向同一对象)。


2、使用volatile关键字:

如果只是为了同步变量,比如一个列表类(List),可以在申明该变量时加上volatile关键字,如下:

private volatile List<ColorFrame> _frameList = new List<ColorFrame>(); 
这样在访问该变量时,即使不使用lock语句,也能避免多线程访问。


三、线程的退出:
对于一个轮询线程,最好的写法如下:

while (!_shouldStop)
{
    //Do anything you want.               
}
需要退出时,直接将_shouldStop变量设置为true即可,如下:

public void Stop()
{            
    _shouldStop = true;
    //Do something else.
}

如果是一个常驻线程,在程序运行过程中不需要停止的,可以简单的将该线程的IsBackground属性设置为true。这样就可以不使用上面的写法,线程会在程序退出时自动结束。

_newColorThread = new Thread(NewColorFrameProcess) {Name = "Color Thread", IsBackground = true};
_newColorThread.Start();

这里再记一个奇怪的事情,如果对后台线程(IsBackground = true)使用阻塞等待,如调用线程的Join方法,整个程序会挂掉,不知这是否是.Net的bug。


四、跨线程抛事件:

后台线程等到新的帧到来时,就会触发事件,如果直接在后台线程上抛出,用户注册事件响应程序也会在该线程上执行,要是响应程序中有访问UI元素的语句,程序必挂(.Net不允许非UI线程访问UI元素)。虽然可以使用Invoke方法解决该问题,但使用这个方法时还必须拿到待访问元素的引用或UI线程的Dispatcher,对于一个可用于任何.Net程序的类库,这显然不是个好方法。

目前发现的最好的方法是使用SynchronizationContext类。该方法有一个静态熟悉Current,可返回当前所在线程的Context。每次用户注册事件响应程序时,就应该将用户所在线程的Context保存下来,如下:

public void AddHandler(EventHandler<T> listener)
{
    if (listener == null) return;
    lock (_locker)
    {
        if (_disposed) return;
        _listenerList.Add(new ContextListenerPair<EventHandler<T>>(listener, SynchronizationContext.Current)); //保存当前Context和Handler
    }
}
其中的ContextListenerPair是自己实现的一个类,用于保存Context和事件响应程序的Handler。

当需要向UI线程抛出事件时,可以使用Context的Send或Post方法。Send方法是同步的,即等到UI线程执行完事件响应程序后才返回,而Post方法是异步的,它将事件消息投放到UI线程的消息队列之后就立即返回。下面以Post方法为例:

EventHandler<T> listener = contextPair.Listener;
SynchronizationContext context = contextPair.Context;

if (context == null)
{
    listener(sender, eventArgs); //如果没有Context,直接抛出事件
}
else
{
    context.Post(PostDelegate, new ContextArgsWrapper<T>(listener, sender, eventArgs));
}
Post方法的第一个参数是需要在UI线程上执行的函数,第二个参数是该函数要用到的参数。在PostDelegate函数中,再进行事件抛出,就可以起到事件在UI线程上执行的目的,如下:

private void PostDelegate(object arg)
{
    ContextArgsWrapper<T> wrapper = arg as ContextArgsWrapper<T>;
    wrapper.Listener(wrapper.Sender, wrapper.Args); //抛出事件
}


你可能感兴趣的:(多线程,事件,C#,跨线程,PInvoke)