【问题标题】:How to read/write from a global variable in main thread如何从主线程中的全局变量读取/写入
【发布时间】:2019-04-25 21:12:55
【问题描述】:

我创建了一个 C# Windows IoT 后台应用程序。该应用程序在 ThreadPool 中有多个无限期运行的线程。

这些线程需要能够读取/写入主线程中的全局变量,但我不知道如何实现这一点。这是我正在尝试做的一个示例:

// main task
public sealed class StartupTask : IBackgroundTask
{
    private static BackgroundTaskDeferral _Deferral = null;

    private static MyThreadClass1 thread1 = null;
    private static MyThreadClass2 thread2 = null;
    private static MyThreadClass3 thread3 = null;

    List<Object> MyDevices = null;

    public async void Run(IBackgroundTaskInstance taskInstance)
    {
        _Deferral = taskInstance.GetDeferral();

        MyDevices = GetDeviceList();

        thread1 = new MyThreadClass1();
        await ThreadPool.RunAsync(workItem =>
        {
            thread1.Start();
        });

        thread2 = new MyThreadClass2();
        await ThreadPool.RunAsync(workItem =>
        {
            thread2.Start();
        });

        thread3 = new MyThreadClass3();
        await ThreadPool.RunAsync(workItem =>
        {
            thread3.Start();
        });
    }
}

internal class MyThreadClass1
{
    public async void Start()
    { }
}

internal class MyThreadClass2
{
    public async void Start()
    { }
}

internal class MyThreadClass3
{
    public async void Start()
    { }
}

在正在运行的三个线程中的任何一个中,我都需要能够读写List&lt;Object&gt; MyDevices

线程都有不同的功能,但它们都与“MyDevices”交互,所以如果一个线程对该列表进行了更改,其他线程需要立即知道更改。

最好的方法是什么?

谢谢!

【问题讨论】:

  • C# 中没有全局变量——只有 static 类中的字段。或多或少相同,但不同。

标签: c# multithreading iot windows-10-iot-core


【解决方案1】:

这些线程需要能够读/写主线程中的全局变量

处理此要求的最简单方法是删除它。是否可以对解决方案进行编码,以便每个线程拥有一个设备?或者是否有可能重新考虑线程的职责,以便它们通过消息传递而不是更新共享数据进行通信?通常,这些替代方法会产生更干净且错误更少的代码。但并非总是如此。

您将需要锁来保护共享数据。最简单的方法是使用lock 语句,例如:

object _mutex = new object();
List<Object> MyDevices = null;

...

var device = ...;
lock (_mutex)
{
  MyDevices.Add(device);
}

通常,您希望最小化lock 语句中的代码。此外,您可能希望为List&lt;Object&gt; 设置一个锁,并为列表中的每个item 设置一个单独的锁,具体取决于您的线程如何使用这些设备。

