.NET基础 (19)多线程

时间:2023-03-10 05:36:30
.NET基础 (19)多线程

多线程编程的基本概念
1 请解释操作系统层面上的线程和进程
2 多线程程序在操作系统里是并行执行的吗
3 什么是纤程

.NET中的多线程
1 如何在.NET程序中手动控制多个线程
2 如何使用.NET的线程池
3 如何查看和设置线程池的上下文
4 如何定义线程独享的全局数据
5 如何使用异步模式读取一个文件
6 如何阻止线程执行上下文的传递

多线程程序的线程同步
1 什么是同步块和同步块索引
2 C#中的lock关键字有何作用
3 可否使用值类型对象来实现线程同步
4 可否对引用类型对象自身进行同步
5 什么是互斥体,Mutex类型和Monitor类型的功能有何区别

多线程编程的基本概念
1 请解释操作系统层面上的线程和进程

进程的概念

进程代表操作系统上运行着的一个应用程序。进程拥有自己的程序块,拥有独占的资源和数据,并且可以被操作系统来调度。即使同一个应用程序,当被强制多次启动时,也会被安装在不同的进程中单独运行。通过进程浏览器可以查看计算机真正运行的进程。但并不是每个进程都会在Windows自带的进程浏览器中显示。

线程的概念

线程是一个可以被调度的单元,并且维护自己的堆栈和上下文。线程是附属于进程的,一个进程可以包含一个或者多个线程,并且同一进程内的多个线程共享一块内存和资源。一个线程是一个操作系统看调度的基本单元,但同时它的调度受限于包含线程的进程,也就是说操作系统首先决定下一个执行的进程,进而才调度该进程内的线程。

线程和进程的区别

线程和进程最大的区别在于隔离性问题。每个进程都被单独地隔离,拥有自己的内存块、独占的资源及运行数据,一个进程的崩溃不会影响到其他进程,而进程之间的交互也是相当困难的。和进程不同,同一进程内的所有线程共享资源和内存块,并且一个线程可以访问、结束同一进程内的其他线程。

2 多线程程序在操作系统里是并行执行的吗

在单CPU的计算机架构上,任何时候只能存在一个运行的线程,操作系统通过快速地调度轮换使使用者感觉到多线程在同时运行。而在多CPU的架构上,则可能存在完全并行的线程,这取决于线程之间是否征用了其他的资源。

3 什么是纤程

纤程是微软提出的最轻量级线程的概念,一个纤程拥有自己的栈和寄存器状态。一个线程可以包含多个纤程,和线程可以由操作系统调度有所不同的是,线程内纤程的调度完全由程序员自己调度,操作系统的内核完全不知道纤程的存在。在.NET中线程的概念不一定和操作系统中的线程对应,在有些情况下.NET中的线程对应一个纤程。

.NET中的多线程
1 如何在.NET程序中手动控制多个线程

.NET提供了System.Threading.Thread类型封装了线程的操作,通过该类型,程序员可以手动地创建、查询、控制以及结束线程。

示例:

    class ThreadState
{
static void Main(string[] args)
{
Console.WriteLine("开始测试线程1"); //初始化一个线程
Thread thread1 = new Thread(Work1);
PrintState(thread1); //启动线程
Console.WriteLine("现在启动线程");
thread1.Start();
PrintState(thread1);
//让线程运行一段时间
Thread.Sleep( * ); //让线程挂起
Console.WriteLine("现在挂起线程");
thread1.Suspend();
//给线程足够的时间来挂起
//否则的话状态可能是SuspendRequested
Thread.Sleep();
PrintState(thread1); //继续线程
Console.WriteLine("现在继续线程");
thread1.Resume();
PrintState(thread1); //停止线程
Console.WriteLine("现在停止线程");
thread1.Abort();
//给线程足够的时间来停止
//否则的话状态可能是AbortRequested
Thread.Sleep();
PrintState(thread1); Console.WriteLine("开始测试线程2");
//初始化一个线程
Thread thread2 = new Thread(Work2);
PrintState(thread2); //查看睡眠状态
thread2.Start();
Thread.Sleep(*);
PrintState(thread2); //给线程足够的时间结束
Thread.Sleep( * );
PrintState(thread2); Console.Read();
}
/// <summary>
/// 线程方法
/// </summary>
private static void Work1()
{
Console.WriteLine("线程运行中...");
//模拟线程运行,但不改变线程状态
//采用忙等状态
while (true) ;
}
/// <summary>
/// 保证一个线程运行10秒就结束
/// </summary>
private static void Work2()
{
Console.WriteLine("线程开始睡眠");
//睡眠10秒就结束
Thread.Sleep( * );
Console.WriteLine("线程恢复运行");
} /// <summary>
/// 打印线程的状态
/// </summary>
/// <param name="thread"></param>
private static void PrintState(Thread thread)
{
Console.WriteLine("线程的状态是:{0}",
thread.ThreadState.ToString());
}
}

