18

これが興味深い図書館作家のジレンマです。私のライブラリ(私の場合はEasyNetQ)で、スレッドローカルリソースを割り当てています。したがって、クライアントが新しいスレッドを作成してから、ライブラリの特定のメソッドを呼び出すと、新しいリソースが作成されます。EasyNetQの場合、クライアントが新しいスレッドで「公開」を呼び出すと、RabbitMQサーバーへの新しいチャネルが作成されます。リソース(チャネル)をクリーンアップできるように、クライアントスレッドがいつ終了するかを検出できるようにしたい。

私が思いついたこれを行う唯一の方法は、クライアントスレッドへのJoin呼び出しをブロックするだけの新しい「ウォッチャー」スレッドを作成することです。ここに簡単なデモンストレーションがあります:

最初に私の「ライブラリ」。クライアントスレッドを取得してから、「参加」をブロックする新しいスレッドを作成します。

public class Library
{
    public void StartSomething()
    {
        Console.WriteLine("Library says: StartSomething called");

        var clientThread = Thread.CurrentThread;
        var exitMonitorThread = new Thread(() =>
        {
            clientThread.Join();
            Console.WriteLine("Libaray says: Client thread existed");
        });

        exitMonitorThread.Start();
    }
}

これが私のライブラリを使用するクライアントです。新しいスレッドを作成してから、ライブラリのStartSomethingメソッドを呼び出します。

public class Client
{
    private readonly Library library;

    public Client(Library library)
    {
        this.library = library;
    }

    public void DoWorkInAThread()
    {
        var thread = new Thread(() =>
        {
            library.StartSomething();
            Thread.Sleep(10);
            Console.WriteLine("Client thread says: I'm done");
        });
        thread.Start();
    }
}

このようにクライアントを実行すると、次のようになります。

var client = new Client(new Library());

client.DoWorkInAThread();

// give the client thread time to complete
Thread.Sleep(100);

私はこの出力を取得します:

Library says: StartSomething called
Client thread says: I'm done
Libaray says: Client thread existed

だからそれは動作しますが、それは醜いです。これらすべてのブロックされたウォッチャースレッドがぶら下がっているという考えは本当に好きではありません。これを行うためのより良い方法はありますか?

最初の選択肢。

IDisposableを実装するワーカーを返すメソッドを提供し、スレッド間でワーカーを共有してはならないことをドキュメントで明確にします。変更されたライブラリは次のとおりです。

public class Library
{
    public LibraryWorker GetLibraryWorker()
    {
        return new LibraryWorker();
    }
}

public class LibraryWorker : IDisposable
{
    public void StartSomething()
    {
        Console.WriteLine("Library says: StartSomething called");
    }

    public void Dispose()
    {
        Console.WriteLine("Library says: I can clean up");
    }
}

クライアントはもう少し複雑になりました。

public class Client
{
    private readonly Library library;

    public Client(Library library)
    {
        this.library = library;
    }

    public void DoWorkInAThread()
    {
        var thread = new Thread(() =>
        {
            using(var worker = library.GetLibraryWorker())
            {
                worker.StartSomething();
                Console.WriteLine("Client thread says: I'm done");
            }
        });
        thread.Start();
    }
}

この変更の主な問題は、APIの重大な変更であるということです。既存のクライアントを書き直す必要があります。今ではそれほど悪いことではありませんが、それは彼らを再訪し、彼らが正しくクリーンアップしていることを確認することを意味します。

ノーブレークの2番目の選択肢。APIは、クライアントが「作業範囲」を宣言する方法を提供します。スコープが完了すると、ライブラリはクリーンアップできます。ライブラリはIDisposableを実装するWorkScopeを提供しますが、上記の最初の選択肢とは異なり、StartSomethingメソッドはLibraryクラスに残ります。

public class Library
{
    public WorkScope GetWorkScope()
    {
        return new WorkScope();
    }

    public void StartSomething()
    {
        Console.WriteLine("Library says: StartSomething called");
    }
}

public class WorkScope : IDisposable
{
    public void Dispose()
    {
        Console.WriteLine("Library says: I can clean up");
    }
}

クライアントは、StartSomething呼び出しをWorkScopeに入れるだけです...

public class Client
{
    private readonly Library library;

    public Client(Library library)
    {
        this.library = library;
    }

    public void DoWorkInAThread()
    {
        var thread = new Thread(() =>
        {
            using(library.GetWorkScope())
            {
                library.StartSomething();
                Console.WriteLine("Client thread says: I'm done");
            }
        });
        thread.Start();
    }
}

