MSDNのドキュメントには、と言っています
public class SomeObject
{
public void SomeOperation()
{
lock(this)
{
//Access instance variables
}
}
}
「インスタンスがパブリックにアクセスできる場合の問題」です。なぜだろう?ロックが必要以上に長く保持されるからでしょうか?それとももっと陰湿な理由がありますか?
MSDNのドキュメントには、と言っています
public class SomeObject
{
public void SomeOperation()
{
lock(this)
{
//Access instance variables
}
}
}
「インスタンスがパブリックにアクセスできる場合の問題」です。なぜだろう?ロックが必要以上に長く保持されるからでしょうか?それとももっと陰湿な理由がありますか?
回答:
this
他の人がそのオブジェクトをロックしている可能性があるのは、通常、あなたのコントロールの外にあるため、lockステートメントで使用するのは不適切な形式です。
並列処理を適切に計画するために、デッドロックの可能性を考慮するように特別な注意を払う必要があります。ロックエントリポイントの数が不明であることは、これを妨げます。たとえば、オブジェクトへの参照を持つすべてのユーザーは、オブジェクトの設計者/作成者がそれを知らなくても、オブジェクトをロックできます。これにより、マルチスレッドソリューションの複雑さが増し、その正確さに影響する可能性があります。
コンパイラーはプライベートフィールドへのアクセス制限を適用し、ロックメカニズムをカプセル化するため、プライベートフィールドは通常、より良いオプションです。を使用this
すると、ロック実装の一部が公開されるため、カプセル化に違反します。this
文書化されていない限り、ロックオンを取得することも明確ではありません。それでも、問題を防止するためにドキュメントに依存することは最適ではありません。
最後に、lock(this)
パラメーターとして渡されたオブジェクトを実際に変更し、何らかの方法で読み取り専用またはアクセス不能にするという一般的な誤解があります。これは誤りです。パラメータとして渡されたオブジェクトは、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'.
lock(this)
が標準的なアドバイスです。これを行うと、通常、外部コードがオブジェクトに関連付けられたロックをメソッド呼び出し間で保持することができなくなることに注意することが重要です。 これは良いことかもしれませんし、そうでないかもしれません。外部コードが任意の期間ロックを保持できるようにすることにはいくつかの危険があり、クラスは一般にそのような使用法を不要にするように設計されるべきですが、必ずしも実用的な代替手段はありません。簡単な例として、コレクションが独自のToArray
or ToList
メソッドを実装しない限り...
there is the common misconception that lock(this) actually modifies the object passed as a parameter, and in some way makes it read-only or inaccessible. This is false
-それらの話はCLRオブジェクトのSyncBlockビットに関するものだと思うので、正式にはこれは正しい-変更されたオブジェクト自体をロックする
なぜなら、人々がオブジェクトインスタンス(つまりthis
:)ポインタにアクセスできる場合、その同じオブジェクトをロックしようとすることもできるからです。今、彼らはあなたがthis
内部でロックしていることを認識していないかもしれません、それでこれは問題(おそらくデッドロック)を引き起こすかもしれません
これに加えて、ロックが多すぎるため、これも悪い習慣です。
たとえば、メンバー変数がありList<int>
、実際にロックする必要があるのはそのメンバー変数だけです。関数内のオブジェクト全体をロックすると、それらの関数を呼び出す他のものはロックを待機してブロックされます。これらの関数がメンバーリストにアクセスする必要がない場合は、他のコードが待機し、理由もなくアプリケーションの速度が低下することになります。
MSDNトピックのスレッドの同期(C#プログラミングガイド)をご覧ください
一般に、パブリックタイプ、またはアプリケーションの制御の及ばないオブジェクトインスタンスのロックは回避するのが最善です。たとえば、インスタンスがパブリックにアクセスできる場合、lock(this)は問題となる可能性があります。これは、制御できないコードもオブジェクトをロックする可能性があるためです。これにより、2つ以上のスレッドが同じオブジェクトの解放を待機するデッドロック状態が発生する可能性があります。オブジェクトではなく、パブリックデータ型をロックすると、同じ理由で問題が発生する可能性があります。リテラル文字列は共通言語ランタイム(CLR)によってインターンされるため、リテラル文字列のロックは特に危険です。これは、プログラム全体に対して特定の文字列リテラルのインスタンスが1つあることを意味します。まったく同じオブジェクトが、すべてのスレッドで、実行中のすべてのアプリケーションドメインのリテラルを表します。その結果、アプリケーションプロセスの任意の場所で同じ内容の文字列にロックをかけると、アプリケーション内のその文字列のすべてのインスタンスがロックされます。その結果、抑留されていないプライベートまたは保護されたメンバーをロックすることが最善です。一部のクラスは、ロック専用のメンバーを提供します。たとえば、ArrayタイプはSyncRootを提供します。多くのコレクション型はSyncRootメンバーも提供します。
私はこれが古いスレッドであることを知っていますが、人々はまだこれを調べてそれに頼ることができるので、それlock(typeof(SomeObject))
がよりもはるかに悪いことを指摘することが重要なようですlock(this)
。そうは言っても; それを指摘するためのアランへの誠実な称賛lock(typeof(SomeObject))
は悪い習慣であるます。
のインスタンス System.Type
、最も一般的で粗いオブジェクトの1つです。少なくとも、System.TypeのインスタンスはAppDomainに対してグローバルであり、.NETはAppDomainで複数のプログラムを実行できます。つまり、2つの完全に異なるプログラムは、同じタイプのインスタンスで同期ロックを取得しようとすると、デッドロックを作成する程度まで、互いに干渉する可能性があります。
そう lock(this)
ため、特に堅牢な形式ではなく、問題を引き起こす可能性があり、引用されているすべての理由で常に眉を上げる必要があります。しかし、個人的にはパターンの変更を確認したいのですが、lock(this)パターンを広範囲に使用するlog4netのように、広く使用されており、比較的評判が高く、明らかに安定しているコードがあります。
しかしlock(typeof(SomeObject))
、まったく新しく強化されたワームの缶を開きます。
それが価値があるもののために。
...そしてまったく同じ引数がこの構成にも適用されます:
lock(typeof(SomeObject))
lock(this)
特に論理的で簡潔に思われることに同意しません。これは非常に粗いロックであり、他のコードはオブジェクトのロックを取得し、内部コードに干渉を引き起こす可能性があります。よりきめ細かいロックを取り、より厳密な制御を想定します。何lock(this)
が起こっているのかは、それよりもはるかに優れているということですlock(typeof(SomeObject))
。
あなたのオフィスに、部門の共有リソースである熟練した秘書がいると想像してください。たまに、あなたは仕事をしているので彼らの方に急いで行きます。同僚の別の誰かがまだ彼らを要求していないことを望んでいるだけです。通常は、しばらく待つだけです。
思いやりは共有であるため、上司は顧客が秘書を直接使用することもできると判断しました。しかし、これには副次的な影響があります。顧客は、この顧客のために作業しているときに請求することさえあり、タスクの一部を実行することも必要です。クレームはもはや階層ではないため、デッドロックが発生します。これは、顧客がそもそも請求することを許可しないことで、すべて一緒に回避できたはずです。
lock(this)
私たちが見てきたように悪いです。外部オブジェクトはオブジェクトをロックする可能性があり、クラスを使用しているユーザーを制御できないため、誰でもそれをロックできます...これは、上記の正確な例です。この場合も、解決策はオブジェクトの露出を制限することです。しかし、あなたが持っている場合private
、protected
またはinternal
クラスが、あなたはすでにあなたのオブジェクトにロックしているユーザーを制御できますが、あなたが自分でコードを書いたことだから、。したがって、ここでのメッセージは、として公開しないでくださいpublic
。また、同様のシナリオでロックが確実に使用されるようにすることで、デッドロックを回避できます。
これとは正反対に、アプリドメイン全体で共有されるリソースをロックします。これは最悪のシナリオです。それはあなたの秘書を外に置き、そこにいる誰もがそれらを主張できるようにするようなものです。結果は完全な混乱です-またはソースコードの観点から:それは悪い考えでした。捨ててやり直してください。それでは、どうすればよいでしょうか。
ここのほとんどの人が指摘するように、タイプはアプリドメインで共有されます。しかし、私たちが使用できるさらに良いものがあります:文字列です。その理由は、文字列がプールされるためです。つまり、アプリドメインに同じ内容の2つの文字列がある場合、それらはまったく同じポインタを持っている可能性があります。ポインターはロックキーとして使用されるため、基本的には「未定義の動作の準備」の同義語が得られます。
同様に、WCFオブジェクト、HttpContext.Current、Thread.Current、シングルトン(一般的に)などをロックするべきではありません。これをすべて回避する最も簡単な方法は? private [static] object myLock = new object();
共有リソースをロックしている場合、このポインタをロックすることは悪いことです。共有リソースは、静的変数またはコンピューター上のファイル、つまりクラスのすべてのユーザー間で共有されるものにすることができます。その理由は、クラスがインスタンス化されるたびに、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);
}
}
これは、これをロックするプログラムの実行です。
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
Random rand = new Random();
nvm を使用するときに何が問題なのかを見つけるのは難しいと思います。繰り返されるバランスが見られると思います
Microsoft®.NETランタイムのパフォーマンスアーキテクトであるRico Marianiによるhttp://bytes.com/topic/c-sharp/answers/249277-dont-lock-type-objectsに関する非常に優れた記事があります。
抜粋:
ここでの基本的な問題は、typeオブジェクトを所有しておらず、他にだれがそれにアクセスできるかわからないということです。一般に、自分で作成していないオブジェクトをロックし、他に誰がアクセスしているかわからないオブジェクトをロックすることは非常に悪い考えです。これを行うと、デッドロックが発生します。最も安全な方法は、プライベートオブジェクトのみをロックすることです。
ここでこれについていくつかの良い議論があります:これはミューテックスの適切な使用ですか?
これは、はるかに単純な図(ここで質問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(); }
回避するために、この男はロックの代わりに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
SomeClass
も、同じデッドロックが発生します。また、メインクラスのロックがプログラムの別のプライベートインスタンスメンバーで行われた場合も、同じロックが発生します。したがって、この答えが誤解を招くものでなく、正しくないかどうかはわかりません。ここでその動作を確認してください:dotnetfiddle.net/DMrU5h
なぜなら、クラスのインスタンスを見ることができるコードの塊は、その参照をロックする可能性があるからです。ロックオブジェクトを非表示(カプセル化)して、オブジェクトを参照する必要のあるコードのみが参照できるようにします。thisキーワードは現在のクラスインスタンスを参照するため、任意の数のオブジェクトがそれを参照し、それを使用してスレッド同期を実行できます。
明確に言うと、他のコードのチャンクがクラスインスタンスを使用してロックし、コードがタイムリーなロックを取得できなかったり、他のスレッド同期の問題を引き起こしたりする可能性があるため、これは悪いことです。最良の場合:クラスへの参照を使用してロックするものは他にありません。中間のケース:何かがロックを行うためにクラスへの参照を使用し、パフォーマンスの問題を引き起こします。最悪の場合:何かがクラスの参照を使用してロックを行うと、本当に悪い、非常に微妙な、非常にデバッグが難しい問題が発生します。
申し訳ありませんが、これをロックするとデッドロックが発生する可能性があるという主張には同意できません。あなたは2つのことを混乱させています:デッドロックと飢えです。
ここでの違いを説明する画像です。
結論スレッドの枯渇が問題でない場合
でも、安全に使用できlock(this)
ます。lock(this)
オブジェクトをロックしているロックでスレッドが枯渇しているスレッドが終了すると、最終的に永遠の飢餓で終了することに注意してください;)
lock(this)
-この種のコードは単に間違っています。私はそれをデッドロックと呼ぶことは少し乱用だと思います。
ロック(これ)が適切でない理由を説明している次のリンクを参照してください。
http://blogs.msdn.com/b/bclteam/archive/2004/01/20/60719.aspx
したがって、解決策は、プライベートオブジェクト(たとえば、lockObject)をクラスに追加し、次に示すようにコード領域をロックステートメント内に配置することです。
lock (lockObject)
{
...
}
次に、より簡単なサンプルコードをいくつか示します(IMO):(LinqPadで機能します。次の名前空間を参照してください:System.NetおよびSystem.Threading.Tasks)
覚えておくべきことは、lock(x)は基本的には構文糖衣であり、Monitor.Enterを使用してからtry、catch、最後にblock。を使用してMonitor.Exitを呼び出すことです。参照:https : //docs.microsoft.com/en-us/dotnet/api/system.threading.monitor.enter(備考セクション)
または、C#ロックステートメント(Visual BasicではSyncLockステートメント)を使用します。これは、EnterおよびExitメソッドをtry…finallyブロックでラップします。
void Main()
{
//demonstrates why locking on THIS is BADD! (you should never lock on something that is publicly accessible)
ClassTest test = new ClassTest();
lock(test) //locking on the instance of ClassTest
{
Console.WriteLine($"CurrentThread {Thread.CurrentThread.ManagedThreadId}");
Parallel.Invoke(new Action[]
{
() => {
//this is there to just use up the current main thread.
Console.WriteLine($"CurrentThread {Thread.CurrentThread.ManagedThreadId}");
},
//none of these will enter the lock section.
() => test.DoWorkUsingThisLock(1),//this will dead lock as lock(x) uses Monitor.Enter
() => test.DoWorkUsingMonitor(2), //this will not dead lock as it uses Montory.TryEnter
});
}
}
public class ClassTest
{
public void DoWorkUsingThisLock(int i)
{
Console.WriteLine($"Start ClassTest.DoWorkUsingThisLock {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
lock(this) //this can be bad if someone has locked on this already, as it will cause it to be deadlocked!
{
Console.WriteLine($"Running: ClassTest.DoWorkUsingThisLock {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(1000);
}
Console.WriteLine($"End ClassTest.DoWorkUsingThisLock Done {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
}
public void DoWorkUsingMonitor(int i)
{
Console.WriteLine($"Start ClassTest.DoWorkUsingMonitor {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
if (Monitor.TryEnter(this))
{
Console.WriteLine($"Running: ClassTest.DoWorkUsingMonitor {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(1000);
Monitor.Exit(this);
}
else
{
Console.WriteLine($"Skipped lock section! {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
}
Console.WriteLine($"End ClassTest.DoWorkUsingMonitor Done {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
Console.WriteLine();
}
}
出力
CurrentThread 15
CurrentThread 15
Start ClassTest.DoWorkUsingMonitor 2 CurrentThread 13
Start ClassTest.DoWorkUsingThisLock 1 CurrentThread 12
Skipped lock section! 2 CurrentThread 13
End ClassTest.DoWorkUsingMonitor Done 2 CurrentThread 13
Thread#12はデッドロックされているため、決して終了しないことに注意してください。
DoWorkUsingThisLock
問題を説明するために2番目のスレッドは必要ないと思われますか?
「this」またはクラス内のコードがインスタンス化するオブジェクトをロックするコードをクラスに含めることができるというルールを確立できます。したがって、パターンが守られていない場合にのみ問題となります。
このパターンに従わないコードから身を守りたいなら、受け入れられた答えは正しいです。しかし、パターンが守られていれば問題ありません。
ロック(これ)の利点は効率です。単一の値を保持する単純な「値オブジェクト」がある場合はどうでしょうか。それは単なるラッパーであり、何百万回もインスタンス化されます。ロックのためだけにプライベート同期オブジェクトの作成を要求することにより、基本的にオブジェクトのサイズを2倍にし、割り当ての数を2倍にしました。パフォーマンスが重要な場合、これは利点です。
割り当ての数やメモリフットプリントを気にしない場合は、他の回答に示されている理由から、ロック(これ)を回避することが推奨されます。
同じオブジェクトインスタンスを使用している可能性のある他のリクエストが存在する可能性があるため、インスタンスがパブリックにアクセスできる場合、問題が発生します。プライベート/静的変数を使用することをお勧めします。