输出:

开始测试线程1
线程的状态是:Unstarted
现在启动线程
线程的状态是:Running
线程运行中...
现在挂起线程
线程的状态是:Suspended
现在继续线程
线程的状态是:Running
现在停止线程
线程的状态是:Aborted
开始测试线程2
线程的状态是:Unstarted
线程开始睡眠
线程的状态是:WaitSleepJoin
线程恢复运行
线程的状态是:Stopped

在.NET Framework 4.0之中,已经不再鼓励使用线程的挂起状态,以及Suspect和Resume方法。

2 如何使用.NET的线程池

System.Threading.ThredPool类型封装了线程池的操作。每一个进程都拥有一个线程池,.NET提供了线程池管理的机制,用户只需要把线程需求插入到线程池中,而不必再理会后续的工作。管理的策略是可变的,当使用者在短时间内投递了相当多的需求时,CLR的线程池管理代码可能会同时运行多个线程来处理需求,而当使用者投递较少的需求时,CLR可能只创建单线程来处理需求。所有线程池中的线程都是后台线程,它们不会阻碍程序的退出。

示例:

    class ThreadPool
{
/// <summary>
/// 插入工作者线程
/// </summary>
/// <param name="args"></param>
static void Main(string[] args)
{
String taskinfo = "运行10秒";
//插入一个新的请求到线程池
bool result = System.Threading.
ThreadPool.QueueUserWorkItem(
DoWork, taskinfo);
if (!result)
Console.WriteLine("分配线程失败");
else
Console.WriteLine("按回车可结束程序");
Console.Read();
} /// <summary>
/// 线程的方法
/// 必须符合WaitCallback委托的申明
/// </summary>
/// <param name="state"></param>
static void DoWork(Object state)
{
//模拟做了一些工作,大约会执行10s
for (int i = ; i < ; i++)
{
Console.WriteLine(
"工作者线程的任务是:{0}",
state);
Thread.Sleep();
}
}
}

输出:

按回车可结束程序
工作者线程的任务是:运行10秒
工作者线程的任务是:运行10秒
工作者线程的任务是:运行10秒
工作者线程的任务是:运行10秒
工作者线程的任务是:运行10秒
工作者线程的任务是:运行10秒
工作者线程的任务是:运行10秒
工作者线程的任务是:运行10秒
工作者线程的任务是:运行10秒
工作者线程的任务是:运行10秒

3 如何查看和设置线程池的上下文

System.Threading.ThreadPool类型提供了查看和修改线程池上下限值的方法,在.NET2.0之后,默认的线程池上限已经修改得足够大而可以满足觉大部分系统的要求。在实际应用中,程序员应该避免无意义地修改线程池的阈值。

