C#多线程之基础篇3

时间:2023-03-08 21:50:54

  在上一篇C#多线程之基础篇2中,我们主要讲述了确定线程的状态、线程优先级、前台线程和后台线程以及向线程传递参数的知识,在这一篇中我们将讲述如何使用C#的lock关键字锁定线程、使用Monitor锁定线程以及线程中的异常处理。

九、使用C#的lock关键字锁定线程

1、使用Visual Studio 2015创建一个新的控制台应用程序。

2、双击打开“Program.cs”文件,然后修改为如下代码:

 using System;
using System.Threading;
using static System.Console; namespace Recipe09
{
abstract class CounterBase
{
public abstract void Increment();
public abstract void Decrement();
} class Counter : CounterBase
{
public int Count { get; private set; } public override void Increment()
{
Count++;
} public override void Decrement()
{
Count--;
}
} class CounterWithLock : CounterBase
{
private readonly object syncRoot = new Object(); public int Count { get; private set; } public override void Increment()
{
lock (syncRoot)
{
Count++;
}
} public override void Decrement()
{
lock (syncRoot)
{
Count--;
}
}
} class Program
{
static void TestCounter(CounterBase c)
{
for (int i = ; i < ; i++)
{
c.Increment();
c.Decrement();
}
} static void Main(string[] args)
{
WriteLine("Incorrect counter");
var c1 = new Counter();
var t1 = new Thread(() => TestCounter(c1));
var t2 = new Thread(() => TestCounter(c1));
var t3 = new Thread(() => TestCounter(c1));
t1.Start();
t2.Start();
t3.Start();
t1.Join();
t2.Join();
t3.Join();
WriteLine($"Total count: {c1.Count}"); WriteLine("--------------------------"); WriteLine("Correct counter");
var c2 = new CounterWithLock();
t1 = new Thread(() => TestCounter(c2));
t2 = new Thread(() => TestCounter(c2));
t3 = new Thread(() => TestCounter(c2));
t1.Start();
t2.Start();
t3.Start();
t1.Join();
t2.Join();
t3.Join();
WriteLine($"Total count: {c2.Count}");
}
}
}

3、运行该控制台应用程序,运行效果(每次运行效果可能不同)如下图所示:

C#多线程之基础篇3

  在第65行代码处,我们创建了Counter类的一个对象,该类定义了一个简单的counter变量,该变量可以自增1和自减1。然后在第66~68行代码处,我们创建了三个线程,并利用lambda表达式将Counter对象传递给了“TestCounter”方法,这三个线程共享同一个counter变量,并且对这个变量进行自增和自减操作,这将导致结果的不正确。如果我们多次运行这个控制台程序,它将打印出不同的counter值,有可能是0,但大多数情况下不是。

  发生这种情况是因为Counter类是非线程安全的。我们假设第一个线程在第57行代码处执行完毕后,还没有执行第58行代码时,第二个线程也执行了第57行代码,这个时候counter的变量值自增了2次,然后,这两个线程同时执行了第58行处的代码,这会造成counter的变量只自减了1次,因此,造成了不正确的结果。

  为了确保不发生上述不正确的情况,我们必须保证在某一个线程访问counter变量时,另外所有的线程必须等待其执行完毕才能继续访问,我们可以使用lock关键字来完成这个功能。如果我们在某个线程中锁定一个对象,其他所有线程必须等到该线程解锁之后才能访问到这个对象,因此,可以避免上述情况的发生。但是要注意的是,使用这种方式会严重影响程序的性能。更好的方式我们将会在仙童同步中讲述。

十、使用Monitor锁定线程

  在这一小节中,我们将描述一个多线程编程中的常见的一个问题:死锁。我们首先创建一个死锁的示例,然后使用Monitor避免死锁的发生。

1、使用Visual Studio 2015创建一个新的控制台应用程序。

2、双击打开“Program.cs”文件,编写代码如下:

 using System;
using System.Threading;
using static System.Console;
using static System.Threading.Thread; namespace Recipe10
{
class Program
{
static void LockTooMuch(object lock1, object lock2)
{
lock (lock1)
{
Sleep();
lock (lock2)
{
}
}
} static void Main(string[] args)
{
object lock1 = new object();
object lock2 = new object(); new Thread(() => LockTooMuch(lock1, lock2)).Start(); lock (lock2)
{
WriteLine("This will be a deadlock!");
Sleep();
lock (lock1)
{
WriteLine("Acquired a protected resource succesfully");
}
}
}
}
}

3、运行该控制台应用程序,运行效果如下图所示:

