当所有线程完成时设置属性值?

4

在我的应用程序中,有三个线程:

 private Thread _analysisThread;
 private Thread _head2HeadThread;
 private Thread _formThread;

每个线程的启动方式如下:

if (_analysisThread == null || !_analysisThread.IsAlive)
{
     _analysisThread = new Thread(() => { Analysis.Logic(match); });
     _analysisThread.Start();
}

我有一个ListView,用户可以选择其中的一个项目并重新启动线程,但我想防止这种情况发生,因为每个线程中的方法都很耗费时间,需要一些时间才能完成。

到目前为止,我希望禁用ListView的选择功能,所以我做了以下操作:


```Java listView.setEnabled(false); ```
<ListView IsEnabled="{Binding IsMatchListEnabled}">

private bool _isMatchListEnabled = true;
public bool IsMatchListEnabled
{
    get { return _isMatchListEnabled; }
    set
    {
        _isMatchListEnabled = value;
        OnPropertyChanged();
    }
}

在启动新线程之前,我会执行以下操作:IsMatchListEnabled = false; 但是我需要检查所有线程是否都已完成,然后才执行 IsMatchListEnabled = true;。如果在所有线程完成之后启用 ListView,由于 Thread 代码是异步的,而 Thread 之外的代码是同步的,因此实际上此属性是无用的。

为了避免这种情况,我尝试创建一个无限循环,如下所示:

while (true)
{
   if (!_analysisThread.IsAlive && !_head2HeadThread.IsAlive && !_formThread.IsAlive)
   {
          IsMatchListEnabled = true;
          break;
   }
}

这个循环放在所有线程执行之后,但是你可以想象,这会冻结应用程序。 有什么解决办法吗?


5
使用Task而非线程(Thread)来执行任务,Task提供了等待任务完成的正常方式。如果你坚持使用线程也可以实现等待任务完成,但实际上并没有太多理由这样做。 - Evk
@Evk 没错,谢谢你的提示 :) - jode
5
你编写了一些“fire-and-forget”多线程代码。但你几乎永远不能忘记它们。不要使用Thread,而是使用方便的帮助类来避免遗漏,如BackgroundWorker、Task,或许还有async/await。并且务必学好这方面的知识,否则多线程会把你吃掉,然后把你吐出来。 - Hans Passant
2个回答

2

所有评论都是正确的 —— 最好使用 Tasks。只是为了回答OP的问题。

你可以使用 ManualResetEvent 同步线程,通过线程数量创建一个事件数组,并有一个额外的线程在所有线程完成后更改 IsMatchListEnabled

public static void SomeThreadAction(object id)
{
    var ev = new ManualResetEvent(false);
    events[id] = ev; // store the event somewhere

    Thread.Sleep(2000 * (int)id); // do your work

    ev.Set(); // set the event signaled
}

然后,在其他地方我们需要初始化等待例程。
// we need tokens to be able to cancel waiting
var cts = new CancellationTokenSource();
var ct = cts.Token;

Task.Factory.StartNew(() =>
{
    bool completed = false;
    while (!ct.IsCancellationRequested && !completed)
    {
        // will check if our routine is cancelled each second
        completed = 
            WaitHandle.WaitAll(
                events.Values.Cast<ManualResetEvent>().ToArray(),
                TimeSpan.FromSeconds(1)); 
    }

    if (completed) // if not completed, then somebody cancelled our routine
        ; // change your variable here
});

完整的示例可以在这里找到并查看。


1
我建议您在此使用微软的反应式框架。它比任务更强大,而且代码比使用线程简单得多。
假设您有3个长时间运行的操作:
Action huey = () => { Console.WriteLine("Huey Start"); Thread.Sleep(5000); Console.WriteLine("Huey Done"); };
Action dewey = () => { Console.WriteLine("Dewey Start"); Thread.Sleep(5000); Console.WriteLine("Dewey Done"); };
Action louie = () => { Console.WriteLine("Louie Start"); Thread.Sleep(5000); Console.WriteLine("Louie Done"); };

现在您可以编写以下简单查询:
IObservable<Unit> query =
    from a in new [] { huey, dewey, louie }.ToObservable()
    from u in Observable.Start(() => a())
    select u;

你可以这样运行它:

Stopwatch sw = Stopwatch.StartNew();
IDisposable subscription = query.Subscribe(u => { }, () =>
{
    Console.WriteLine("All Done in {0} seconds.", sw.Elapsed.TotalSeconds);
});

我得到的结果是:
Huey开始
Dewey开始
Louie开始
Huey完成
Louie完成
Dewey完成
所有操作在5.0259197秒内完成。

三个5秒的操作在5.03秒内全部并行完成。

如果想要提前停止计算,只需调用subscription.Dispose()

获取NuGet "System.Reactive"以获取这些资源。


如果任何一个线程出现异常,会发生什么情况?它会在subscription.Subscribe附近重新抛出吗? - cassandrad
@cassandrad - 不是用我发布的代码,但是.Subscribe方法有另一个重载,如果抛出异常,它将接受一个异常。您还可以在可观察对象上放置.Retry运算符,以重试由于异常而失败的某些代码。 - Enigmativity

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接