示例:

     class ThreadPoolMaxMin
{
static void Main(string[] args)
{
//打印阀值和可用数量
GetLimitation();
GetAvailable();
//使用掉三个线程
Console.WriteLine("申请使用3个线程...");
ThreadPool.QueueUserWorkItem(Work);
ThreadPool.QueueUserWorkItem(Work);
ThreadPool.QueueUserWorkItem(Work);
Thread.Sleep();
//打印阀值和可用数量
GetLimitation();
GetAvailable();
//设置最小值
Console.WriteLine("修改了线程池的最小线程数");
ThreadPool.SetMinThreads(, );
//打印阀值
GetLimitation();
Console.Read();
} /// <summary>
/// 运行10秒的线程方法
/// </summary>
/// <param name="o"></param>
static void Work(Object o)
{
Thread.Sleep( * );
}
/// <summary>
/// 打印线程池上下阀值
/// </summary>
static void GetLimitation()
{
int maxwork;
int minwork;
int maxio;
int minio;
ThreadPool.GetMaxThreads(out maxwork, out maxio);
ThreadPool.GetMinThreads(out minwork, out minio);
Console.WriteLine("线程池最多有{0}个工作者线程" +
",{1}个IO线程", maxwork.ToString(),
maxio.ToString());
Console.WriteLine("线程池最少有{0}个工作者线程" +
",{1}个IO线程", minwork.ToString(),
minio.ToString());
}
/// <summary>
/// 打印可用线程数量
/// </summary>
static void GetAvailable()
{
int remainwork;
int remainio;
ThreadPool.GetAvailableThreads(
out remainwork, out remainio);
Console.WriteLine("线程池中有{0}个工作者线程可用" +
",{1}个IO线程可用", remainwork.ToString(),
remainio.ToString());
}
}

输出:

线程池最多有32767个工作者线程,1000个IO线程
线程池最少有8个工作者线程,8个IO线程
线程池中有32767个工作者线程可用,1000个IO线程可用
申请使用3个线程...
线程池最多有32767个工作者线程,1000个IO线程
线程池最少有8个工作者线程,8个IO线程
线程池中有32764个工作者线程可用,1000个IO线程可用
修改了线程池的最小线程数
线程池最多有32767个工作者线程,1000个IO线程
线程池最少有10个工作者线程,10个IO线程

4 如何定义线程独享的全局数据

线程本地存储(TLS),是指存储在线程环境块内的一个结构,用来存放该线程内独享的数据。进程内的线程不能访问不属于自己的TLS,这就保证了TLS内的数据在线程内是全局共享的,在线程外却是不可见的。

定义在System.Threading.Thread类型内的AllocateDataSlot和AllocateNameDataSlot方法负责提供一个存储在所有线程内的数据插槽,而通过这个插槽结构,程序员可以通过SetData和GetData来存取线程独享数据。

示例:使用线程数据插槽

     class MainClass
{
/// <summary>
/// 测试数据插槽
/// </summary>
static void Main()
{
Console.WriteLine("现在开始测试数据插槽");
//开辟五个线程来同时运行
//这里不适合用线程池,
//因为线程池内的线程会被反复使用
//导致线程ID一致
for (int i = ; i < ; i++)
{
Thread thread =
new Thread(ThreadDataSlot.Work);
thread.Start();
}
Console.Read();
}
} /// <summary>
/// 包含线程方法和数据插槽
/// </summary>
class ThreadDataSlot
{
//分配一个数据插槽,注意插槽本身是全局可见的,
//因为这里的分配是在所有线程的TLS内建立数据块
static LocalDataStoreSlot _localSlot =
Thread.AllocateDataSlot(); /// <summary>
/// 线程方法,操作数据插槽来存放数据
/// </summary>
public static void Work()
{
// 这里把线程ID存放在数据插槽内
// 一个应用程序内线程ID不会重复
Thread.SetData(_localSlot,
Thread.CurrentThread.ManagedThreadId); // 查看一下刚刚插入的数据
Console.WriteLine("线程{0}内的数据是:{1}",
Thread.CurrentThread.ManagedThreadId.ToString(),
Thread.GetData(_localSlot).ToString()); // 这里线程睡眠1秒
Thread.Sleep(); //查看其它线程的运行是否干扰了当前线程数据插槽内的数据
Console.WriteLine("线程{0}内的数据是:{1}",
Thread.CurrentThread.ManagedThreadId.ToString(),
Thread.GetData(_localSlot).ToString());
}
}

输出:

现在开始测试数据插槽
线程11内的数据是:11
线程12内的数据是:12
线程13内的数据是:13
线程14内的数据是:14
线程15内的数据是:15
线程11内的数据是:11
线程12内的数据是:12
线程13内的数据是:13
线程14内的数据是:14
线程15内的数据是:15

