Deadlock when waiting for task to finish

2019-08-28 01:15发布

问题:

When the user clicks a certain part of my form, I need to start updating another area of the same form. This is done using a separate task. Thing is, if an update is triggered and there is an update in progress, I need to cancel it, wait for it to finish, and launch a new one.

I am new to TPL and cannot use .NET 5 yet due to time constraints (we have a VS2012 license but have yet to migrate our solution). Here's the code:

private mLoadGridLock = new Object();
private CancellationTokenSource mCancellationTokenSource;
private bool mLoadingFrames;
private Task mLoadingTask;

private List<string> mFramesList;
public FrameLoader(){
    InitializeComponent();
    mLoadingFrames = false;
    mCancellationTokenSource = new CancellationTokenSource();
}

public void LoadGrid(){
    lock (mLoadGridLock) {
        if (mStudiesGrid.RowCount <= 0)
            return;
        Debug.WriteLine("--->LoadGrid Called");
        if (mLoadingFrames) {
            mCancellationTokenSource.Cancel();
            Debug.WriteLine("--->Waiting for cancellation...");
            mLoadingTask.Wait();
            Debug.WriteLine("--->Wait for cancellation over!");
        }
        Debug.WriteLine("--->Launching new task");
        mLoadingTask = Task.Factory.StartNew(() => LoadFrames(), mCancellationTokenSource.Token);
    }
}

private void LoadFrames(){
    mLoadingFrames = true;
    Debug.WriteLine("      +++> Loader task started!");
    int i = 0;
    BeginInvoke(() => flp.Controls.Clear());
    mFramesList = FrameRules.GenerateFrameList(this);
    if (mFramesList <= 0)
        return;
    foreach (string framePath in mFramesList) {
        if (mCancellationTokenSource.Token.IsCancellationRequested) {
            mLoadingFrames = false;
            Debug.WriteLine("      +++>Loader task cancelled");
            return;
        }
        Debug.WriteLine("      +++>Loading frame " + i.ToString());
        i = i + 1;
        FrameOfReference fofr = new FrameOfReference();
        fofr.BackgroundImage = My.Resources.Loading;
        BeginInvoke(() => flp.Controls.Add(fofr));
        Thread.Sleep(3000);
        BeginInvoke(() => fofr.BackgroundImage == My.Resources.Done);
    }
    mLoadingFrames = false;
    Debug.WriteLine("      +++>Loader task finished");
}

This runs fine the first time, but on the second one I get a deadlock. This is the debug output, which is consistent (it's always the same):

--->LoadGrid Called
--->Launching new task
      +++> Loader task started!
      +++>Loading frame 0
      +++>Loading frame 1
The thread 'ShowMessage-Execute' (0x168c) has exited with code 0 (0x0).
The thread 'Win32 Thread' (0x168c) has exited with code 0 (0x0).
      +++>Loader task finished
--->LoadGrid Called
--->Launching new task
      +++> Loader task started!
      +++>Loading frame 0
--->LoadGrid Called
--->Waiting for cancellation...

It's clear that the problem is caused by LoadGrid being called twice on the second time I trigger an update; that's why I added the lock block. But this still hangs... This is my first shot at using TPL, so any input is welcome.

UPDATE: After changing lock(this) to lock(mLoadGridLock), the deadlock disappeared, but on the second or third attempt, it doesn't load any frame and from then onwards it loads none. Here's a log:

--->LoadGrid Called
--->Launching new task
      +++> Loader task started!
      +++>Loading frame 0
The thread 'ShowMessage-Execute' (0xe50) has exited with code 0 (0x0).
The thread 'Win32 Thread' (0xe50) has exited with code 0 (0x0).
      +++>Loading frame 1
      +++>Loader task finished
--->LoadGrid Called
--->Launching new task
      +++> Loader task started!
      +++>Loading frame 0
--->LoadGrid Called
--->Waiting for cancellation...
      +++>Loader task cancelled
--->Wait for cancellation over!
--->Launching new task