传递给ConcurrentStack.Push的类实例消失了吗?

时间:2017-06-04 09:48:51

标签: c# multithreading concurrency stack queue

编辑:TL; DR ConcurrentStack.Push在多线程环境中表现不佳,我对此无能为力。

摘要

在尝试编写"自定义ThreadPool",被称为" ThreadFactory"后,我开始偶然发现一些特殊问题。

ConcurrentStack.Push 似乎并不总是将传递的参数推送到堆栈。

来源

ThreadFactory.cs

using System.Collections.Concurrent;

namespace CustomThreadFactory
{
    public class ThreadFactory
    {
        public uint Working
        {
            get
            {
                return (uint)( _numWorkers - _available.Count );    // Returns how many workers are currently busy
            }

            set { }
        }

        private uint _numWorkers;                                   // Stores the amount of workers

        private ConcurrentStack<ThreadWorker> _available;           // Stores the available workers
        private ConcurrentQueue<ThreadTask> _backlog;               // ThreadTask backlog, ThreadTasks queue up here if there're no workers available

        public ThreadFactory( uint numWorkers = 8 )
        {
            _numWorkers = numWorkers;

            _available = new ConcurrentStack<ThreadWorker>();
            _backlog = new ConcurrentQueue<ThreadTask>();

            for ( uint i = 0; i < numWorkers; i++ )
                _available.Push( new ThreadWorker( this ) );        // Populate worker stack
        }

        public bool Continue( ThreadWorker sender, ref ThreadTask task )
        {                                                           // This function is called by ThreadWorker to decide whether it should lock next call
            if( _backlog.Count > 0 )                                // More work available = Do not lock, a new ThreadTask has been set
            {
                while ( !_backlog.TryDequeue( out task ) ) ;        // Busy-wait TryDequeue on the backlog queue, this should only take a couple cycles (if not one)
                return false;                                       // ThreadWorker._lockThread boolean is set to false here, it won't lock next cycle
            }
            else
            {
                task = null;                                        // No more tasks available, last task is marked for GC
                _available.Push( sender );                          // Push the worker back onto the available stack

                return true;                                        // ThreadWorker._lockThread boolean is set to true here, the worker thread will now lock and wait
            }
        }

        public void Enqueue( ThreadTask task )
        {
            if( _available.Count > 0 )                              // If there's a worker available (and sleeping/locked), wake them and pass the new task
            {
                ThreadWorker worker;
                while ( !_available.TryPop( out worker ) ) ;        // Busy-wait TryPop on worker stack, this should only take a couple cycles (if not one)

                worker.Wake( task );
            }
            else                                                    // Otherwise this task will be added to the backlog queue
            {
                _backlog.Enqueue( task );
            }
        }
    }
}

ThreadWorker.cs

using System.Threading;

namespace CustomThreadFactory
{
    public class ThreadWorker
    {
        private ThreadFactory _factory;         // Parent ThreadFactory

        private volatile bool _lockThread;      // Should lock on next cycle?
        private object _threadLock;             // Locking object
        private Thread _thread;                 // Worker thread

        private ThreadTask _task;               // Current task

        public ThreadWorker( ThreadFactory parent )
        {
            _factory = parent;

            _lockThread = true;
            _threadLock = new object();

            _thread = new Thread( _Work );
            _thread.Start();
        }

        ~ThreadWorker()
        {
            _thread?.Abort();                   // If _thread is "valid", _thread.Abort
            _threadLock = null;                 // Mark for GC (not sure if required)
        }

        private void _Work()
        {
            while( true )
            {
                if ( _lockThread )
                    lock ( _threadLock )
                        Monitor.Wait( _threadLock );
                // ---

                _task?.Run();                   // If _task is "valid", _task.Run

                                                // If _factory has more tasks, _task will be set and _lockThread will be false
                                                // Otherwise the worker thread will lock on the next cycle
                _lockThread = _factory.Continue( this, ref _task );
            }
        }

        public void Wake( ThreadTask task )
        {
            _task = task;                       // Set _task
            _lockThread = false;                // Don't lock the thread unless the ThreadFactory specifically "says so"

            lock ( _threadLock )
                Monitor.Pulse( _threadLock );   // Unlock if currently locked
        }
    }
}

ThreadTask.cs

namespace CustomThreadFactory
{
    public delegate void ThreadAction( object parameters );

