一、线程的基础知识
1.1 System.Threading.Thread类
System.Threading.Thread是用于控制线程的基础类,通过Thread可以控制当前应用程序域中线程的创建、挂起、停止、销毁。公共属性如下:
属性名称 | 说明 |
---|---|
CurrentContext | 获取线程正在其中执行的当前上下文。 |
CurrentThread | 获取当前正在运行的线程。 |
ExecutionContext | 获取一个 ExecutionContext 对象,该对象包含有关当前线程的各种上下文的信息。 |
IsAlive | 获取一个值,该值指示当前线程的执行状态。 |
IsBackground | 获取或设置一个值,该值指示某个线程是否为后台线程。 |
IsThreadPoolThread | 获取一个值,该值指示线程是否属于托管线程池。 |
ManagedThreadId | 获取当前托管线程的唯一标识符。 |
Name | 获取或设置线程的名称。 |
Priority | 获取或设置一个值,该值指示线程的调度优先级。 |
ThreadState | 获取一个值,该值包含当前线程的状态。 |
1.1.1 线程优先级别
成员名称 | 说明 |
---|---|
Lowest | 可以将 Thread 安排在具有任何其他优先级的线程之后。 |
BelowNormal | 可以将 Thread 安排在具有 Normal 优先级的线程之后,在具有 Lowest 优先级的线程之前。 |
Normal | 默认选择。可以将 Thread 安排在具有 AboveNormal 优先级的线程之后,在具有BelowNormal 优先级的线程之前。 |
AboveNormal | 可以将 Thread 安排在具有 Highest 优先级的线程之后,在具有 Normal 优先级的线程之前。 |
Highest | 可以将 Thread 安排在具有任何其他优先级的线程之前。 |
1.1.2 线程常用方法
Thread 中包括了多个方法来控制线程的创建、挂起、停止、销毁。
方法名称 | 说明 |
---|---|
Abort() | 终止本线程。 |
GetDomain() | 返回当前线程正在其中运行的当前域。 |
GetDomainId() | 返回当前线程正在其中运行的当前域Id。 |
Interrupt() | 中断处于 WaitSleepJoin 线程状态的线程。 |
Join() | 已重载。 阻塞调用线程,直到某个线程终止时为止。 |
Resume() | 继续运行已挂起的线程。 |
Start() | 执行本线程。 |
Suspend() | 挂起当前线程,如果当前线程已属于挂起状态则此不起作用 |
Sleep() | 把正在运行的线程挂起一段时间。 |
1.1.3 简单示例
class Program
{
static void Main(string[] args)
{
ThreadDemoClass demoClass = new ThreadDemoClass();
//创建一个新的线程
Thread thread = new Thread(demoClass.Run);
//设置为后台线程
thread.IsBackground = true;
//开始线程
thread.Start();
Console.WriteLine("Main thread working...");
Console.WriteLine("Main thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString());//主线程
Console.ReadKey();
}
}
public class ThreadDemoClass
{
public void Run()
{
Console.WriteLine("Child thread working...");
Console.WriteLine("Child thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString());//子线程
}
}
执行结果:
二、以ThreadStart方式实现多线程
2.1 使用ThreadStart委托
修改:在main()中通过ThreadStart委托绑定ThreadDemoClass对象的Run()方法,然后通过Thread.Start()执行异步方法。
//创建一个委托,并把要执行的方法作为参数传递给这个委托
ThreadStart threadStart = new ThreadStart(demoClass.Run);
Thread thread = new Thread(threadStart);
//或
Thread thread = new Thread(new ThreadStart(demoClass.Run));
执行结果:
请注意运行结果,在调用Thread.Start()方法后,系统以异步方式运行ThreadDemoClass.Run(),而主线程的操作是继续执行的,在ThreadDemoClass.Run()完成前,主线程已完成所有的操作。这就涉及到了线程异步或同步的问题了,这个我们后面再说。
继续上面的问题,那么如果我想要等到子线程执行完成之后再继续主线程的工作呢(当然,我觉得一般不会有这种需求)。我们可以使用 Join() 这个方法,修改之后的代码。
class Program
{
static void Main(string[] args)
{
ThreadDemoClass demoClass = new ThreadDemoClass();
//创建一个新的线程
Thread thread = new Thread(demoClass.Run);
//设置为后台线程
thread.IsBackground = true;
//开始线程
thread.Start();
//等待直到线程完成
thread.Join();
Console.WriteLine("Main thread working...");
Console.WriteLine("Main thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString());
Console.ReadKey();
}
}
public class ThreadDemoClass
{
public void Run()
{
Console.WriteLine("Child thread working...");
Console.WriteLine("Child thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString());
}
}
执行结果:
上面的代码相比之前就添加了一句 thread.Join(),它的作用就是用于阻塞后面的线程,直到当前线程完成之后。当然,还有其他的方法可以做到,比如我们现在把 thread.Join() 换成下面这句代码
//挂起 当前线程指定的时间
Thread.Sleep(100);
就当前的场景来说,这样的确可以满足需求,但是这样做有一个弊端,就是,当子线程所执行的方法逻辑比较复杂耗时较长的时候,这样的方式就不一定可以,虽然可以修改线程挂起的时间,但是这个执行的时间却是不定的。所以,Thread.Sleep() 方法一般用来设置多线程之间执行的间隔时间的。
另外,Join() 方法也接受一个参数,该参数用于指定阻塞线程的时间,如果在指定的时间内该线程没有终止,那么就返回 false,如果在指定的时间内已终止,那么就返回 true。
2.2 使用ParameterizedThreadStart委托
当需要对线程调用的方法传入参数和接收返回值时,可以使用 ParameterizedThreadStart 委托来创建多线程,这个委托可以接受一个 object 类型的参数。
注意:上面Thread方法和ThreadStart方法是不接受参数并且没有返回值的。
示例一:
class Program
{static void Main(string[] args)
{
ThreadDemoClass demoClass = new ThreadDemoClass();
//创建一个委托,并把要执行的方法作为参数传递给这个委托
ParameterizedThreadStart threadStart = new ParameterizedThreadStart(demoClass.Run);
//创建一个新的线程
Thread thread = new Thread(threadStart);
//开始线程,并传入参数
thread.Start("Brambling");
Console.WriteLine("Main thread working...");
Console.WriteLine("Main thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString());
Console.ReadKey();
}
}
public class ThreadDemoClass
{
public void Run(object obj)
{
string name = obj as string;
Console.WriteLine("Child thread working...");
Console.WriteLine("My name is " + name);
Console.WriteLine("Child thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString());
}
}
执行结果:
PS:这里我没有加这句代码了(thread.IsBackground = true,即把当前线程设置为后台线程),因为使用 thread.Start() 启动的线程默认为前台线程。那么前台线程和后台线程有什么区别呢?
前台线程就是系统会等待所有的前台线程运行结束后,应用程序域才会自动卸载。而设置为后台线程之后,应用程序域会在主线程执行完成时被卸载,而不会等待异步线程的执行完成。
示例二:
传参是object 类型,也就是说既可以是值类型或引用类型,也可以是自定义类型。下面使用自定义类型作为参数传递。
class Program
{
static void Main(string[] args)
{
ThreadDemoClass demoClass = new ThreadDemoClass();
//创建一个委托,并把要执行的方法作为参数传递给这个委托
ParameterizedThreadStart threadStart = new ParameterizedThreadStart(demoClass.Run);
//创建一个新的线程
Thread thread = new Thread(threadStart);
UserInfo userInfo = new UserInfo();
userInfo.Name = "Brambling";
userInfo.Age = 333;
//开始线程,并传入参数
thread.Start(userInfo);
Console.WriteLine("Main thread working...");
Console.WriteLine("Main thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString());
Console.ReadKey();
}
}
public class ThreadDemoClass
{
public void Run(object obj)
{
UserInfo userInfo = (UserInfo)obj;
Console.WriteLine("Child thread working...");
Console.WriteLine("My name is " + userInfo.Name);
Console.WriteLine("I'm " + userInfo.Age + " years old this year");
Console.WriteLine("Child thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString());
}
}
public class UserInfo
{
public string Name { get; set; }
public int Age { get; set; }
}
执行结果:
2.3 挂起线程
Thread.Sleep(int)阻塞主线程。
.NET专门为等待异步线程完成开发了另一个方法thread.Join(),可以保证主线程在异步线程thread运行结束后才会终止。
2.4 终止线程
若想终止正在运行的线程,可以使用Abort()方法。在使用Abort()的时候,将引发一个特殊异常 ThreadAbortException 。若想在线程终止前恢复线程的执行,可以在捕获异常后 ,在catch(ThreadAbortException ex){…} 中调用Thread.ResetAbort()取消终止。
而使用Thread.Join()可以保证应用程序域等待异步线程结束后才终止运行。
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Main threadId is:" + Thread.CurrentThread.ManagedThreadId);
Thread thread = new Thread(new ThreadStart(AsyncThread));
thread.IsBackground = true;
thread.Start();
thread.Join();
}
//以异步方式调用
static void AsyncThread()
{
try
{
string message = string.Format("\nAsync threadId is:{0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine(message);
for (int n = 0; n < 10; n++)
{
if (n >= 4) //当n等于4时,终止线程
{
Thread.CurrentThread.Abort(n);
}
Thread.Sleep(300);
Console.WriteLine("The number is:" + n.ToString());
}
}
catch (ThreadAbortException ex)
{
//输出终止线程时n的值
if (ex.ExceptionState != null)
Console.WriteLine(string.Format("Thread abort when the number is: {0}!",
ex.ExceptionState.ToString()));
Thread.ResetAbort(); //取消终止,继续执行线程
Console.WriteLine("Thread ResetAbort!");
Console.WriteLine("Thread Close!"); //线程结束
Console.ReadKey();
}
}
}
执行结果:
三、线程池
3.1 关于CLR线程池
使用ThreadStart与ParameterizedThreadStart建立新线程非常简单,但通过此方法建立的线程难于管理,若建立过多的线程反而会影响系统的性能。
出于对性能的考虑,.NET引入CLR线程池这个概念。CLR线程池并不会在CLR初始化的时候立刻建立线程,而是在应用程序要创建线程来执行任务时,线程池才初始化一个线程。线程的初始化与其他的线程一样。在完成任务以后,该线程不会自行销毁,而是以挂起的状态返回到线程池。直到应用程序再次向线程池发出请求时,线程池里挂起的线程就会再度激活执行任务。这样既节省了建立线程所造成的性能损耗,也可以让多个任务反复重用同一线程,从而在应用程序生存期内节约大量开销。
CLR线程池分为工作者线程(workerThreads)与I/O线程 (completionPortThreads) 两种,工作者线程是主要用作管理CLR内部对象的运作,I/O(Input/Output) 线程顾名思义是用于与外部系统交换信息。
注:通过CLR线程池所建立的线程总是默认为后台线程,优先级数为ThreadPriority.Normal。
3.2 工作者线程
工作者线程一般有两种方式,一是直接通过 ThreadPool.QueueUserWorkItem() 方法,二是通过委托。
3.2.1 通过QueueUserWorkItem启动工作者线程
ThreadPool线程池中包含有两个静态方法可以直接启动工作者线程:
一为 ThreadPool.QueueUserWorkItem(WaitCallback)
二为 ThreadPool.QueueUserWorkItem(WaitCallback,Object)
static void Main(string[] args)
{
ThreadDemoClass demoClass = new ThreadDemoClass();
//设置线程池按需创建的线程的最小数量
//第一个参数为由线程池根据需要创建的新的最小工作程序线程数
//第二个参数为异步 I/O 线程数
ThreadPool.SetMinThreads(5, 5);
//设置同时处于活动状态的线程池的线程数,所有大于次数目的请求将保持排队状态,直到线程池变为可用
//第一个参数为线程池中辅助线程的最大数目
//第二个参数为异步 I/O 线程数
ThreadPool.SetMaxThreads(100, 100);
//使用委托绑定线程池要执行的方法(无参数)
WaitCallback waitCallback1 = new WaitCallback(demoClass.Run1);
//将方法排入队列,在线程池变为可用时执行
ThreadPool.QueueUserWorkItem(waitCallback1);
//使用委托绑定线程池要执行的方法(有参数)
WaitCallback waitCallback2 = new WaitCallback(demoClass.Run1);
//将方法排入队列,在线程池变为可用时执行
ThreadPool.QueueUserWorkItem(waitCallback2, "Brambling");
UserInfo userInfo = new UserInfo();
userInfo.Name = "Brambling";
userInfo.Age = 33;
//使用委托绑定线程池要执行的方法(自定义类型的参数)
WaitCallback waitCallback3 = new WaitCallback(demoClass.Run2);
//将方法排入队列,在线程池变为可用时执行
ThreadPool.QueueUserWorkItem(waitCallback3, userInfo);
Console.WriteLine();
Console.WriteLine("Main thread working...");
Console.WriteLine("Main thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString());
Console.ReadKey();
}
public class ThreadDemoClass
{
public void Run1(object obj)
{
string name = obj as string;
Console.WriteLine();
Console.WriteLine("Child thread working...");
Console.WriteLine("My name is " + name);
Console.WriteLine("Child thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString());
}
public void Run2(object obj)
{
UserInfo userInfo = (UserInfo)obj;
Console.WriteLine();
Console.WriteLine("Child thread working...");
Console.WriteLine("My name is " + userInfo.Name);
Console.WriteLine("I'm " + userInfo.Age + " years old this year");
Console.WriteLine("Child thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString());
}
}
public class UserInfo
{
public string Name { get; set; }
public int Age { get; set; }
}
执行结果:
发现两个问题:第一,每次运行的时候,输出的内容的顺序都不一定是一样的(不只是线程池,前面的也是),这涉及到线程同步/异步、线程阻塞/非阻塞、线程优先级、线程安全的问题,这个在下一讲再说;第二,使用线程池建立的线程也可以选择传递参数或不传递参数,并且参数也可以是值类型或引用类型(包括自定义类型),但以上多线程示例都没有返回值,下面将会讲解。
3.2.2 异步委托
通过ThreadPool.QueueUserWorkItem启动工作者线程虽然是方便,但WaitCallback委托指向的必须是一个带有Object参数的无返回值方法,这无疑是一种限制。若方法需要有返回值,或者带有多个参数,这将多费周折。因此,.NET提供了另一种方式去建立工作者线程,那就是委托。
3.2.2.1 利用BeginInvoke与EndInvoke完成异步委托方法
BeginInvoke() 方法用于异步委托的执行开始;EndInvoke() 方法用于结束异步委托,并获取异步委托执行完成后的返回值;IAsyncResult.IsCompleted 用于监视异步委托的执行状态(true / false),这里的时间是不定的,也就是说一定要等到异步委托执行完成之后,这个属性才会返回 true。
class Program
{
//定义一个委托类
private delegate UserInfo MyDelegate(UserInfo userInfo);
static void Main(string[] args)
{
ThreadDemoClass demoClass = new ThreadDemoClass();
List<UserInfo> userInfoList = new List<UserInfo>();
UserInfo userInfo = null;
UserInfo userInfoRes = null;
//创建一个委托并绑定方法
MyDelegate myDelegate = new MyDelegate(demoClass.Run);
for (int i = 0; i < 3; i++)
{
userInfo = new UserInfo();
userInfo.Name = "Brambling" + i.ToString();
userInfo.Age = 33 + i;
//传入参数并执行异步委托
IAsyncResult result = myDelegate.BeginInvoke(userInfo, null, null);
//异步操作是否完成
while (!result.IsCompleted)
{
Thread.Sleep(100);
Console.WriteLine("Main thread working...");
Console.WriteLine("Main thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString());
Console.WriteLine();
}
//结束异步委托,并获取返回值
userInfoRes = myDelegate.EndInvoke(result);
userInfoList.Add(userInfoRes);
}
foreach (UserInfo user in userInfoList)
{
Console.WriteLine("My name is " + user.Name);
Console.WriteLine("I'm " + user.Age + " years old this year");
Console.WriteLine("Thread ID is:" + user.ThreadId);
}
Console.ReadKey();
}
}
public class ThreadDemoClass
{
public UserInfo Run(UserInfo userInfo)
{
userInfo.ThreadId = Thread.CurrentThread.ManagedThreadId;
Console.WriteLine("Child thread working...");
Console.WriteLine("Child thread ID is:" + userInfo.ThreadId);
Console.WriteLine();
return userInfo;
}
}
public class UserInfo
{
public string Name { get; set; }
public int Age { get; set; }
public int ThreadId { get; set; }
}
执行结果:
3.2.2.2 利用WaitOne()方法、WaitAll() 方法和 WaitAny()完成异步委托方法
1、 WaitOne()方法,自定义一个等待的时间,如果在这个等待时间内异步委托没有执行完成,那么就会执行 while 里面的主线程的逻辑,反之就不会执行。
//传入参数并执行异步委托
IAsyncResult result = myDelegate.BeginInvoke(userInfo,null,null);
//阻止当前线程,直到 WaitHandle 收到信号,参数为指定等待的毫秒数
while (!result.AsyncWaitHandle.WaitOne(1000))
{
Console.WriteLine("Main thread working...");
Console.WriteLine("Main thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString());
Console.WriteLine();
}
2、WaitOne() 方法只能用于监视当前线程的对象,如果要监视多个对象可以使用 WaitAny(WaitHandle[], int)或 WaitAll (WaitHandle[] , int) 这两个方法。
//传入参数并执行异步委托
IAsyncResult result = myDelegate.BeginInvoke(userInfo,null,null);
IAsyncResult result1 = myDelegate.BeginInvoke(userInfo, null, null);
//定义要监视的对象,不能包含对同一对象的多个引用
WaitHandle[] waitHandles = new WaitHandle[] { result.AsyncWaitHandle, result1.AsyncWaitHandle };
while (!WaitHandle.WaitAll(waitHandles,1000))
{
Console.WriteLine("Main thread working...");
Console.WriteLine("Main thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString());
Console.WriteLine();
}
WaitAll() 方法和 WaitAny() 方法都可以监视多个对象,不同的是 WaitAll() 方法需要等待所有的监视对象都收到信号之后才会返回 true,否则返回 false。而 WaitAny() 则是当有一个监视对象收到信号之后就会返回一个 int 值,这个 int 值代表的是当前收到信号的监视对象的索引。注意:在定义监视对象的时候,不能包含对同一个对象的多个引用,我这里是定义的两个示例,所以是不同的对象。
3.2.2.3 利用“回调函数”完成异步委托方法
以上的异步委托方法可以看出,虽然使用的是异步的方式调用的方法,但是依旧需要等待异步的方法返回执行的结果,尽管我们可以不阻塞主线程,但是还是觉得不太方便,所以也就有了异步委托的回调函数。
1、回调函数无参数
class Program
{
//定义一个委托类
private delegate UserInfo MyDelegate(UserInfo userInfo);
static void Main(string[] args)
{
ThreadDemoClass demoClass = new ThreadDemoClass();
UserInfo userInfo = null;
//创建一个委托并绑定方法
MyDelegate myDelegate = new MyDelegate(demoClass.Run);
//创建一个回调函数的委托
AsyncCallback asyncCallback = new AsyncCallback(Complete);
for (int i = 0; i < 3; i++)
{
userInfo = new UserInfo();
userInfo.Name = "Brambling" + i.ToString();
userInfo.Age = 33 + i;
//传入参数并执行异步委托,并设置回调函数
IAsyncResult result = myDelegate.BeginInvoke(userInfo, asyncCallback, null);
}
Console.WriteLine("Main thread working...");
Console.WriteLine("Main thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString());
Console.WriteLine();
Console.ReadKey();
}
public static void Complete(IAsyncResult result)
{
UserInfo userInfoRes = null;
AsyncResult asyncResult = (AsyncResult)result;
//获取在其上调用异步调用的委托对象
MyDelegate myDelegate = (MyDelegate)asyncResult.AsyncDelegate;
//结束在其上调用的异步委托,并获取返回值
userInfoRes = myDelegate.EndInvoke(result);
Console.WriteLine("My name is " + userInfoRes.Name);
Console.WriteLine("I'm " + userInfoRes.Age + " years old this year");
Console.WriteLine("Thread ID is:" + userInfoRes.ThreadId);
}
}
public class ThreadDemoClass
{
public UserInfo Run(UserInfo userInfo)
{
userInfo.ThreadId = Thread.CurrentThread.ManagedThreadId;
Console.WriteLine("Child thread working...");
Console.WriteLine("Child thread ID is:" + userInfo.ThreadId);
Console.WriteLine();
return userInfo;
}
}
执行结果:
从上面可以看到主线程再执行了异步委托之后继续执行了下去,然后在回调函数里输出了信息,也就是说在调用了异步委托之后就不管了,把之后的结束委托和获取委托的返回值放到了回调函数中,因为回调函数是没有返回值的,但是回调函数可以有一个参数。
2、回调函数有参数
BeginInvoke() 方法的最后两个参数,它的倒数第二个参数就是一个回调函数的委托,最后一个参数可以设置传入回调函数的参数。回调函数的参数是 object 类型,可以string 类型,也可以是自定义类型的参数,本示例以string为例。
class Program
{
//定义一个委托类
private delegate UserInfo MyDelegate(UserInfo userInfo);
static List<UserInfo> userInfoList = new List<UserInfo>();
static void Main(string[] args)
{
ThreadDemoClass demoClass = new ThreadDemoClass();
UserInfo userInfo = null;
//创建一个委托并绑定方法
MyDelegate myDelegate = new MyDelegate(demoClass.Run);
//创建一个回调函数的委托
AsyncCallback asyncCallback = new AsyncCallback(Complete);
//回调函数的参数
string str = "I'm the parameter of the callback function!";
for (int i = 0; i < 3; i++)
{
userInfo = new UserInfo();
userInfo.Name = "Brambling" + i.ToString();
userInfo.Age = 33 + i;
//传入参数并执行异步委托,并设置回调函数
IAsyncResult result = myDelegate.BeginInvoke(userInfo, asyncCallback, str);
}
Console.WriteLine("Main thread working...");
Console.WriteLine("Main thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString());
Console.WriteLine();
Console.ReadKey();
}
public static void Complete(IAsyncResult result)
{
UserInfo userInfoRes = null;
AsyncResult asyncResult = (AsyncResult)result;
//获取在其上调用异步调用的委托对象
MyDelegate myDelegate = (MyDelegate)asyncResult.AsyncDelegate;
//结束在其上调用的异步委托,并获取返回值
userInfoRes = myDelegate.EndInvoke(result);
Console.WriteLine("My name is " + userInfoRes.Name);
Console.WriteLine("I'm " + userInfoRes.Age + " years old this year");
Console.WriteLine("Thread ID is:" + userInfoRes.ThreadId);
//获取回调函数的参数
string str = result.AsyncState as string;
Console.WriteLine(str);
}
}
public class ThreadDemoClass
{
public UserInfo Run(UserInfo userInfo)
{
userInfo.ThreadId = Thread.CurrentThread.ManagedThreadId;
Console.WriteLine("Child thread working...");
Console.WriteLine("Child thread ID is:" + userInfo.ThreadId);
Console.WriteLine();
return userInfo;
}
}
执行结果:
3.3 I/O线程
I/O 线程是.NET专为访问外部资源所设置的一种线程,因为访问外部资源常常要受到外界因素的影响,为了防止让主线程受影响而长期处于阻塞状态,.NET为多个I/O操作都建立起了异步方法,例如:FileStream、TCP/IP、WebRequest、WebService等等,而且每个异步方法的使用方式都非常类似,都是以BeginXXX为开始,以EndXXX结束。
参考文章:
C#综合揭秘——细说多线程:http://www.cnblogs.com/leslies2/archive/2012/02/08/2320914.html#t6
C#多线程和线程池:https://www.cnblogs.com/mq0036/p/6984508.html
今天的文章C# 多线程编程——理解多线程(一)分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/8799.html