WPF: Concerns about Concurrent Access to Singleton Run Logger across Threads

Jane Jie Chen 506 Reputation points
2024-12-11T01:50:23.13+00:00

Our WPF application targets .NET framework 4.8.

the WPF application can control instrument and run a plate and collect run data and show run status in the User Interface.

The application defines a run log which can record application operations, instrument operation and run status. we define run log class as singleton class as following. It can be access from multiple threads (GUI and background threads).

Is it OK that GUI and background threads use singleton RunLogger to write texts into the run log file concurrently (at the same time)? Do you say any problem such as race condition? Thx!

public class RunLogger : GenericLogger
    {
        ///-------------------------------------------------------------------------------------------------------------
        /// Properties
        ///-------------------------------------------------------------------------------------------------------------
        public static RunLogger Instance        { get; set; }

        public string Username                  { get; set; }

        ///-------------------------------------------------------------------------------------------------------------
        /// <summary>
        /// Class constructor
        /// </summary>
        ///-------------------------------------------------------------------------------------------------------------
        public RunLogger()
        : base( "RunLog [{SessionName}]{Version}.txt" )
        {
        }

        ///-------------------------------------------------------------------------------------------------------------
        /// <summary>
        /// Creates the singleton instance of the run logger.
        /// </summary>
        ///-------------------------------------------------------------------------------------------------------------
        public static RunLogger CreateInstance()
        {
            Instance = new RunLogger();

            return Instance;
        }

        public static void Write( string format, params object[] args )
        {
            if ( Instance != null )
            {
                Instance.WriteLine( format, args );
            }
        }

        public static void Write( string msg )
        {
            if ( Instance != null )
            {
                Instance.WriteLine( msg );
            }
        }

        ///-------------------------------------------------------------------------------------------------------------
        /// <summary>
        /// Starts the run logging
        /// </summary>
        ///-------------------------------------------------------------------------------------------------------------
        public override void StartLogging()
        {
            base.StartLogging();

            if ( VersionManifest.Instance == null )
            {
                throw new Exception( "VersionManifest.Instance MUST be established before logging is started." );
            }

            string versionMessage = string.Format( "Software Version: {0}.{1}.{2}",
                                                   VersionManifest.Instance.SoftwareVersionMajor,
                                                   VersionManifest.Instance.SoftwareVersionMinor,
                                                   VersionManifest.Instance.SoftwareVersionBuild );

            WriteLine( versionMessage );

            ///////////////////////////////////////////////////////////////////
            // Run Date
            ///////////////////////////////////////////////////////////////////
            WriteLine( string.Format( "Run Date: {0}", DateTime.Now.ToString( "MM-dd-yyyy" ) ) );

            ///////////////////////////////////////////////////////////////////
            // Session Name
            ///////////////////////////////////////////////////////////////////
            WriteLine( string.Format( "Session Name: {0}", LoggingSession.SessionName ) );

            ///////////////////////////////////////////////////////////////////
            // Instrument Manifest - Serial Number and Manufacture Date
            ///////////////////////////////////////////////////////////////////
            InstrumentManifest _instrumentManifest = InstrumentManifest.CreateInstance();

            WriteLine( string.Format( "Instrument Serial Number: {0}", _instrumentManifest.SerialNumber ) );

            WriteLine( string.Format( "Instrument Manufacture Date: {0}", _instrumentManifest.ManufactureDate ) );

            if ( ! string.IsNullOrEmpty( Username ))
            {
                WriteLine( string.Format( "User Name: {0}", Username ));
            }

            ///////////////////////////////////////////////////////////////////
            // Version Manifest
            ///////////////////////////////////////////////////////////////////
            //VersionManifest _versionManifest = VersionManifest.CreateInstance();

            //WriteLine( string.Format( "Version Manifest Software Version: {0}", _versionManifest.InstrumentSoftwareVersion ) );

            //WriteLine( string.Format( "Version Manifest Console Version: {0}", _versionManifest.ConsoleApplicationVersion ) );
        }

        ///-------------------------------------------------------------------------------------------------------------
        /// <summary>
        /// ...
        /// </summary>
        ///-------------------------------------------------------------------------------------------------------------
        public void WriteLine( string message )
        {
            GenerateLogEntry( message );
        }

        public void WriteLine( string format, params object[] list )
        {
            try
            {
                WriteLine( string.Format( format, list ) );
            }
            catch( Exception )
            {
                WriteLine( "** Exception in RunLogger.WriteLine **" );
                WriteLine( "(probably format exception" );
                WriteLine( format );
                foreach( var arg in list )
                    WriteLine( arg.ToString() );
                WriteLine( "** **" );
            }
        }
    }

