【C# 线程】Thread类 以及使用案例
阅读原文时间:2023年07月09日阅读:2

System.Threading.Thread类

涉及到的类和枚举

Volatile 类
Interlocked 类
SpinLock 类
SpinWait类
Barrier 类
ThreadLocal
ApartmentState 枚举
ThreadPriority 枚举
ThreadStart 类
ThreadStartException
ThreadState 枚举

名称                                                说明
Thread(ParameterizedThreadStart)     初始化 Thread 类的新实例,指定允许对象在线程启动时传递给线程的委托。要执行的方法是有参的。

public delegate void ParameterizedThreadStart(object? obj)

Thread(ParameterizedThreadStart, Int32)     初始化 Thread 类的新实例,指定允许对象在线程启动时传递给线程的委托,并指定线程的最大堆栈大小
Thread(ThreadStart)     初始化 Thread 类的新实例。要执行的方法是无参的。
Thread(ThreadStart, Int32)     初始化 Thread 类的新实例,指定线程的最大堆栈大小。

属性名称     说明

ApartmentState 属性已过时。 未过时的替代 GetApartmentState 方法是检索单元状态的方法,以及 SetApartmentState 用于设置单元状态的方法。
CurrentContext     获取线程正在其中执行的当前上下文。
CurrentThread     获取当前正在运行的线程。
ExecutionContext     获取一个 ExecutionContext 对象,该对象包含有关当前线程的各种上下文的信息。
IsAlive     获取一个值,该值指示当前线程的执行状态。
IsBackground     获取或设置一个值,该值指示某个线程是否为后台线程。
IsThreadPoolThread     获取一个值,该值指示线程是否属于托管线程池。
ManagedThreadId     获取当前托管线程的唯一标识符。
Name     获取或设置线程的名称。
Priority     获取或设置一个值,该值指示线程的调度优先级。
ThreadState     获取一个值,该值包含当前线程的状态。

使用案例

Thread thread = new Thread(SleepAwait);
Thread thread2 = new Thread(SleepAwait2);

thread.Name = " thread";
thread.Start();

thread2.Name = " thread2";
thread2.Priority=ThreadPriority.BelowNormal;
thread2.Start(6000);

Console.WriteLine(thread2.Name);
Console.WriteLine("thread2.ManagedThreadId:{0}",thread2.ManagedThreadId);
Console.WriteLine("thread2.ThreadState:{0}", thread2.ThreadState);
Console.WriteLine("thread2.GetApartmentState:{0}", thread2.GetApartmentState());
Console.WriteLine("thread2.IsAlive:{0}", thread2.IsAlive);
Console.WriteLine("thread2.Priority:{0}", thread2.Priority);
Console.WriteLine("thread2.IsBackground:{0}", thread2.IsBackground);
Console.WriteLine("thread2.IsThreadPoolThread:{0}", thread2.IsThreadPoolThread);
Console.WriteLine("thread2.CurrentCulture:{0}", thread2.CurrentCulture);
Console.WriteLine("thread2.CurrentUICulture:{0}", thread2.CurrentUICulture);

void SleepAwait2(object? obj)
{
Console.WriteLine(Thread.CurrentThread.Name);
Console.WriteLine(Thread.CurrentThread.CurrentUICulture.ToString());
int time=4000;
if (obj != null)
{
time = (int)obj;
}

Thread.Sleep(time);  
Console.WriteLine($"I won to sleep {time} second ");  

}

static void SleepAwait()
{
Console.WriteLine(Thread.CurrentThread.Name);
Console.WriteLine(Thread.CurrentThread.CurrentUICulture.ToString());
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
Console.WriteLine(Thread.CurrentThread.ThreadState);
Console.WriteLine(Thread.CurrentThread.GetApartmentState());
Console.WriteLine(Thread.CurrentThread.IsAlive);
Console.WriteLine("I won to sleep 5000 second ");
Console.WriteLine(Thread.CurrentThread.Priority);

Thread.Sleep(1000);

}

=======================cpu控制============================================

Thread.Yeild()方法
让有需要的人先用
Yield 的中文翻译为 “屈服,让步”,这里意思是主动放弃当前线程的时间片,并让操作系统调度其它就绪态的线程使用一个时间片。但是如果调用 Yield,只是把当前线程放入到就绪队列中,而不是阻塞队列。如果没有找到其它就绪态的线程,则当前线程继续运行。Yield可以让低于当前优先级的线程得以运行,调用者可以通过返回值判断是否成功调度了其它线程。注意,Yield只能调度运行在当前CPU上的线程,如果有多个cpu 无法让步给其他cpu上的线程。
如果操作系统转而执行另一个线程,则为 true;否则为 false。此方法等效于使用平台调用来调用本机 Win32 SwitchToThread 函数。 应调用 Yield 方法,而不是使用平台调用