    public class ThreadTask
    {
        private ThreadAction _action;               // Delegate action
        private object _parameters;                 // Object parameter(s)

        public ThreadTask( ThreadAction action )    // Parameterless constructor
        {
            _action = action;
            _parameters = null;
        }

        public ThreadTask( ThreadAction action, object parameters )
        {
            _action = action;
            _parameters = parameters;
        }

        public void Run()                          // Executes the delegate, passing the stored parameters (or null)
        {
            _action( _parameters );
        }
    }
}

Program.cs

namespace CustomThreadFactory
{
    class Program
    {
        static void Main( string[] args )
        {
            ThreadFactory f = new ThreadFactory();

            Stopwatch s = new Stopwatch();
            s.Start();

            for ( uint i = 0; i < 512; i++ )
            {
                f.Enqueue( new ThreadTask( ( object parameters ) =>
                {
                    for ( uint x = 0; x < 2048; x++ )
                    {
                        long y = (long)Math.Pow( x, 2 );
                    }
                } ) );
            }

            while ( f.Working > 0 )
                Thread.Sleep( 1 );

            s.Stop();
            Console.WriteLine( string.Format( "Took {0}ms", s.Elapsed.TotalMilliseconds - 1 ) );

            Console.ReadLine();
        }
    }
}

问题

问题似乎在于ThreadFactory.Continue,如果未达到_backlog.Count > 0ThreadWorker sender应该重新添加到可用工人堆叠 虽然这并不总是发生。 (暗示有时重新成功添加所有ThreadWorker

我尝试过:

  • 调试与发布(无差异)
  • ThreadWorker.Continue中的Console.WriteLine(&#34;线程现在可用&#34;),始终打印正确的行数(默认为8),但并非所有ThreadWorker都被推送到可用工人堆栈
  • 编辑: lock( _stackLock ) _available.Push( sender )左右(无差异)

从现在开始,我无法真正看到导致ConcurrentStack不将ThreadWorker重新推送到可用工作人员堆栈的原因。
非常感谢你的帮助。

1 个答案:

答案 0 :(得分:1)

您有一些竞争条件,Monitor.Lock / Monitor.Pulse出现问题。我更改了一些代码以使其基本正常工作;

ThreadFactory

的变化
public class ThreadFactory
{
    ...

    public ThreadTask Continue( ThreadWorker sender )
    {
        if ( _backlog.Count > 0 )
        {
            ThreadTask task;
            do
            {
                if ( _backlog.TryDequeue( out task ) )
                {
                    return task;
                }
            }
            while ( _backlog.Count > 0 );
        }

        _available.Push( sender );
        return null;

    }

    public void Enqueue( ThreadTask task )
    {
        if ( _available.Count > 0 )                              // If there's a worker available (and sleeping/locked), wake them and pass the new task
        {
            ThreadWorker worker;
            do
            {
                if ( _available.TryPop( out worker ) )
                {
                    worker.Wake( task );
                }
            }
            while ( _available.Count > 0 );        // Busy-wait TryPop on worker stack, this should only take a couple cycles (if not one)
        }
        else                                                    // Otherwise this task will be added to the backlog queue
        {
            _backlog.Enqueue( task );
        }
    }
}

以及整个ThreadWorker

public class ThreadWorker
{
    private ThreadFactory _factory;         // Parent ThreadFactory

    private AutoResetEvent _event = new AutoResetEvent( false );
    private Thread _thread;                 // Worker thread
    private volatile ThreadTask _task;               // Current task

    public ThreadWorker( ThreadFactory parent, string name = null )
    {
        Name = name;

        _factory = parent;

        _thread = new Thread( _Work );
        _thread.Start();
    }

    public string Name { get; }

    ~ThreadWorker()
    {
        _thread?.Abort();                   // If _thread is "valid", _thread.Abort
    }

    private void _Work()
    {
        while ( true )
        {
            _event.WaitOne();
            _task?.Run();                   // If _task is "valid", _task.Run

            // If _factory has more tasks, _task will be set and _lockThread will be false
            // Otherwise the worker thread will lock on the next cycle
            Wake( _factory.Continue( this ) );
        }
    }

    public void Wake( ThreadTask task )
    {
        _task = task;
        if ( _task != null )
        {
            _event.Set();
        }
    }
}