同时,.NET提供了名为ThreadStatic的特性来申明线程的独享数据。

示例:

     class MainClass
{
/// <summary>
/// 测试线程静态字段
/// </summary>
static void Main()
{
Console.WriteLine("现在开始测试线程静态字段");
//开辟五个线程来同时运行
//这里不适合用线程池,
//因为线程池内的线程会被反复使用
//导致线程ID一致
for (int i = ; i < ; i++)
{
Thread thread =
new Thread(ThreadStatic.Work);
thread.Start();
}
Console.Read();
}
} /// <summary>
/// 包含线程静态数据
/// </summary>
class ThreadStatic
{
//值类型的线程静态数据
[ThreadStatic]
static int _threadid = ; //引用类型的线程静态数据
static Ref _refthreadid = new Ref(); /// <summary>
/// 线程方法,操作线程静态数据
/// </summary>
public static void Work()
{
// 存储线程ID
// 一个应用程序内线程ID不会重复
_threadid = Thread.CurrentThread.ManagedThreadId;
_refthreadid._id = Thread.CurrentThread.ManagedThreadId; // 查看一下刚刚插入的数据
Console.WriteLine("[{0}线程]:线程静态值变量:{1}"+
",线程静态引用变量:{2}",
Thread.CurrentThread.ManagedThreadId.ToString(),
_threadid,
_refthreadid._id.ToString()); // 这里线程睡眠1秒
Thread.Sleep(); //查看其它线程的运行是否干扰了当前线程静态数据
Console.WriteLine("[{0}线程]:线程静态值变量:{1}" +
",线程静态引用变量:{2}",
Thread.CurrentThread.ManagedThreadId.ToString(),
_threadid,
_refthreadid._id.ToString());
}
}
/// <summary>
/// 简单引用类型
/// </summary>
class Ref
{
public int _id;
}

输出:

现在开始测试线程静态字段
[9线程]:线程静态值变量:9,线程静态引用变量:9
[10线程]:线程静态值变量:10,线程静态引用变量:10
[11线程]:线程静态值变量:11,线程静态引用变量:11
[12线程]:线程静态值变量:12,线程静态引用变量:12
[13线程]:线程静态值变量:13,线程静态引用变量:13
[9线程]:线程静态值变量:9,线程静态引用变量:13
[10线程]:线程静态值变量:10,线程静态引用变量:13
[11线程]:线程静态值变量:11,线程静态引用变量:13
[12线程]:线程静态值变量:12,线程静态引用变量:13
[13线程]:线程静态值变量:13,线程静态引用变量:13

5 如何使用异步模式读取一个文件

异步模式区别于线程池机制的地方在于其允许程序查看操作的执行状态,而如果利用线程池的后台线程,则无法确切地知道操作的进行状态以及是否已经结束。

调用FileStream的BeginRead和EndRead方法可以实现异步读取文件。

示例:异步读取文件

主线程负责开始异步读取并且传入聚集时需要使用的方法和状态对象。

     partial class AsyncReadFile
{
//测试文件
const String _testFile = "C:\\TestAsyncRead.txt";
static void Main(string[] args)
{
try
{
//创建测试文件
if (File.Exists(_testFile))
File.Delete(_testFile);
using (FileStream fs = File.Create(_testFile))
{
String content = "我是文件内容。";
Byte[] contentbyte = Encoding.Default.GetBytes(content);
fs.Write(contentbyte, , contentbyte.Length);
}
//开始异步读取文件内容
using (FileStream fs = new FileStream(_testFile, FileMode.Open,
FileAccess.Read, FileShare.Read, ,FileOptions.Asynchronous))
{
Byte[] data = new Byte[];
ReadFileClass rfc = new ReadFileClass(fs, data);
//这里开始异步读取
IAsyncResult ir = fs.BeginRead(data, , , FinishReading, rfc);
//这里模拟做了一些其他的工作
Thread.Sleep( * );
Console.Read();
}
}
finally
{
//这里做清理工作
try
{
if (File.Exists(_testFile))
File.Delete(_testFile);
}
finally { }
}
}
}
partial class AsyncReadFile
{
/// <summary>
/// 完成异步读取时调用的方法
/// </summary>
/// <param name="ir">状态对象</param>
static void FinishReading(IAsyncResult ir)
{
ReadFileClass rfc = (ReadFileClass)ir.AsyncState;
//这一步是必须的
//这会让异步读取占用的资源被释放
int length = rfc._fs.EndRead(ir);
Console.WriteLine("读取文件结束。\r\n文件的长度为:{0}\r\n文件内容为:",length.ToString());
Byte[] result = new Byte[length];
Array.Copy(rfc._data, , result, , length);
Console.WriteLine(Encoding.Default.GetString(result));
}
}
/// <summary>
/// 打包传递给完成异步后回调的方法
/// </summary>
class ReadFileClass
{
public FileStream _fs;
public Byte[] _data;
public ReadFileClass(FileStream fs, Byte[] data)
{
_fs = fs;
_data = data;
}
}