案例:

while (true)
{
// 主动让出CPU,如果有其他就绪线程就执行其他线程,如果没有则继续当前线程
var result = Thread.Yield();

            // Do Some Work  
            Console.WriteLine("Thread 1 running, yield result {0}", result);  
        }

Thread.Sleep(0):让级别高的线程先执行(让领导先用),如果等待线程中,没有优先级更高的,该线程就继续执行。
Thread.Sleep(1),进入就绪队列。该方法使用 1 作为参数,这会强制当前线程放弃剩下的时间片,并休息 1 毫秒(因为不是实时操作系统,时间无法保证精确,一般可能会滞后几毫秒或一个时间片)。但因此的好处是,所有其它就绪状态的线程都有机会竞争时间片,而不用在乎优先级。
Sleep(N)休息一伙

让领导先走
在线程没退出之前,线程有三个状态,就绪态,运行态,等待态。sleep(n)之所以在n秒内不会参与CPU竞争,是因为,当线程调用sleep(n)的时候,线程是由运行态转入等待态,线程被放入等待队列中,等待定时器n秒后的中断事件,当到达n秒计时后,线程才重新由等待态转入就绪态,被放入就绪队列中,等待队列中的线程是不参与cpu竞争的,只有就绪队列中的线程才会参与cpu竞争,所谓的cpu调度,就是根据一定的算法(优先级,FIFO等。。。),从就绪队列中选择一个线程来分配cpu时间。
而sleep(0)之所以马上回去参与cpu竞争,是因为调用sleep(0)后,因为0的原因,线程直接回到就绪队列,而非进入等待队列,只要进入就绪队列,那么它就参与cpu竞争。

SpinWait():

SpinWait本质上是将处理器放入一个非常紧凑的循环中,循环计数由迭代参数指定。因此,等待的时间长短取决于处理器的速度。说白了SpinWait就是一个for循环,我们只要输入一个数字就行。总的循环的时间由处理器的处理速度决定。****
SpinWait对于普通应用程序通常不是很有用。在大多数情况下,你应该使用。net框架提供的同步类;例如:调用Monitor
Thread.SpinWait 案例

using System.Diagnostics;

class Program
{

static SpinLock sl = new();

static void Main(string\[\] args)  
{

    Stopwatch stopwatch = new ();  
    SpinWait sw = new();

    for (int i = 0; i < 20; i++)  
    {  
        int ss = 1 << i;//1向左移动i位  
        stopwatch.Reset();  
        stopwatch.Start();

        Thread.SpinWait(ss);  
        stopwatch.Stop();

        Console.WriteLine(stopwatch.ElapsedTicks+sw.NextSpinWillYield.ToString()+"count:"+ ss);  
    }

}  

}

BeginThreadAffinity:目前是空方法,以前是该方法调用IHostTaskManager 接口通知宿主托管代码正在输入一个时间段,在此期间,不能将当前任务移动到另一个线程。。主要用到的是线程的亲和调度算法Affinity
EndThreadAffinity:线程不再需要使用物理操作系统线程运行时,可调用Thread的EndThreadAffinity方法来通知CLR。
BeginCriticalRegion:线程中访问临界资源的那段代码。通知宿主执行将要进入一个代码区域,在该代码区域内线程中止或未处理的异常的影响可能会危害应用程序域中的其他任务。
EndCriticalRegion:通知宿主执行将要进入一个代码区域,在该代码区域内线程中止或未处理的异常仅影响当前任务。

==================================数据曹的用法==============================================

AllocateDataSlot    申请匿名的数据曹
AllocateNamedDataSlot    申请命名的数据曹
GetNamedDataSlot(name)    释放name数据曹
FreeNamedDataSlot(name)    释放name数据曹,
GetApartmentState:设置线程状态 MTA或者STA
SetApartmentState:设置线程状态 MTA或者STA
TrySetApartmentState
GetData    获取据曹的值
SetData    给数据曹赋值

这些函数用法:

哪个线程设定的数据曹,就那个线程使用(谁设定谁使用),其他线程无权读取和设定。

using System;
using System.Threading;
class Programe
{ static void Main()
{

    Foo foo = new Foo();  
    Thread thread = new Thread(new ThreadStart(foo.A));  
    thread.SetApartmentState(ApartmentState.STA);  
    //设置数据曹  
    LocalDataStoreSlot dataslot = Thread.AllocateNamedDataSlot("maindata");  
    Thread.SetData(dataslot, "mainThreadDataslot");

   //获取数据曹  
    LocalDataStoreSlot dataslot1 = Thread.GetNamedDataSlot("maindata");  
    Console.WriteLine("主线程数据曹{0}:",(string)Thread.GetData(dataslot1) );

    thread.Start();  
    Console.Read();  
}  

}
class Foo
{
public void A()
{
LocalDataStoreSlot dataslot = Thread.GetNamedDataSlot("maindata");
if (dataslot is null) return;
string see = (string)Thread.GetData(dataslot) ;
Console.WriteLine("Thread{0}读取MainThread的数据曹:{1}", Environment.CurrentManagedThreadId, see);
Console.WriteLine(Thread.CurrentThread.GetApartmentState());
}

}

===========================内存屏障=====================================

MemoryBarrier:内存屏障  相当于分隔符,内部调用了Interlocked.MemoryBarrier() 作用主要两个:
      1、阻隔代码流动:编译器或clr cpu不能将Thread.MemoryBarrier() 前面代码,移动到他后面,也不允许它后面的代码 移到它前面。它就像一堵墙隔离了代码优化带来的代码移动。
      2、缓冲和寄存器数据的新老划段:Thread.MemoryBarrier() 后面的代码用的的变量值,只能从内存中重新提取,不允许使用Thread.MemoryBarrier() 前面代码放在缓存或寄存器中的值"

本文简单的介绍一下这两个概念,假设下面的代码:

using System;
class Foo
{
int _answer;
bool _complete;

void A()  
{  
    \_answer = 123;  
    \_complete = true;  
}

void B()  
{  
    if (\_complete) Console.WriteLine(\_answer);  
}  

}

如果方法A和方法B同时在两个不同线程中运行,控制台可能输出0吗?答案是可能的,有以下两个原因:

  • 编译器,CLR或者CPU可能会更改指令的顺序来提高性能
  • 编译器,CLR或者CPU可能会通过缓存来优化变量,这种情况下对其他线程是不可见的。

最简单的方式就是通过MemoryBarrier来保护变量,来防止任何形式的更改指令顺序或者缓存。调用Thread.MemoryBarrier会生成一个内存栅栏,我们可以通过以下的方式解决上面的问题:

using System;
using System.Threading;
class Foo
{
int _answer;
bool _complete;

void A()  
{  
    \_answer = 123;  
    Thread.MemoryBarrier();    // Barrier 1  
    \_complete = true;  
    Thread.MemoryBarrier();    // Barrier 2  
}

void B()  
{  
    Thread.MemoryBarrier();    // Barrier 3  
    if (\_complete)  
    {  
        Thread.MemoryBarrier();       // Barrier 4  
        Console.WriteLine(\_answer);  
    }  
}  

}

上面的例子中,barrier1和barrier2用来保证指令顺序不会改变和不缓存数据,直接将数据写入内存,barrier3和barrier4用来 直接读取内存数,保证数值式最新的。

VolatileRead:原子锁,立即将字段的数值写入内存中,内部调用了Volatile.Read(), 具有Load Memory Barrier(读屏障),会刷新一次store bufferes,保证获取到最新的值。
             Read方法会保证函数中,所有在Read方法执行之后的数据读写操作,一定实在Read方法执行后才进行
        该方法作用是,当线程在共享区(临界区)传递信息时,通过此方法来原子性的读取第一个值。
VolatileWrite:原子锁,立即读取内存中字段的值,不从寄存器和缓存中读取,内部调用了Volatile.Write(),  写入后会刷新一次store bufferes,将新的值即刻写入内存中。

      所有在Write方法之前执行的数据读写操作都在Write方法写入之前就执行了。该方法作用是,当线程在共享区(临界区)传递信息时,通过此方法来原子性的写入最后一个值。

这个2个Thread静态方法用法和System.Threading.Volatile,它提供了两个静态方法Write和Read。一样

这些函数的用法:

哪个线程设定的Volatile变量,就哪个线程使用(谁设定谁使用),其他线程无权读取,但是可以从新设定该变量名称。

private void VolatileRW()
{
m_stopWork = 0;

        Thread t = new Thread(new ParameterizedThreadStart(Worker));  
        t.Start(0);  
        Thread.Sleep(5000);  
        Thread.VolatileWrite(ref m\_stopWork, 1);//设定m\_stopWrok为1,这里和顺序有关,这里应该用VolatileWrite,不要妄图去猜想编译器的优化顺序  
        LogHelper.WriteInfo("Main thread waiting for worker to stop");  
        t.Join();

    }  
    private void Worker(object o)  
    {  
        int x = 0;  
        //while (m\_stopWork == 0) //如果这样判定,m\_stopWork被缓存后可能不会再去读取内存的值(循序变量可能会被编译器优化),所以可能会是个死循环  
        while (Thread.VolatileRead(ref m\_stopWork) == 0)//用VolatileRead每次就会去读新的值  
        {  
            x++;  
        }  
        LogHelper.WriteInfo(string.Format("worker stoped:x={0}", x));  
    }

Interrupt()方法:

其他线程中对目标线程调用interrupt()方法,目标线程将自身的中断状态位为true ,线程会不时地检测这个中断标示位,以判断线程是否应该被中断。并且在抛出异常后立即将线程的中断标示位清除,即重新设置为false。抛出异常是为了线程从阻塞状态醒过来,并在结束线程前让程序员有足够的时间来处理中断请求。如果该异常被捕获,线程就不会终止。如果线程未阻塞就可能在不中断的情况下运行完成线程。

Thread thread = new Thread(() =>
{
for (int i = 0; i < 10; i++)
{
object o = new();
try
{

            Thread.Sleep(500);//如果该线程内没有阻塞语句例如 Thread.Sleep(500);那么 thread.Interrupt();将不影响线程执行  
        Console.WriteLine(Thread.CurrentThread.ThreadState);

    }      ///如果捕获 Thread.Sleep(1000); 那么其他线程运行thread.Interrupt();将起不到终止线程的效果。所以不要什么异常都捕获  
           ///将会设置该线程的中断状态位为true ,线程会不时地检测这个中断标示位,以判断线程是否应该被中断。并且在抛出异常后立即将线程的中断标示位清除,  

///即重新设置为false。抛出异常是为了线程从阻塞状态醒过来,并在结束线程前让程序员有足够的时间来处理中断请求。

    catch (ThreadInterruptedException ex)  
    {

        Console.WriteLine($"第{i}次中断{Thread.CurrentThread.ThreadState}");  
    }

}

});
Console.WriteLine(thread.ThreadState);
thread.Start();
Console.WriteLine(thread.ThreadState);
thread.Interrupt();

Task.Run(() =>
{

Thread.Sleep(1000); thread.Interrupt();

});
thread.Join();
Console.ReadKey();

===============================================================================================

GetDomain    获取Domain
GetDomainID    获取DomainID
GetCurrentProcessorId    获取当前进程ID

==========================线程状态控制==================================================

Start:开始Start()、Start(object)object是方法参数
Join:阻塞直到某个线程终止时为止。
ResetAbort:取消为当前线程请求的 Abort。
Sleep:"将当前线程阻塞指定的毫秒数,Sleep()使得线程立即停止执行,线程将不再得到CPU时间。Thread.Sleep(0)是比较特殊的 表示让让出cpu剩余的时间,给线程优先级更高的线程用,如果没有更高优先级的线程,它自己继续使用。
当一个任务或者线程调用Thread.Sleep方法时,底层线程会让出当前处理器时间片的剩余部分,这是一个大开销的操作。因此,在大部分情况下, 不要在循环内调用Thread.Sleep方法等待特定的条件满足 。循环内用SpinWait"

============================其他方法==========================================
    
DisableComObjectEagerCleanup    
Finalize:确保垃圾回收器回收 Thread 对象时释放资源并执行其他清理操作。
GetHashCode    

====================================过时的方法=============================
Suspend:过时 因为容易造成死锁 挂起 ,调用Suspend()方法 停止线程运行,不是及时的,它要求公共语言运行时必须到达一个安全点,线程将不再得到CPU时间。
  但是可以调用Suspend()方法使得另外一个线程暂停执行。对已经挂起的线程调用Thread.Resume()方法会使其继续执行。不管使用多少次Suspend()方法来阻塞一个线程,只需一次调用Resume()方法就可以使得线程继续执行。
  尽可能的不要用Suspend()方法来挂起阻塞线程,因为这样很容易造成死锁。假设你挂起了一个线程,而这个线程的资源是其他线程所需要的,会发生什么后果。
  因此,我们尽可能的给重要性不同的线程以不同的优先级,用Thread.Priority()方法来代替使用Thread.Suspend()方法。"
  Resume:过时 恢复挂起
