web-dev-qa-db-ja.com

なぜlock(this){...}が悪いのですか?

MSDNのドキュメント によると

public class SomeObject
{
  public void SomeOperation()
  {
    lock(this)
    {
      //Access instance variables
    }
  }
}

msgstr "インスタンスが公にアクセスできる場合の問題"です。私はなぜだろうか?ロックが必要以上に長く保持されるためですか。それとももっと潜行的な理由がありますか?

447
Anton

他の人がそのオブジェクトをロックしている可能性があるため、ロックステートメントでthisを使用するのは不適切な形式です。

並列操作を適切に計画するためには、起こり得るデッドロック状況を考慮するために特別な注意を払うべきであり、未知の数のロック入口点を持つことはこれを妨げる。たとえば、オブジェクトを参照している人は誰でも、オブジェクトのデザイナー/作成者がそれを知らなくてもロックできます。これはマルチスレッドソリューションの複雑さを増し、その正しさに影響を与える可能性があります。

プライベートフィールドは、コンパイラがアクセス制限を強制し、ロックメカニズムをカプセル化するため、通常はより良い選択肢です。 thisを使用すると、ロックの実装の一部が一般に公開されるため、カプセル化に違反します。それが文書化されていない限り、あなたがthisのロックを取得することになるかどうかも明確ではありません。それでも、問題を防ぐためにドキュメンテーションに頼るのは最適ではありません。

最後に、lock(this)は実際にはパラメータとして渡されたオブジェクトを変更し、何らかの形でそれを読み取り専用またはアクセス不能にするという一般的な誤解があります。これはfalseです。 lockにパラメータとして渡されたオブジェクトは、単にキーとして機能します。そのキーにすでにロックがかけられている場合は、ロックをかけることはできません。そうでなければ、ロックは許可されます。

これがlockステートメントでキーとして文字列を使用するのは悪い理由です。なぜなら、それらは不変であり、アプリケーションの一部で共有/アクセス可能だからです。代わりにプライベート変数を使用してください。Objectインスタンスがうまく機能します。

例として、次のC#コードを実行してください。

public class Person
{
    public int Age { get; set;  }
    public string Name { get; set; }