输出:

读取文件结束。
文件的长度为:14
文件内容为:
我是文件内容。

6 如何阻止线程执行上下文的传递

线程的执行上下文

在.NET中每个线程都会包含一个执行上下文,执行上下文是指线程运行中某时刻的上下文概念,可以说它是一个动态过程的快照。定义在System.Threading中的ExecutionContext类型代表了一个执行上下文。执行上下文包含下列内容:

安全上下文

调用上下文

同步上下文

本地化上下文

事务上下文

CLR宿主上下文

线程的执行上下文相当于一个所有上下文的打包类型,我们可以把所有这些和称为线程上下文。

上下文的流动

当程序中新建一个线程时,执行上下文会自动从当前线程流入到新建的线程中,这样做可以保证新建的线程天生具有和主线程相同的安全设置和文化设置。

撇去功能上的需求,执行上下文的流动却是使得程序的执行效率下降很多,程序上下文的包装是一个成本较高的工作,有时候这个包装不是必要的。可以通过System.Threading.ThreadPool 类型中的UnsafeQueueUserWorkItem方法和定义在ExecutionContext类型中的SuppressFlow方法来阻止执行上下文的流动。

多线程程序的线程同步
1 什么是同步块和同步块索引

.NET为每个堆内对象分配一个同步索引,该索引中只保存一个表明数组内索引的整数。.NET在加载时会新建一个同步块数组,当某个对象需要被同步时,.NET会为其分配一个同步块,并且把该同步块在同步块数组中的索引加入该对象的同步块索引中。

同步块机制包含如下几点:

  • 在.NET被加载时初始化同步块数组。
  • 每一个被分配在堆上的对象都会包含2个额外的字段,其中一个存储类型指针,而另外一个就是同步块索引,初始时被赋值为-1.
  • 当一个线程试图使用该对象进入同步时,会检查该对象的同步索引。如果索引为负数,则会在同步块数组中寻找或者创建一个同步块,并且把同步块的索引值写入该对象的同步索引中。如果对象的同步索引不为负值,则找到该对象的同步块并且检查是否有其他线程在使用该同步块,如果有则进入等待状态,如果没有则申明使用该同步块。
  • 当一个对象退出同步时,该对象的同步索引被赋值为-1,并且对应的同步块数组内的同步块被视为不再使用。

.NET基础 (19)多线程

2 C#中的lock关键字有何作用

C#中lock关键字实质上是调用Monitor.Enter和Monitor.Exit两个方法的简化语法,功能上实现了 进入和退出某个对象的同步。通常,通过lock一个私有的引用成员变量俩完成方法内的线程同步,而通过lock一个私有的静态引用成员变量来完成静态方法内的线程同步。

示例:

    /// <summary>