Abort:过时。.NET 5(包括 .NET Core)及更高版本不支持 Thread.Abort 方法,ancellationToken 已成为一个安全且被广泛接受的 Thread.Abort 替代者。如果线程已经在终止 。Thread.Abort()方法使得系统悄悄的销毁了线程而且不通知用户。一旦实施Thread.Abort()操作,该线程不能被重新启动。调用了这个方法并不是意味着线程立即销毁,因此为了确定线程是否被销毁,我们可以调用Thread.Join()来确定其销毁。对于A和B两个线程,A线程可以正确的使用Thread.Abort()方法作用于B线程,但是B线程却不能调用Thread.ResetAbort()来取消Thread.Abort()操作。

GetCompressedStack:过时
SetCompressedStack  :过时

ThreadState枚举

Running :0 线程已启动且尚未停止。
StopRequested :1 正在请求线程停止。 这仅用于内部。
SuspendRequested :2 正在请求线程挂起。
Background :4 线程正作为后台线程执行(相对于前台线程而言)。 此状态可以通过设置 IsBackground 属性来控制。
Unstarted :8 尚未对线程调用 Start() 方法。
Stopped :16 线程已停止。
WaitSleepJoin
:32     线程已被阻止。 这可能是调用 Sleep(Int32) 或 Join()、请求锁定(例如通过调用 Enter(Object) 或
Wait(Object, Int32, Boolean))或在线程同步对象上(例如ManualResetEvent)等待的结果。
Suspended :64 线程已挂起。
AbortRequested :128 已对线程调用了 Abort(Object) 方法,但线程尚未收到试图终止它的挂起的 ThreadAbortException。
Aborted :256 线程状态包括 AbortRequested 并且该线程现在已死,但其状态尚未更改为 Stopped。

ThreadPriority 枚举

ThreadPriority 枚举
Lowest :0  可以将 Thread 安排在具有任何其他优先级的线程之后。
Normal :2 可以将 Thread 安排在具有 AboveNormal 优先级的线程之后,在具有 BelowNormal 优先级的线程之前。 默认情况下,线程具有 Normal 优先级。
BelowNormal :1 可以将 Thread 安排在具有 Normal 优先级的线程之后,在具有 Lowest 优先级的线程之前。
AboveNormal :3 可以将 Thread 安排在具有 Highest 优先级的线程之后,在具有 Normal 优先级的线程之前。
Highest:4     可以将 Thread 安排在具有任何其他优先级的线程之前。

ApartmentState枚举

MTA :1 Thread 将创建并进入一个多线程单元。
STA:0 Thread 将创建并进入一个单线程单元。
Unknown :2 尚未设置 ApartmentState 属性。

Thread使用ThreadState属性指示线程状态,它 是带Flags特性的枚举类型对象。

1.判断线程是否处于取消状态

A.错误的判断

(MyThread.ThreadState == ThreadState.AbortRequested)

B.正确的判断

(MyThread.ThreadState & ThreadState.AbortRequested) != 0

2.判断线程是否处于运行状态
   

ThreadState.Running本身等于0,不能用&运算,所以判断可用以下方法:

(MyThread.ThreadState == ThreadState.Running) 

什么是临界资源和临界区

1.临界资源
  临界资源是一次仅允许一个进程使用的共享资源。各进程采取互斥的方式,实现共享的资源称作临界资源。属于临界资源的硬件有,打印机,磁带机等;软件有消息队列,变量,数组,缓冲区等。诸进程间采取互斥方式,实现对这种资源的共享。

2.临界区:
  每个进程中访问临界资源的那段代码称为临界区(criticalsection),每次只允许一个进程进入临界区,进入后,不允许其他进程进入。不论是硬件临界资源还是软件临界资源,多个进程必须互斥的对它进行访问。多个进程涉及到同一个临界资源的的临界区称为相关临界区。使用临界区时,一般不允许其运行时间过长,只要运行在临界区的线程还没有离开,其他所有进入此临界区的线程都会被挂起而进入等待状态,并在一定程度上影响程序的运行性能。

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器