これは、ライブラリユーザーにスコープについて考える必要がないため、最初の選択肢よりも好きではありません。

4

5 に答える 5

9

ファイナライザーを持つスレッド静的モニターを作成できます。スレッドが生きているときは、モニター オブジェクトを保持します。テアドが死ぬと、それを保持しなくなります。その後、GC が開始されると、モニターがファイナライズされます。ファイナライザーでは、クライアント スレッドの (観測された) 終了についてフレームワークに通知するイベントを発生させることができます。

サンプル コードは、https ://gist.github.com/2587063 の要点にあります。

これがそのコピーです:

public class ThreadMonitor
{
    public static event Action<int> Finalized = delegate { };
    private readonly int m_threadId = Thread.CurrentThread.ManagedThreadId;

    ~ThreadMonitor()
    {
        Finalized(ThreadId);
    }

    public int ThreadId
    {
        get { return m_threadId; }
    }
}

public static class Test
{
    private readonly static ThreadLocal<ThreadMonitor> s_threadMonitor = 
        new ThreadLocal<ThreadMonitor>(() => new ThreadMonitor());

    public static void Main()
    {
        ThreadMonitor.Finalized += i => Console.WriteLine("thread {0} closed", i);
        var thread = new Thread(() =>
        {
            var threadMonitor = s_threadMonitor.Value;
            Console.WriteLine("start work on thread {0}", threadMonitor.ThreadId);
            Thread.Sleep(1000);
            Console.WriteLine("end work on thread {0}", threadMonitor.ThreadId);
        });
        thread.Start();
        thread.Join();

        // wait for GC to collect and finalize everything
        GC.GetTotalMemory(forceFullCollection: true);

        Console.ReadLine();
    }
}

お役に立てば幸いです。余分な待機スレッドよりもエレガントだと思います。

于 2012-05-03T16:41:07.930 に答える
4

スレッドの作成を直接制御していないため、スレッドがいつ作業を終了したかを知ることは困難です。別のアプローチは、クライアントが完了したときに強制的に通知することです。

public interface IThreadCompletedNotifier
{
   event Action ThreadCompleted;
}

public class Library
{
    public void StartSomething(IThreadCompletedNotifier notifier)
    {
        Console.WriteLine("Library says: StartSomething called");
        notifier.ThreadCompleted += () => Console.WriteLine("Libaray says: Client thread existed");
        var clientThread = Thread.CurrentThread;
        exitMonitorThread.Start();
    }
}

このようにして、あなたを呼び出すクライアントは、その作業がいつ完了したかを知らせる何らかの通知メカニズムを強制的に渡す必要があります。

public class Client : IThreadCompletedNotifier
{
    private readonly Library library;

    public event Action ThreadCompleted;

    public Client(Library library)
    {
        this.library = library;
    }

    public void DoWorkInAThread()
    {
        var thread = new Thread(() =>
        {
            library.StartSomething();
            Thread.Sleep(10);
            Console.WriteLine("Client thread says: I'm done");
            if(ThreadCompleted != null)
            {
               ThreadCompleted();
            }
        });
        thread.Start();
    }
}
于 2012-05-03T13:49:02.553 に答える
1

クライアント スレッドが、一部のリソースを内部的に割り当てるライブラリへの呼び出しを行う場合、クライアントはライブラリを「開き」、以降のすべての操作のトークンを取得する必要があります。このトークンは、ライブラリ内部のベクトルへの int インデックス、または内部オブジェクト/構造体への void ポインターである可能性があります。クライアントは、終了する前にトークンを閉じる必要があると主張します。

これは、クライアント呼び出し間で状態を保持する必要があるすべての lib 呼び出しの 99% が機能する方法です。ソケット ハンドル、ファイル ハンドル。

于 2012-05-03T13:48:20.517 に答える
1

スレッドを完全に回避するために非同期の派手なことを行うこととは別に、すべての監視を単一のスレッドに結合して、ライブラリにヒットしたすべてのスレッドの .ThreadState プロパティを、たとえば 100ms ごとにポーリングしようとします (よくわかりません)リソースをどれだけ迅速にクリーンアップする必要があるか...)

于 2012-05-03T13:57:14.630 に答える
0

あなたの.Joinソリューションは私にはかなりエレガントに見えます。ブロックされたウォッチャー スレッドはそれほどひどいものではありません。

于 2012-05-03T13:48:53.627 に答える