Martin Niederl Martin Niederl - 2 months ago 39
C# Question

C# Dual Threading, Thread.IsAlive is false even when this thread didn't finish yet

I wrote a short Program which searches for empty directories and deletes them.
This process should run in background while a second process should write something to the Console every second so that the user knows the program is still running.
My problem is that the whole program stops after about 3 seconds while the processDirectory method didn't even finish.

My Main Method which calls a Method (processDirectory()) which runs in a second Thread:

static void Main(string[] args)
{
Thread delEmpty = new Thread(() => Thread2.processDirectory(@"C:\Users\Mani\Documents"));

delEmpty.Start();
printRunning(delEmpty);

File.WriteAllLines(@"C:\Users\Mani\Desktop\Unauthorized Folders.txt", Thread2.unauthorized);
File.WriteAllLines(@"C:\Users\Mani\Desktop\Empty Folders.txt", Thread2.emptyFolders);
Console.ReadKey();
}


My second Class which stores my processDirectory Method which should run in background:

public static List<string> unauthorized = new List<string>();
public static List<string> emptyFolders = new List<string>();
public static void processDirectory(string rootPath)
{
if (!Directory.Exists(rootPath)) return;

foreach (var dir in Directory.GetDirectories(rootPath))
{
try
{
processDirectory(dir);
if (Directory.GetFiles(dir).Length == 0 && Directory.GetDirectories(dir).Length == 0) Directory.Delete(dir, false);
}
catch (UnauthorizedAccessException uae) { unauthorized.Add(uae.Message); }
}
}


Code for printing something:

static async void printRunning(Thread delEmpty)
{
Console.CursorVisible = false;
for (int cnt = 1; delEmpty.IsAlive; cnt++)
{
switch (cnt)
{
case 1:
Console.Write("Running. ");
break;
case 2:
Console.Write("Running . ");
break;
case 3:
Console.Write("Running .");
cnt = 0;
break;
}
await Task.Delay(1000);
}
Console.Write("Finished!");
Console.CursorVisible = true;
}

Answer

I'm going to suggest that you avoid using threads and use an abstraction that deals with your threading issues for you. I suggest making use of Microsoft's Reactive Framework Team's Reactive Extensions (NuGet "System.Reactive") and Interactive Extensions (NuGet "System.Interactive").

Then you can do this:

static void Main(string[] args)
{
    var rootPath = @"C:\Users\Mani\Documents";

    using (Observable
        .Interval(TimeSpan.FromSeconds(1.0))
        .Subscribe(x => Console.WriteLine($"Running{"".PadLeft((int)x % 3)}.")))
    {
        Thread2.processDirectory(rootPath);
    }

}

public static class Thread2
{
    public static List<string> unauthorized = new List<string>();
    public static List<string> emptyFolders = null;
    public static void processDirectory(string rootPath)
    {
        if (!Directory.Exists(rootPath)) return;

        emptyFolders = 
            EnumerableEx
                .Expand(Directory.GetDirectories(rootPath), dir => Directory.GetDirectories(dir))
                .Where(dir => Directory.GetFiles(dir).Length == 0 && Directory.GetDirectories(dir).Length == 0)
                .ToList();

        emptyFolders
            .AsEnumerable()
            .Reverse()
            .ForEach(dir =>
            {
                try
                {
                    Directory.Delete(dir, false);
                }
                catch (UnauthorizedAccessException uae) { unauthorized.Add(uae.Message); }
            });
    }
}

The key elements here are:

  1. the Observable.Interval that sets up a timer to display the "Running" message every second.
  2. the EnumerableEx.Expand which recursively builds the list of folders to be deleted.
  3. the Reverse/ForEach which runs through the folders to be deleted (in reverse order) and deletes them.

It's important to not that the deleting happens on the main thread - it's just the "Running" message that comes out on the other thread. If needed, though, it would be fairly easy to push the deleting to another thread, but it isn't necessary.


To handle the case when GetDirectories throws an error, use this code:

    Func<string, string[]> getDirectories = dir =>
    {
        try
        {
            return Directory.GetDirectories(dir);
        }
        catch (UnauthorizedAccessException uae)
        {
            unauthorized.Add(uae.Message);
            return new string[] { };
        }
    };

    emptyFolders =
        EnumerableEx
            .Expand(getDirectories(rootPath), dir => getDirectories(dir))
            .Where(dir => Directory.GetFiles(dir).Length == 0 && getDirectories(dir).Length == 0)
            .ToList();
Comments