    public void LockThis()
    {
        lock (this)
        {
            System.Threading.Thread.Sleep(10000);
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        var nancy = new Person {Name = "Nancy Drew", Age = 15};
        var a = new Thread(nancy.LockThis);
        a.Start();
        var b = new Thread(Timewarp);
        b.Start(nancy);
        Thread.Sleep(10);
        var anotherNancy = new Person { Name = "Nancy Drew", Age = 50 };
        var c = new Thread(NameChange);
        c.Start(anotherNancy);
        a.Join();
        Console.ReadLine();
    }

    static void Timewarp(object subject)
    {
        var person = subject as Person;
        if (person == null) throw new ArgumentNullException("subject");
        // A lock does not make the object read-only.
        lock (person.Name)
        {
            while (person.Age <= 23)
            {
                // There will be a lock on 'person' due to the LockThis method running in another thread
                if (Monitor.TryEnter(person, 10) == false)
                {
                    Console.WriteLine("'this' person is locked!");
                }
                else Monitor.Exit(person);
                person.Age++;
                if(person.Age == 18)
                {
                    // Changing the 'person.Name' value doesn't change the lock...
                    person.Name = "Nancy Smith";
                }
                Console.WriteLine("{0} is {1} years old.", person.Name, person.Age);
            }
        }
    }

    static void NameChange(object subject)
    {
        var person = subject as Person;
        if (person == null) throw new ArgumentNullException("subject");
        // You should avoid locking on strings, since they are immutable.
        if (Monitor.TryEnter(person.Name, 30) == false)
        {
            Console.WriteLine("Failed to obtain lock on 50 year old Nancy, because Timewarp(object) locked on string \"Nancy Drew\".");
        }
        else Monitor.Exit(person.Name);

        if (Monitor.TryEnter("Nancy Drew", 30) == false)
        {
            Console.WriteLine("Failed to obtain lock using 'Nancy Drew' literal, locked by 'person.Name' since both are the same object thanks to inlining!");
        }
        else Monitor.Exit("Nancy Drew");
        if (Monitor.TryEnter(person.Name, 10000))
        {
            string oldName = person.Name;
            person.Name = "Nancy Callahan";
            Console.WriteLine("Name changed from '{0}' to '{1}'.", oldName, person.Name);
        }
        else Monitor.Exit(person.Name);
    }
}

コンソール出力

'this' person is locked!
Nancy Drew is 16 years old.
'this' person is locked!
Nancy Drew is 17 years old.
Failed to obtain lock on 50 year old Nancy, because Timewarp(object) locked on string "Nancy Drew".
'this' person is locked!
Nancy Smith is 18 years old.
'this' person is locked!
Nancy Smith is 19 years old.
'this' person is locked!
Nancy Smith is 20 years old.
Failed to obtain lock using 'Nancy Drew' literal, locked by 'person.Name' since both are the same object thanks to inlining!
'this' person is locked!
Nancy Smith is 21 years old.
'this' person is locked!
Nancy Smith is 22 years old.
'this' person is locked!
Nancy Smith is 23 years old.
'this' person is locked!
Nancy Smith is 24 years old.
Name changed from 'Nancy Drew' to 'Nancy Callahan'.
479
Esteban Brenes

なぜなら、人々があなたのオブジェクトインスタンス(つまりあなたのthis)のポインタにたどり着くことができれば、彼らはその同じオブジェクトをロックしようとすることもできるからです。今、彼らはあなたがthisを内部的にロックしていることに気付いていないかもしれないので、これは問題を引き起こすかもしれません(おそらくデッドロック)

これに加えて、それは「やり過ぎ」をロックしているので、それはまた悪い習慣です。

たとえば、List<int>のメンバー変数があるとします。実際にロックする必要があるのは、そのメンバー変数だけです。あなたの関数の中でオブジェクト全体をロックすると、それらの関数を呼び出す他のものはロックを待ってブロックされます。これらの関数がメンバーリストにアクセスする必要がない場合は、他のコードを待機させ、アプリケーションを遅くしてもまったく理由がありません。

62
Orion Edwards

MSDNトピックを見てください スレッド同期(C#プログラミングガイド)

一般に、パブリック型、またはアプリケーションの制御を超えたオブジェクトインスタンスへのロックを避けることが最善です。たとえば、インスタンスにパブリックにアクセスできる場合、lock(this)は問題になる可能性があります。これは、管理できないコードでもオブジェクトがロックされる可能性があるためです。 これにより、2つ以上のスレッドが同じオブジェクトの解放を待つというデッドロック状態が発生する可能性があります。。オブジェクトではなくパブリックデータ型をロックすると、同じ理由で問題が発生する可能性があります。リテラル文字列は共通言語ランタイム(CLR)によって保護されているため、リテラル文字列をロックすることは特に危険です。つまり、プログラム全体に対して任意の文字列リテラルのインスタンスが1つあり、すべてのスレッド上で、実行中のすべてのアプリケーションドメインでまったく同じオブジェクトがリテラルを表します。その結果、アプリケーションプロセス内のどこかで同じ内容の文字列に設定されたロックは、アプリケーション内のその文字列のすべてのインスタンスをロックします。結果として、インターンされていないプライベートまたは保護されたメンバーをロックするのが最善です。一部のクラスは特にロック用のメンバーを提供します。たとえば、Array型はSyncRootを提供します。多くのコレクション型はSyncRootメンバーも提供します。

42
crashmstr

私はこれが古いスレッドであることを知っています、しかし人々はこれを調べてそれに頼ることができるので、lock(typeof(SomeObject))lock(this)よりかなり悪いということを指摘することは重要なようです。そうは言っても; lock(typeof(SomeObject))は悪い習慣であることを指摘してくれたAlanに心からの敬意を表します。

System.Typeのインスタンスは、最も一般的で粗いオブジェクトの1つです。少なくとも、System.TypeのインスタンスはAppDomainに対してグローバルであり、.NETはAppDomain内で複数のプログラムを実行できます。つまり、2つのまったく異なるプログラムが、同じタイプのインスタンスで同期ロックを取得しようとした場合でも、デッドロックが発生する範囲でも、互いに干渉を引き起こす可能性があります。

そのため、lock(this)は特に堅牢な形式ではなく、問題を引き起こす可能性があり、引用されたすべての理由から常に眉を引き上げる必要があります。それでも、私は個人的にはそのパターンの変更を見たいと思いますが、広く使用され、比較的よく尊重され、見かけ上安定したlog4netのようなロック(this)パターンを広範囲に使用するコードがあります。

しかしlock(typeof(SomeObject))は、ワームのまったく新しくて拡張された缶を開きます。

それは価値があるものです。

32
Craig

...そして、まったく同じ引数がこの構文にも適用されます。

lock(typeof(SomeObject))
25
Alan

あなたがあなたのオフィスに熟練した秘書を持っていると想像してください。たまには、他の同僚がまだ彼らを主張していないことを祈るためだけに、あなたは仕事を持っているので彼らに向かって駆けつけます。通常は、しばらく待つだけで済みます。

思いやりが共有されているので、あなたの上司は顧客が直接秘書を使うこともできると決めました。しかし、これには副作用があります。この顧客のために働いている間に顧客がそれらを要求する可能性もあり、タスクの一部を実行するためにもそれらが必要になります。主張はもはや階層ではないため、デッドロックが発生します。そもそも顧客がそれらを主張できないようにすることで、これを完全に回避することができました。

lock(this)は見たとおり悪いものです。外部のオブジェクトがそのオブジェクトをロックしているかもしれませんし、誰がそのクラスを使用しているのかを制御していないので、誰でもロックできます...これは上で説明した正確な例です。繰り返しますが、解決策はオブジェクトの露出を制限することです。ただし、privateprotected、またはinternalクラスを使用している場合は、自分でコードを作成したことを確認しているので、既にオブジェクトをロックしているユーザーを制御できます。メッセージはここにあります:それをpublicとして公開しないでください。また、ロックが同様のシナリオで使用されるようにすることで、デッドロックを回避できます。

これと正反対のことは、アプリドメイン全体で共有されているリソースをロックすることです - 最悪のシナリオ。それはあなたの秘書を外に置き、そこにいる誰もがそれらを主張することを許可するようなものです。結果は全くの混乱です - あるいはソースコードの観点から:それは悪い考えでした。それを捨ててやり直す。では、どうすればいいのでしょうか。

ほとんどの人が指摘しているように、型はアプリドメインで共有されています。しかし、もっと良いものがあります:文字列。その理由は、文字列プールされているです。言い換えれば、アプリドメイン内に同じコンテンツを持つ2つの文字列がある場合、それらがまったく同じポインタを持つ可能性があります。ポインタはロックキーとして使用されるので、基本的に得られるのは「未定義の動作の準備」の同義語です。

同様に、WCFオブジェクト、HttpContext.Current、Thread.Current、Singletons(一般に)などをロックするべきではありません。これをすべて回避する最も簡単な方法はありますか? private [static] object myLock = new object();

6
atlaste

thisポインタをロックすると、shared resourceでロックしている場合、badになることがあります。共有リソースは、静的変数またはコンピュータ上のファイル(クラスのすべてのユーザー間で共有されるもの)です。その理由は、クラスがインスタンス化されるたびに、thisポインタにはメモリ内の場所への異なる参照が含まれるためです。そのため、あるクラスのインスタンスでのロックオーバーthisは、クラスの別のインスタンスでのロックオーバーthisとは異なります。

このコードをチェックして、私の意味を確認してください。コンソールアプリケーションのメインプログラムに次のコードを追加します。

    static void Main(string[] args)
    {
         TestThreading();
         Console.ReadLine();
    }

    public static void TestThreading()
    {
        Random Rand = new Random();
        Thread[] threads = new Thread[10];
        TestLock.balance = 100000;
        for (int i = 0; i < 10; i++)
        {
            TestLock tl = new TestLock();
            Thread t = new Thread(new ThreadStart(tl.WithdrawAmount));
            threads[i] = t;
        }
        for (int i = 0; i < 10; i++)
        {
            threads[i].Start();
        }
        Console.Read();
    }

以下のような新しいクラスを作成してください。

 class TestLock
{
    public static int balance { get; set; }
    public static readonly Object myLock = new Object();

    public void Withdraw(int amount)
    {
      // Try both locks to see what I mean
      //             lock (this)
       lock (myLock)
        {
            Random Rand = new Random();
            if (balance >= amount)
            {
                Console.WriteLine("Balance before Withdrawal :  " + balance);
                Console.WriteLine("Withdraw        : -" + amount);
                balance = balance - amount;
                Console.WriteLine("Balance after Withdrawal  :  " + balance);
            }
            else
            {
                Console.WriteLine("Can't process your transaction, current balance is :  " + balance + " and you tried to withdraw " + amount);
            }
        }

    }
    public void WithdrawAmount()
    {
        Random Rand = new Random();
        Withdraw(Rand.Next(1, 100) * 100);
    }
}

これはthisをロックするプログラムの実行です。

   Balance before Withdrawal :  100000
    Withdraw        : -5600
    Balance after Withdrawal  :  94400
    Balance before Withdrawal :  100000
    Balance before Withdrawal :  100000
    Withdraw        : -5600
    Balance after Withdrawal  :  88800
    Withdraw        : -5600
    Balance after Withdrawal  :  83200
    Balance before Withdrawal :  83200
    Withdraw        : -9100
    Balance after Withdrawal  :  74100
    Balance before Withdrawal :  74100
    Withdraw        : -9100
    Balance before Withdrawal :  74100
    Withdraw        : -9100
    Balance after Withdrawal  :  55900
    Balance after Withdrawal  :  65000
    Balance before Withdrawal :  55900
    Withdraw        : -9100
    Balance after Withdrawal  :  46800
    Balance before Withdrawal :  46800
    Withdraw        : -2800
    Balance after Withdrawal  :  44000
    Balance before Withdrawal :  44000
    Withdraw        : -2800
    Balance after Withdrawal  :  41200
    Balance before Withdrawal :  44000
    Withdraw        : -2800
    Balance after Withdrawal  :  38400

これはmyLockをロックするプログラムの実行です。

Balance before Withdrawal :  100000
Withdraw        : -6600
Balance after Withdrawal  :  93400
Balance before Withdrawal :  93400
Withdraw        : -6600
Balance after Withdrawal  :  86800
Balance before Withdrawal :  86800
Withdraw        : -200
Balance after Withdrawal  :  86600
Balance before Withdrawal :  86600
Withdraw        : -8500
Balance after Withdrawal  :  78100
Balance before Withdrawal :  78100
Withdraw        : -8500
Balance after Withdrawal  :  69600
Balance before Withdrawal :  69600
Withdraw        : -8500
Balance after Withdrawal  :  61100
Balance before Withdrawal :  61100
Withdraw        : -2200
Balance after Withdrawal  :  58900
Balance before Withdrawal :  58900
Withdraw        : -2200
Balance after Withdrawal  :  56700
Balance before Withdrawal :  56700
Withdraw        : -2200
Balance after Withdrawal  :  54500
Balance before Withdrawal :  54500
Withdraw        : -500
Balance after Withdrawal  :  54000
4
ItsAllABadJoke

それについての非常に良い記事があります http://bytes.com/topic/c-sharp/answers/249277-dont-lock-type-objects Microsoft®のパフォーマンスアーキテクトであるRico Marianiによる。 NETランタイム

抜粋:

ここでの基本的な問題はあなたがタイプオブジェクトを所有していないこと、そして他の誰がそれにアクセスできるのかわからないということです。一般に、作成していないオブジェクトをロックして、他に誰がアクセスしているのかわからないようにすることに頼るのは非常に悪い考えです。そうすることはデッドロックを招きます。最も安全な方法は、プライベートオブジェクトのみをロックすることです。

3
Vikrant

ここでこれについてのいくつかの良い議論もあります: これはミューテックスの適切な使用ですか?

2
Bob Nadler

これはもっと単純な例です( ここの質問34 から引用)。なぜlock(this)が悪いのか、そしてクラスの消費者がオブジェクトをロックしようとするとデッドロックになるかもしれません。以下では、3つのスレッドのうち1つのみが続行でき、他の2つはデッドロックされています。

class SomeClass
{
    public void SomeMethod(int id)
    {
        **lock(this)**
        {
            while(true)
            {
                Console.WriteLine("SomeClass.SomeMethod #" + id);
            }
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        SomeClass o = new SomeClass();

        lock(o)
        {
            for (int threadId = 0; threadId < 3; threadId++)
            {
                Thread t = new Thread(() => {
                    o.SomeMethod(threadId);
                        });
                t.Start();
            }

            Console.WriteLine();
        }

回避するために、この男はlockの代わりにThread.TryMonitor(タイムアウト付き)を使用しました。

            Monitor.TryEnter(temp, millisecondsTimeout, ref lockWasTaken);
            if (lockWasTaken)
            {
                doAction();
            }
            else
            {
                throw new Exception("Could not get lock");
            }

https://blogs.appbeat.io/post/c-how-to-lock-without-deadlocks

2
user3761555

これは、従うのがより簡単なサンプルコードです(IMO)。(LinqPadで動作します。以下の名前空間を参照してください:System.NetおよびSystem.Threading.Tasks)

void Main()
{
    ClassTest test = new ClassTest();
    lock(test)
    {
        Parallel.Invoke (
            () => test.DoWorkUsingThisLock(1),
            () => test.DoWorkUsingThisLock(2)
        );
    }
}

public class ClassTest
{
    public void DoWorkUsingThisLock(int i)
    {
        Console.WriteLine("Before ClassTest.DoWorkUsingThisLock " + i);
        lock(this)
        {
            Console.WriteLine("ClassTest.DoWorkUsingThisLock " + i);
            Thread.Sleep(1000);
        }
        Console.WriteLine("ClassTest.DoWorkUsingThisLock Done " + i);
    }
}
1
Raj Rao

あなたのクラスのインスタンスを見ることができるコードのどんな塊もその参照をロックすることができるからです。それを参照する必要があるコードだけがそれを参照できるように、あなたはあなたのロッキングオブジェクトを隠し(カプセル化)したいです。 thisというキーワードは現在のクラスインスタンスを参照するので、いくつかのものがそれを参照し、それを使ってスレッド同期を行うことができます。

明らかに、他のコードの塊がクラスインスタンスを使用してロックする可能性があり、コードがタイムリーにロックを取得できない可能性があるため、または他のスレッド同期の問題が発生する可能性があります。ベストケース:あなたのクラスへの参照を使ってロックするものは他には何もありません。ミドルケース:何かがあなたのクラスへの参照を使ってロックをかけていて、それはパフォーマンスの問題を引き起こします。最悪の場合:何かがあなたのクラスの参照を使ってロックをしていて、それが本当に悪い、本当に微妙な、本当にデバッグが難しい問題を引き起こしています。

1
Jason Jackson

なぜlock(this)がお勧めできないのかを説明している次のリンクを参照してください。

http://blogs.msdn.com/b/bclteam/archive/2004/01/20/60719.aspx

そのための解決策は、クラスにlockObjectなどのプライベートオブジェクトを追加し、次に示すようにlockステートメント内にコード領域を配置することです。

lock (lockObject)
{
...
}
1

申し訳ありませんが、これをロックするとデッドロックが発生する可能性があるという議論に同意できません。あなたは2つのことを混乱させています。デッドロックと飢えです。

  • スレッドの1つに割り込まずにデッドロックを取り消すことはできないので、デッドロックに入った後は出ることはできません。
  • スレッドの1つがその仕事を終えると、飢餓は自動的に終了します

ここ は違いを説明する写真です。

まとめ
スレッド飢餓が問題にならない場合でも、lock(this)を安全に使用できます。 lock(this)を使って飢えているスレッドがあなたのオブジェクトをロックしているロックで終わったとき、それはついに永遠の飢餓で終わるだろうということを覚えておかなければなりません;)

0
SOReader

あなたは、クラスが 'this'をロックするコード、またはクラス内のコードがインスタンス化する任意のオブジェクトを持つことができるという規則を確立することができます。したがって、パターンに従わないと問題になります。

このパターンに従わないコードから身を守りたいのなら、受け入れられた答えは正しいです。しかし、パターンに従えば、それは問題にはなりません。

ロック(これ)の利点は効率です。単一の値を保持する単純な「値オブジェクト」があるとしたらどうでしょうか。これは単なるラッパーであり、数百万回インスタンス化されます。ロックのためだけにプライベート同期オブジェクトの作成を要求することで、基本的にオブジェクトのサイズを2倍にし、割り当て数を2倍にしました。パフォーマンスが重要な場合、これは利点です。

割り当て数やメモリ使用量を気にしない場合は、他の回答で示されている理由からロック(これ)を回避することをお勧めします。

0
zumalifeguard