public abstract class GenericLogger
{
	public void GenerateLogEntry( string logMessage )
        {
            GenerateLogEntry( logMessage, true );
        }
        public void GenerateLogEntry2( string logMessage )
        {
            GenerateLogEntry( logMessage, false );
        }
        public void GenerateLogEntry( string logMessage, bool prependTime )
        {
            try
            {
                string logFolder = LogFolder;
                if ( logFolder != "" )
                {
                    if ( prependTime )
                    {
                        logMessage = string.Format( "{0:yyyy-MM-dd HH:mm:ss} {1}", DateTime.Now, logMessage );
                    }
                    if ( ! Directory.Exists( logFolder ))
                    {
                        Directory.CreateDirectory( logFolder );
                    }
                    //  try 2 times (if IO error, increment version number and try again)
                    lock ( _memberLock )
                    {
                        for ( int i = 0; i < 2; i++ )
                        {
                            try
                            {
                                string filePath = Path.Combine( logFolder, LogFilename );
                                //  add header (mostly for .csv files)
                                if ( ! File.Exists( filePath ))
                                {
                                    string fileHeader = GetFileHeader();
                                    if ( ! string.IsNullOrEmpty( fileHeader ) )
                                    {
                                        WriteLine( filePath, fileHeader );
                                    }
                                }
                                WriteLine( filePath, logMessage );
                                break;
                            }
                            catch ( IOException /*ex*/ )       //  If file is opened (locked), increment version of file and try again
                            {
                                _version++;
                            }
                            catch                          //  for now, catch (and ignore) all exceptions (maybe someone opened the file
                            {
                                break;
                            }
                        }
                    }
                    RaiseLogEntryGenerated( logMessage );
                }
            }
            catch( ThreadAbortException )
            {
            }
        }
        private void WriteLine( string filePath, string message )
        {
            using ( StreamWriter streamWriter = File.AppendText( filePath ))
            {
                streamWriter.WriteLine( message );
            }
        }
}

The reason we ask this question is that we saw some run logs which includes mix text lines from both GUI threads and background threads after aborting BackgroundWorker during an instrument run. After GUI and background threads perform some actions and wrote into run logs. The GUI supposes received an BackgroundWorker "RunWorkerCompleted" event and display a dialog to tell that "Instrument Error Happened." However, GUI did not receive the "RunWorkerCompleted" event. we want to figure out what cause it.

Developer technologies | .NET | Other
{count} votes

Accepted answer
  1. Anonymous
    2024-12-12T06:22:26.0533333+00:00

    Hi,@Jane Jie Chen.

    At present, I roughly summarize your description:

    1. Mixed lines appear when the GUI and background threads write to the log
    2. Mixed lines appear when the ProtocolProcess macro and the Pxx sub-macro write to the log, and Pxx is running in another thread.

    During this period, you use lock and Thread.Sleep to control the writing of the log, but the above concurrency problem still exists.

    You could consider using ConcurrentQueue to store the log first, and then start a separate thread to take out the logs in the ConcurrentQueue one by one and write them to the log file.


0 additional answers

Sort by: Most helpful

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.