【问题标题】:can you make a method of an object accessible to all code in all threads?您可以使所有线程中的所有代码都可以访问对象的方法吗?
【发布时间】:2013-01-21 17:47:51
【问题描述】:

感谢大家对这个问题的关注。你们中的一些人要求更清楚地了解所涉及的代码,因此为了提供更多信息,我将对其进行编辑以提供更多细节。

关于my previous question,我正在尝试在 WPF 窗口中模拟基本控制台(仅限文本输出)。它适用于在后台运行大量代码并在单独线程上运行的程序。这段代码也严重依赖 while 循环,因此我的计划是将 WPF 控制台窗口保留在主线程上(以及可能需要的任何其他 GUI 窗口)并在单独的线程上执行所有代码。

窗口有一个 WriteLine 方法,使用如下:

mainConsole.WriteLine("This is a message for the user.", SomeSender);

其余代码将需要定期调用此方法。

附加信息:

窗口本身由一个包裹在 Scroller 中的 Textblock 组成。窗口的 WriteLine 方法将消息和格式(字体、字体大小和颜色 - 取决于消息的发送者)添加到包含此信息的对象列表中,然后显示这些消息的列表,包括它们的格式)作为文本块的内容。该方法完全按照预期工作,因此不需要重写,只需要可访问即可。

我已尽量使此描述保持简洁。更多信息请见my previous question

所以我现在的问题是:有没有一种有效的方法使窗口的 WriteLine 方法可用于任何类的所有线程,从而使我能够像 Console.WriteLine() 一样使用它?

【问题讨论】:

  • 在不知道WriteLine 的方法体访问了哪些共享项的情况下无法说出。提供这种实现可能是一个想法。
  • @spender - OP 询问如何使其可访问,而不是如何实现它...

标签: c# wpf multithreading user-interface


【解决方案1】:

虽然您有多种选择,但听起来,就您而言,任何人、任何地方都能够写入您的控制台确实很有意义。鉴于此,我会创建这样的东西:

public class MyConsole
{
    public static event Action<string> TextWritten;
    public static void Write(object obj)
    {
        string text = (obj ?? "").ToString();
        if (TextWritten != null)
            TextWritten(text);
    }

    public static void WriteLine(object obj)
    {
        Write(obj + "\n");
    }
}

然后让您的控制台表单订阅TextWritten 事件,并在写入文本时将该文本写入控制台。 (确保首先编组到 UI 线程。)

在这里使用和事件的主要优点,与让此类直接处理您的表单相反,是您可以轻松添加额外的事件处理程序,允许您与标准输入/输出交互,向文件添加额外的日志记录,一次打开多个控制台表单等。这种灵活性对于调试(即向平面文件的附加写入)和生产(允许通过标准输入/输出更轻松地重定向)都很有用。

【讨论】:

  • 这在多线程环境中无法正常工作,这就是 OP 所说的他将在其中使用它。
  • @SiLo 那为什么?请注意,我特别指出事件接收器应该编组到 UI 线程,这将同步所有这些。如果需要(有些可能不需要),任何其他事件处理程序也将负责同步他们的工作。
  • 编组到 UI 线程可能是线程安全的,但 Invoke 会阻塞正在运行的线程。在高 UI 负载的情况下,这些线程的性能将受到很大影响。使用 BeginInvoke 可以工作,但我不知道(或怀疑)它会保留正确的顺序。
  • @SiLo 使用BeginInvoke 至少在一定程度上解决了这个问题。此外,如果在短时间内有大量写入是一项常见任务,则事件处理程序可以缓冲写入。 (但它可能不是受支持的用例;您可以期望调用者改为进行缓冲,这是 Console 所做的。短时间内的许多写入效率不高。)
  • @SiLo BeginInvoke 确实会保留正确的顺序。请记住,您需要编组到 UI 线程,因为这里的目标是修改 UI 控件。在编组到 UI 线程之前进行同步不会提高性能。
【解决方案2】:

您似乎正在尝试编写一个日志服务,允许您从代码中的任何位置访问日志。您提到了线程,因此您必须注意并相应地处理该同步。

我会首先创建一个ILogger 接口,如下所示:

public interface ILogger
{
  void Log(string line);
  void Log(string format, params object[] args);
}

然后是一个合适的Logger 基类:

public abstract class Logger : ILogger
{
  public abstract void Log(string line);

  public virtual void Log(string format, params object[] args)
  {
    Log(string.Format(format, args));
  }
}

当然,你需要一个实际的实现:

using System.Collections.Concurrent;
using System.Threading.Tasks;

public class ConcurrentLogger : Logger, ILogger, IDisposable
{
  bool isDisposed;
  BlockingCollection<string> loggedLines;
  Action<string> callback;

  public ConcurrentLogger(Action<string> callback)
  {
    if (callback == null)
      throw new ArgumentNullException("callback");

    var queue = new ConcurrentQueue<string>();
    this.loggedLines = new BlockingCollection<string>(queue);

    this.callback = callback;

    StartMonitoring();
  }

  public void Dispose()
  {
    Dispose(true);
    GC.SuppressFinalize(this);
  }

  protected virtual void Dispose(bool isDisposing)
  {
    if (isDisposed) return;

    if (isDisposing)
    {
      if (loggedLines != null)
        loggedLines.CompleteAdding();
    }

    isDisposed = true;
  }

  public override void Log(string line)
  {
    if (!loggedLines.IsAddingCompleted)
      loggedLines.Add(line);
  }

  protected virtual void StartMonitoring()
  {
    Task.Factory.StartNew(() =>
      {
        foreach (var line in loggedLines.GetConsumingEnumerable())
        {
          if (callback != null)
            callback(line);
        }

        loggedLines.Dispose();

      }, TaskCreationOptions.LongRunning);
  }
}

对于全局访问,您将需要一个 Singleton 类,所以我会创建这个 LogManagerclass:

public sealed class LogManager : ILogger
{
  #region Singleton
  static readonly LogManager instance = new LogManager();

  public static LogManager Current { get { return instance; } }

  private LogManager() { } // Disallow creating instances.
  #endregion

  ILogger logger;

  public ILogger Logger { get { return logger; } }

  public void StartLogging(ILogger logger)
  {
    if (logger == null)
      throw new ArgumentNullException("logger");

    this.logger = logger;
  }

  public void StopLogging(bool dispose = true)
  {
    var previousLogger = this.logger as IDisposable;
    this.logger =null;

    if (previousLogger != null && dispose)
      previousLogger.Dispose();
  }

  public void Log(string line)
  {
    if (logger != null) logger.Log(line);
  }

  public void Log(string format, params object[] args)
  {
    if (logger != null) logger.Log(format, args);
  }
}

通过一些快速初始化:

void InitializeLog()
{
  var log = new ConcurrentLogger(LogToTextBox);
  LogManager.Current.StartLogging(log);
}

void LogToTextBox(string line)
{
  if (!CheckAccess())
  {
    this.Dispatcher.BeginInvoke((Action<string>)LogToTextBox,
                                DispatcherPriority.Background, 
                                line);
    return;
  }

  logTextBox.AppendText(line + Environment.NewLine);
}

然后您可以在代码中的任何位置调用:LogManager.Current.Log(...);

【讨论】:

  • 您的记录器正在忙着等待,直到它得到更多要记录的内容。那是……非常糟糕。至少非常,请使用BlockingCollection,这样您将执行一个非忙碌的等待,直到您有更多的工作。尽管如此,这也是一个问题,因为当它不会经常进行生产性工作时,你会保持一个线程。这是基于事件的模型的主要优势;你不需要一直在无所事事。
  • 啊,是的,我忘了BlockingCollection,我会为此进行编辑。我认为长期运行的日志记录线程不一定是坏事。在启动时创建一次的开销可以忽略不计,除非您不断交换日志,否则我看不出这是一个问题。
  • 创建线程的成本不在于创建线程,问题是让线程始终无所事事。这是调度程序所花费的精力,它消耗的内存量很大,它是线程之间的上下文切换,它是线程被饿死的可能性,也是大多数操作系统中每个进程的线程数的硬限制。哦,在访问示例中的文本框之前,您并没有编组到 UI 线程。
  • C# 的 ThreadPool每个处理器保留 25 个线程用于后台工作。对于大多数现代 CPU,这在 50 到 100 之间。我非常怀疑一个长期运行的Thread 用于监视和排队调试日志输出是否会产生巨大影响。每个 CLR 线程都被分配了一个兆字节的内存,对于现代计算机,我也不认为这是一个真正的大问题。除非你有大量的伐木,否则饥饿应该不是问题,如果有的话,它会爆发。至于编组问题,我已经解决了。
  • C# 线程池大部分时间空闲在 0 或 1 个线程左右,而不是 25 个。当队列中有项目等待足够长的时间时,它会增加池中的线程数, 一段时间不使用就会被拆掉。请记住,线程饥饿处理物理机器上的总线程,而不仅仅是线程日志记录。如果您每次遇到此问题时都经常创建额外的线程,那么它就会增加。记录器进行编组也没有任何意义。它不需要了解 UI。
【解决方案3】:

创建一个静态类,该类包含 WriteLine 方法和一个引用窗口、控件或您在 writeline 方法中所需的任何内容的属性。 然后将一些代码添加到您的 MainWindow 构造函数或加载的事件中,以将 Reference-Property 设置为所需的项目。 之后,您可以随时随地使用 Writeline。

顺便说一句:使用带有 Instance getter 的静态 MainViewModel 可能会更干净,将 MainWindow 的 DataContext 绑定到此 ViewModel 并使用 MVVM 模式。然后,您只需设置一些 ConsoleOutput 属性或调用 AddLine 方法,甚至从您想要的任何地方调用命令,而不必知道视图如何显示它。您可以使用单元测试来测试您的应用程序,您可以更改视觉表示,......所有这些都无需触及应用程序的逻辑。

【讨论】:

    【解决方案4】:
    namespace {yourrootnamespace}
    {
       namespace GlobalMethods
       {
            static class ConsoleMethods
            {
            mainConsole mc;
            public static WriteLine(string msg, object sender)
            {
                lock (this)
                {
                    mc.WriteLine(msg, sender)
                }
            }
            static ConsoleMethods()
            {
                mc = new mainConsole();
            }
            //more methods
        }
    }
    

    然后:using {yourrootnamespace}.GlobalMethods;

    或者让这些方法接受一个 mainConsole 参数,然后使用它来调用。

    【讨论】:

    • 那静态方法如何访问mainConsole
    • @ofstream 您需要创建和维护mainConsole 的实例才能访问它。这可能很棘手,因为它可能需要在某个时间点创建。
    • @ofstream 它将可以访问该表单的类型,但不能访问相关实例,所以不,在同一个命名空间中是不够的。
    • 然后也添加一些缩进。
    猜你喜欢
    • 2015-09-20
    • 1970-01-01
    • 1970-01-01
    • 2012-02-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多