C#多线程之基础篇3

  在上述结果中我们可以看到程序发生了死锁,程序一直结束不了。

  在第10~19行代码处,我们定义了一个名为“LockTooMuch”的方法,在该方法中我们锁定了第一个对象lock1,等待1秒钟后,希望锁定第二个对象lock2。

  在第26行代码处,我们创建了一个新的线程来执行“LockTooMuch”方法,然后立即执行第28行代码。

  在第28~32行代码处,我们在主线程中锁定了对象lock2,然后等待1秒钟后,希望锁定第一个对象lock1。

  在创建的新线程中我们锁定了对象lock1,等待1秒钟,希望锁定对象lock2,而这个时候对象lock2已经被主线程锁定,所以新建线程会等待对象lock2被主线程解锁。然而,在主线程中,我们锁定了对象lock2,等待1秒钟,希望锁定对象lock1,而这个时候对象lock1已经被创建的线程锁定,所以主线程会等待对象lock1被创建的线程解锁。当发生这种情况的时候,死锁就发生了,所以我们的控制台应用程序目前无法正常结束。

4、要避免死锁的发生,我们可以使用“Monitor.TryEnter”方法来替换lock关键字,“Monitor.TryEnter”方法在请求不到资源时不会阻塞等待,可以设置超时时间,获取不到直接返回false。修改代码如下所示:

 using System;
using System.Threading;
using static System.Console;
using static System.Threading.Thread; namespace Recipe10
{
class Program
{
static void LockTooMuch(object lock1, object lock2)
{
lock (lock1)
{
Sleep();
lock (lock2)
{
}
}
} static void Main(string[] args)
{
object lock1 = new object();
object lock2 = new object(); new Thread(() => LockTooMuch(lock1, lock2)).Start(); lock (lock2)
{
WriteLine("This will be a deadlock!");
Sleep();
//lock (lock1)
//{
// WriteLine("Acquired a protected resource succesfully");
//}
if (Monitor.TryEnter(lock1, TimeSpan.FromSeconds()))
{
WriteLine("Acquired a protected resource succesfully");
}
else
{
WriteLine("Timeout acquiring a resource!");
}
}
}
}
}

5、运行该控制台应用程序,运行效果如下图所示:

C#多线程之基础篇3

  此时,我们的控制台应用程序就避免了死锁的发生。

十一、处理异常

  在这一小节中,我们讲述如何在线程中正确地处理异常。正确地将try/catch块放置在线程内部是非常重要的,因为在线程外部捕获线程内部的异常通常是不可能的。

1、使用Visual Studio 2015创建一个新的控制台应用程序。

2、双击打开“Program.cs”文件,修改代码如下所示:

 using System;
using System.Threading;
using static System.Console;
using static System.Threading.Thread; namespace Recipe11
{
class Program
{
static void BadFaultyThread()
{
WriteLine("Starting a faulty thread...");
Sleep(TimeSpan.FromSeconds());
throw new Exception("Boom!");
} static void FaultyThread()
{
try
{
WriteLine("Starting a faulty thread...");
Sleep(TimeSpan.FromSeconds());
throw new Exception("Boom!");
}
catch(Exception ex)
{
WriteLine($"Exception handled: {ex.Message}");
}
} static void Main(string[] args)
{
var t = new Thread(FaultyThread);
t.Start();
t.Join(); try
{
t = new Thread(BadFaultyThread);
t.Start();
}
catch (Exception ex)
{
WriteLine(ex.Message);
WriteLine("We won't get here!");
}
}
}
}

3、运行该控制台应用程序,运行效果如下图所示:

C#多线程之基础篇3

  在第10~15行代码处,我们定义了一个名为“BadFaultyThread”的方法,在该方法中抛出一个异常,并且没有使用try/catch块捕获该异常。

  在第17~29行代码处,我们定义了一个名为“FaultyThread”的方法,在该方法中也抛出一个异常,但是我们使用了try/catch块捕获了该异常。

  在第33~35行代码处,我们创建了一个线程,在该线程中执行了“FaultyThread”方法,我们可以看到在这个新创建的线程中,我们正确地捕获了在“FaultyThread”方法中抛出的异常。

  在第37~46行代码处,我们又新创建了一个线程,在该线程中执行了“BadFaultyThread”方法,并且在主线程中使用try/catch块来捕获在新创建的线程中抛出的异常,不幸的的是我们在主线程中无法捕获在新线程中抛出的异常。

  由此可以看到,在一个线程中捕获另一个线程中的异常通常是不可行的。

  至此,多线程(基础篇)我们就讲述到这儿,之后我们将讲述线程同步相关的知识,敬请期待!

  源码下载