【讨论】:

    【解决方案2】:

    您可能要考虑使用ObservableCollection。该类实现了INotifyPropertyChanged 接口,该接口将基础集合的更改通知任何侦听器。

    接下来,您需要在您的 Thread 类中为 PropertyChanged 实现一个事件处理程序,就像这样(我建议创建一个接口或基类来处理这个问题,因为您似乎为每个 @ 使用不同的类987654327@):

    public sealed class MyThreadBase
    {
        private ObservableCollection<object> MyDevices;
    
        public MyThreadBase(ObservableCollection<object> deviceList)
        {
            MyDevices = deviceList;
            MyDevices.PropertyChanged += MyDevices_PropertyChanged; // Register listener
        }
    
        private void MyDevices_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            lock (MyDevices)
            {
                // Do something with the data...
            }
        }
    }
    

    lock 语句用于在另一个线程读取或写入MyDevices 时阻塞线程。这在同步中通常很重要,被称为readers-writers problem。我建议您阅读该内容以及可能的解决方案。

    但是,如果您打算让每个线程遍历设备并对每个设备执行某些操作,那么您将遇到问题,因为遍历不断变化的集合不是一个好主意(并且在使用 foreach 时)循环,实际上会抛出异常),所以也要记住这一点。

    【讨论】:

    • 我不确定 OP 到底想要什么,但我认为他们希望在不同线程上获得通知。事件处理程序仅在一个线程上执行,与引发事件的线程相同。
    【解决方案3】:

    其他线程需要立即了解更改

    如果您想要低延迟通知,线程必须花费大部分时间在某事上休眠。例如。执行Dispatcher.Run(),它将休眠等待消息/任务处理。

    如果是这种情况,您可以使用 ObservableCollection 代替 List,并编写 CollectionChanged 处理程序来转发您的 3 个线程的通知。或者,如果您希望这样做,请将通知转发到其他 2 个线程,不包括当前线程,如果您不希望启动更改的线程处理更改的事件。

    我不确定 Dispatcher 类在 Windows IoT 平台上是否可用。 .NET 核心绝对不是这种情况。即使没有,也可以使用高级构建块来创建一个。这是一个实现同步上下文的示例实现,非常简单,因为依赖于高级 ConcurrentQueue 和 BlockingCollection 泛型类。

    using kvp = KeyValuePair<SendOrPostCallback, object>;
    
    enum eShutdownReason : byte
    {
        Completed,
        Failed,
        Unexpected,
    }
    
    class Dispatcher : IDisposable
    {
        const int maxQueueLength = 100;
    
        readonly ConcurrentQueue<kvp> m_queue;
        readonly BlockingCollection<kvp> m_block;
    
        public Dispatcher()
        {
            m_queue = new ConcurrentQueue<kvp>();
            m_block = new BlockingCollection<kvp>( m_queue, maxQueueLength );
            createdThreadId = Thread.CurrentThread.ManagedThreadId;
            prevContext = SynchronizationContext.Current;
            SynchronizationContext.SetSynchronizationContext( new SyncContext( this ) );
        }
    
        readonly SynchronizationContext prevContext;
        readonly int createdThreadId;
    
        class SyncContext : SynchronizationContext
        {
            readonly Dispatcher dispatcher;
    
            public SyncContext( Dispatcher dispatcher )
            {
                this.dispatcher = dispatcher;
            }
    
            // https://blogs.msdn.microsoft.com/pfxteam/2012/01/20/await-synchronizationcontext-and-console-apps/
            public override void Post( SendOrPostCallback cb, object state )
            {
                dispatcher.Post( cb, state );
            }
        }
    
        /// <summary>Run the dispatcher. Must be called on the same thread that constructed the object.</summary>
        public eShutdownReason Run()
        {
            Debug.Assert( Thread.CurrentThread.ManagedThreadId == createdThreadId );
    
            while( true )
            {
                kvp h;
                try
                {
                    h = m_block.Take();
                }
                catch( Exception ex )
                {
                    ex.logError( "Dispatcher crashed" );
                    return eShutdownReason.Unexpected;
                }
                if( null == h.Key )
                    return (eShutdownReason)h.Value;
    
                try
                {
                    h.Key( h.Value );
                }
                catch( Exception ex )
                {
                    ex.logError( "Exception in Dispatcher.Run" );
                }
            }
        }
    
        /// <summary>Signal dispatcher to shut down. Can be called from any thread.</summary>
        public void Stop( eShutdownReason why )
        {
            Logger.Info( "Shutting down, because {0}", why );
            Post( null, why );
        }
    
        /// <summary>Post a callback to the queue. Can be called from any thread.</summary>
        public void Post( SendOrPostCallback cb, object state = null )
        {
            if( !m_block.TryAdd( new kvp( cb, state ) ) )
                throw new ApplicationException( "Unable to post a callback to the dispatcher: the dispatcher queue is full" );
        }
    
        void IDisposable.Dispose()
        {
            Debug.Assert( Thread.CurrentThread.ManagedThreadId == createdThreadId );
            SynchronizationContext.SetSynchronizationContext( prevContext );
        }
    }
    

    无论你是使用内置 Dispatcher 还是我自定义的 Dispatcher,所有线程都必须调用它的 Run 方法,然后使用异步发布任务或异步方法在 Dispatcher 中运行代码。

    【讨论】: