如何确保获取和设置操作的原子性以重定向Console.Out以记录控制台输出?

时间:2008-12-15 00:23:09

标签: .net-2.0 console locking atomic console.setout

我需要拦截控制台输出流,以便捕获日志,但仍然将内容传递给原始流,以便应用程序正常工作。这显然意味着在使用Console.Out更改原始Console.SetOut(new MyTextWriterClass(originalOut)) TextWriter之前存储它。

我假设获取Out属性和调用SetOut()方法的各个操作由Console以线程安全的方式实现。但我想确保一些其他线程(例如,运行客户端应用程序代码,我不能控制,不能指望改变,所以我不能依赖我自己的自定义锁定方案)不能在我的get和set之间意外地改变它并最终被我的改变覆盖(打破他们的应用程序的行为!)。由于其他代码可能只是调用SetOut(),我的代码理想情况下应该获得Console内部使用的相同锁(假设有一个)。

不幸的是,Console是一个(静态)类,而不是一个实例,所以你不能只是lock (Console)。在类文档中查看似乎没有提到锁定。这不是这些Console方法通常预期的用法,但应该有一些安全的方法将其作为原子操作。

未通过标准锁定方案,还有其他方法可以确保这一点吗?对于如此短的关键部分(并且仅执行一次),即使暂时阻塞所有其他线程也是可以接受的,如果这是唯一的方法。我们正在使用C#和.NET2.0。

如果 可以(不破坏客户端应用程序),那么我们将不得不依赖客户端应用程序重定向其控制台输出碰巧在我们的get和set操作之间进行。我只想覆盖所有基础,以防万一。

编辑:现在我们已经通过示例代码得到了具体的答案,我已经重新编写了问题标题,以更一般地反映答案可以提供帮助的用例,更加清晰。另外,添加了“atomic”的标签。

4 个答案:

答案 0 :(得分:2)

如果你看一下SetOut的实现,它看起来对我来说是安全的:

[HostProtection(SecurityAction.LinkDemand, UI=true)]
public static void SetOut(TextWriter newOut)
{
    if (newOut == null)
    {
        throw new ArgumentNullException("newOut");
    }
    new SecurityPermission(SecurityPermissionFlag.UnmanagedCode).Demand();
    _wasOutRedirected = true;
    newOut = TextWriter.Synchronized(newOut);
    lock (InternalSyncObject)
    {
        _out = newOut;
    }
}

修改

我能想到的最简单的解决方案是使用反射来获取他们的InternalSyncObject,然后锁定它。

提醒一句,这是一个极端糟糕的主意,只有在没有其他选择的情况下才能使用。您可能会导致框架出现意外情况并导致进程崩溃。

您还需要注意任何服务包和主要版本,确保仍然使用内部变量。由于其内部没有承诺它将在下一版本中出现。编写你的代码defensivley,如果不是带有反射的对象,请尝试很好地降低用户体验。

祝你好运: - )

答案 1 :(得分:1)

好的,现在我已经有时间实际完成Josh建议的反射方法的实现。基本代码(在我的ConsoleIntercepter类中,继承自TextWriter)是:

private static object GetConsoleLockObject()
{
    object lockObject;
    try
    {
        const BindingFlags bindingFlags = BindingFlags.GetProperty |
            BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public;
        // It's currently private, but we'd be happy if it were public, too.
        Type consoleType = typeof(Console);

        lockObject = consoleType.InvokeMember("InternalSyncObject", bindingFlags,
                                              null, null, null);
    }
    catch
    {
        lockObject = null; // Return null on any failure.
    }
    return lockObject;
}
public static void RegisterConsoleIntercepter()
{
    object lockObject = GetConsoleLockObject();
    if (lockObject != null)
    {
        // Great!  We can make sure any other changes happen before we read
        // or after we've written, making this an atomic replacement operation.
        lock (lockObject)
        {
            DoIntercepterRegistration();
        }
    }
    else
    {
        // Couldn't get the lock object, but we still need to work, so
        // just do it without an outer lock, and keep your fingers crossed.
        DoIntercepterRegistration();
    }
}

然后DoIntercepterRegistration()会是这样的:

private static void DoIntercepterRegistration()
{
    Console.SetOut(new ConsoleIntercepter(Console.Out));
    Console.SetError(new ConsoleIntercepter(Console.Error));
}

这只是针对Out和Error的锁保护原子替换;显然,实际的拦截,处理和传递到之前的TextWriter需要额外的代码,但这不是这个问题的一部分。

请注意(在.NET2.0提供的源代码中)Console类使用InternalSyncObject来保护Out和Error的初始化并保护SetOut()和SetError(),但它不使用锁定读取一旦先前已初始化,则输出和错误。这对我的特定情况来说已经足够了,但是如果你做了一些更复杂(和疯狂)的事情,可能会有违规行为;我无法想到任何有用的情景,但可以想象故意的病态。

答案 2 :(得分:0)

如果你能在Main()早期做到这一点,你将有更好的机会避免任何竞争条件,特别是如果你能在创造工作线程之前做到这一点。或者在某个类的静态构造函数中。

如果Main()标有[STAThread]属性,它将在单线程公寓中运行,因此也应该有所帮助。

答案 3 :(得分:0)

通常,我会使用安全模型来防止客户端代码在您不查看时重定向控制台。我确信这需要完全信任,所以如果客户端代码使用部分信任运行,它将无法调用SetOut()。