/// 程序入口
/// </summary>
class MainClass
{
/// <summary>
/// 测试同步效果
/// </summary>
static void Main(string[] args)
{
Console.WriteLine("开始测试静态方法的同步");
for (int i = ; i < ; i++)
{
Thread t = new Thread(Lock.Increment1);
t.Start();
}
//这里等待线程执行结束
Thread.Sleep(*);
Console.WriteLine("开始测试成员方法的同步");
Lock l = new Lock();
for (int i = ; i < ; i++)
{
Thread t = new Thread(l.Increment2);
t.Start();
}
Console.Read();
}
} /// <summary>
/// 演示同步锁
/// </summary>
public class Lock
{
//用来在静态方法中同步
private static Object o1 = new object();
//用来在成员方法中不同
private Object o2 = new object();
//成员变量
private static int i1 = ;
private int i2 = ;
/// <summary>
/// 测试静态方法的同步
/// </summary>
/// <param name="state">状态对象</param>
public static void Increment1(Object state)
{
lock (o1)
{
Console.WriteLine("i1的值为:{0}", i1.ToString());
//这里刻意制造线程并行机会
//来检查同步的功能
Thread.Sleep();
i1++;
Console.WriteLine("i1自增后为:{0}", i1.ToString());
}
}
/// <summary>
/// 测试成员方法的同步
/// </summary>
/// <param name="state">状态对象</param>
public void Increment2(Object state)
{
lock (o2)
{
Console.WriteLine("i2的值为:{0}", i2.ToString());
//这里刻意制造线程并行机会
//来检查同步的功能
Thread.Sleep();
i2++;
Console.WriteLine("i2自增后为:{0}", i2.ToString());
}
}
}

输出:

开始测试静态方法的同步
i1的值为:0
i1自增后为:1
i1的值为:1
i1自增后为:2
i1的值为:2
i1自增后为:3
i1的值为:3
i1自增后为:4
i1的值为:4
i1自增后为:5
开始测试成员方法的同步
i2的值为:0
i2自增后为:1
i2的值为:1
i2自增后为:2
i2的值为:2
i2自增后为:3
i2的值为:3
i2自增后为:4
i2的值为:4
i2自增后为:5

3 可否使用值类型对象来实现线程同步

值类型对象分配在堆栈上,没有同步索引字段,所以不能用来进行同步。即使使用了装箱机制,也会导致同步失败。对值类型使用lock关键字将导致一个编译错误,而对值类型使用Monitor.Enter和Monitor.Exit方法,将导致一个运行时的错误。

4 可否对引用类型对象自身进行同步

把对象自身作为同步对象,会导致类型缺乏健壮性。当某个使用者恶意长期占用对象的同步块时,所有其他使用者将会被死锁。

应该避免使用this对象和当前类型对象作为同步对象,而应该在类型中定义私有的同步对象,同时应该使用lock而不是Monitor类型,这样可以有效地减少同步块不被释放的情况。

示例:死锁示例

     class MainClass
{
/// <summary>
/// 使用SynchroThis
/// </summary>
/// <param name="args"></param>
static void Main(string[] args)
{
SynchroThis st = new SynchroThis(); //恶意的使用者
Monitor.Enter(st); //正常的使用者
//但是受到恶意使用者的影响
//这里的代码完全正确,却被死锁
Thread t = new Thread(st.Work);
t.Start();
t.Join(); //程序不会执行到这里
Console.WriteLine("使用结束");
Console.Read();
}
}
/// <summary>
/// 使用this来同步线程
/// 缺乏健壮性的类型
/// </summary>
class SynchroThis
{
private int i = ; /// <summary>
/// 使用this来同步线程
/// </summary>
/// <param name="state"></param>
public void Work(Object state)
{
lock (this)
{
Console.WriteLine("i的值为:{0}",
i.ToString());
i++;
//模拟做了其他工作
Thread.Sleep();
Console.WriteLine("i自增1后的值为:{0}",
i.ToString());
}
}
}

5 什么是互斥体,Mutex类型和Monitor类型的功能有何区别

互斥体是操作系统内同步线程的内核对象,有相应的Win32函数来操作互斥体对象。在.NET中,Mutex类型封装了所有互斥体的操作,和Monitor类型相比,Mutex类型的作用可以跨进程,相应的,因为是在操作系统的内核中完成,所以Mutex类型的性能较差。

转载请注明出处:

作者:JesseLZJ
出处:http://jesselzj.cnblogs.com