携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第12天,点击查看活动详情 >>
线程Thread
- 线程是一个可执行路径,它可以独立于其他线程执行,每个线程都在操作系统的进程内执行,而操作系统提供了程序运行的独立环境
- 在C#中线程的创建方式是var thread = new Thread(WriteY);,他必须传入一个执行任务,这个执行任务可以是一个函数(委托),然后可以设置它的名字,调用Start()来执行线程中的任务,在这里会看到X和Y交替输出,这是thread线程和主线程并发执行的效果。
namespace ThreadDemo
{
class ThreadDemo
{
public static void Main(string[] args)
{
var thread = new Thread(WriteY);
thread.Name = "Y Thread";
thread.Start();
for (int i = 0; i < 100; i++)
{
Console.Write("X");
}
}
static void WriteY()
{
for (int i = 0; i < 100; i++)
{
Console.Write("Y");
}
}
}
}
-
- IsAlive:线程一旦开始执行,IsAlive就是true,线程结束就为false,结束的条件是传入的委托结束了执行
- Name:线程的名称,只能设置一次,一般用于调试。
- CurrentThread:返回当前执行的线程
- 注意:线程一旦结束,就无法再重启
Join和Sleep
- Join:调用Join方法,就可以等待另一个线程结束,即thread2调用了Join方法后,其他的线程就会等待thread2执行结束后才能执行
class Program
{
private static Thread thread1, thread2
static void Main(string[] args)
{
thread1 = new Thread(ThreadProc)
thread1.Name = "Thread1"
thread1.Start()
thread2 = new Thread(ThreadProc)
thread2.Name = "Thread2"
thread2.Start()
}
private static void ThreadProc()
{
Console.WriteLine("\nCurrent thread: {0}", Thread.CurrentThread.Name)
if (Thread.CurrentThread.Name == "Thread1" && thread2.ThreadState != ThreadState.Unstarted)
//等待thread2执行结束后thread1才往下执行
thread2.Join()
Thread.Sleep(4000)
Console.WriteLine("\nCurrent thread: {0}", Thread.CurrentThread.Name)
Console.WriteLine("Thread1: {0}", thread1.ThreadState)
Console.WriteLine("Thread2: {0}\n", thread2.ThreadState)
}
}
- Sleep:会暂停当前的线程,但线程不会被抢占
- 添加超时:使用TimeSpan对象来设置Join的超时时间或设置Sleep的超时时间
public class JoinTimeSpan
{
static TimeSpan waitTime = new TimeSpan(0, 0, 1);
public static void Run()
{
var thread = new Thread(Work);
thread.Start();
if (thread.Join(waitTime))
{
Console.WriteLine("New thread terminated");
}
else
{
Console.WriteLine("Join timed out.");
}
}
static void Work()
{
Thread.Sleep(waitTime);
}
}
-
- Thread.Sleep(0)这样调用会导致线程立即放弃当前的时间片,自动将cpu移交给其他线程。这个功能类似于Thread.Yield(),但是它只会把执行交给同一处理器上的其他线程。
- 当等待Sleep或Join的时候,线程处于阻塞的状态。
- Sleep(0)或Yield()在高级性能调试中是一个很好的诊断工具,有助于发现线程安全问题,但在生产代码中不能随意地使用Yield()函数。
阻塞
- 如果线程的执行由于某种原因导致暂停,则就认为该线程被阻塞了。被阻塞的线程会立即将其处理器的时间片生成给其他线程,从此就不再消耗处理器的时间,直到满足其阻塞条件为止。
- 可以使用ThreadState属性来判断线程是否处于被阻塞状态,ThreadState是一个枚举,可以用来获取线程的状态。它可以通过按位的形式合并状态的选项。

class Program
{
static void Main(string[] args)
{
var state = ThreadState.Unstarted | ThreadState.Stopped | ThreadState.WaitSleepJoin;
Console.WriteLine($"{Convert.ToString((int)state, 2)}");
}
}
解除阻塞Unblocking
- 阻塞条件被满足
- 操作超时(需要设置超时条件)
- 通过Thread.Interrupt()进行打断
- 通过Thread.Abort()进行中止
上下文切换
- 当线程阻塞时或接触阻塞时,操作系统将执行上下文切换,这回产生少量开销。
阻塞vs忙等待
- 阻塞和忙等待也被称为IO-bound和Cpu-bound
- IO-bound操作的工作方式有两种:
-
- 在当前线程上同步等待
- 异步的操作
| 1 2 | //例如 Console.ReadLine(), Thread.Sleep(), Thread.Join()... |
|---|
| 1 2 3 4 5 | //例如 while(true); //忙等待+阻塞 while(true) Thread.Sleep(100); |
|---|
线程安全
本地状态和共享状态
- Local本地独立:CLR为每个线程分配自己的内存栈,以便使本地变量保持独立。
- Shared共享:
-
- 如果多个线程都引用到同一个对象实例,那么它们就共享了数据。
- 被lambda表达式或匿名委托所捕获的本地变量,会被编译器转化为字段,所以也会被共享。
- 静态字段也会在线程间共享。
class Program
{
private static bool _done;
static void Main(string[] args)
{
new Thread(Go).Start();
Go();
}
static void Go()
{
if (!_done)
{
_done = true;
Console.WriteLine("Done");
}
}
}
- 字段共享就会引出线程安全问题,因为上述例子的输出有可能不是固定的,如果在_done = true;前加上Thread.Sleep()结果就会不一样
class Program
{
private static bool _done;
static void Main(string[] args)
{
new Thread(Go).Start();
Go();
}
static void Go()
{
if (!_done)
{
Thread.Sleep(100);
_done = true;
Console.WriteLine("Done");
}
}
}
lock
- 在现实中应该尽量避免这种共享变量的使用,这种线程安全也可以用互斥锁lock来解决。
class Program
{
static readonly object _locker = new object();
private static bool _done;
static void Main(string[] args)
{
new Thread(Go).Start();
Go();
}
static void Go()
{
lock (_locker)
{
if (!_done)
{
Thread.Sleep(100);
_done = true;
Console.WriteLine("Done");
}
}
}
}
- 在这里使用lock将共享代码块包括,这个lock代码块就称为临界区,每次只允许一个线程进入,在多线程上下文中,以这种方式避免不确定性的代码就叫做线程安全
- 但是使用lock来解决线程安全也存在很大的问题,第一很容易忘记对字段加锁,第二会引起死锁。
线程传参
- 可以直接使用lambda表达式做为Thread的参数,比如:
class Program
{
static void Main(string[] args)
{
new Thread(() =>
{
Console.WriteLine("Hello World");
}).Start();
}
}
- 可以使用Thread的Start方法来传递任务的参数,因为传参委托的类型是ThreadStart,所以参数必须是object类型。
class Program
{
static void Main(string[] args)
{
new Thread(G).Start("Hello World");
}
static void G(object str)
{
Console.WriteLine((string)str);
}
}
- 使用lambda表达式可以很简单地给Thread传递参数。但是线程开始后,可能会不小心修改了被捕获地变量,比如下面的例子,每一次运行都有不同的输出,而且输出有可能会有相同的值。
for (int i = 0; i < 10; i++)
{
new Thread(() => Console.Write(i)).Start();
}
for (int i = 0
{
int temp = i
new Thread(() => Console.Write(temp)).Start()
}
线程的优先级
- 线程的优先级(Priority属性)决定了相对于操作系统中其他活跃线程所占的执行时间。
- 如果想让某线程的优先级比其他进程中的线程高,那就必须提升进程的优先级,这里可以使用Process类
using (Process p = Process.GetCurrentProcess())
{
p.PriorityClass = ProcessPriorityClass.High
}
- 但是,提高线程或进程的优先级可能会导致其他线程或线程处于饥饿状态,不能随便设置。
信号
- 有时,你需要让某个线程一直处于等待状态,直到接收到其他线程发来的通知,才解除等待状态,这就叫做signaling。最简单的信号结构是MaunalResetEvent类对象。
- 调用MaunalResetEvent类对象的WaitOne方法就会阻塞当前的线程,直到另一个线程通过调用Set方法来打开信号(发送信号)。
internal class Program
{
private static void Main(string[] args)
{
var signal = new ManualResetEvent(false);
var thread = new Thread(() =>
{
Console.WriteLine("Waiting for signal...");
signal.WaitOne();
signal.Dispose();
Console.WriteLine("Got signal!");
});
thread.Start();
Thread.Sleep(3000);
signal.Set();
}
}
同步上下文
- Thread Marshaling:Marshaling的意思是假如要将一个平台上的数据发送给另一个平台,但是两个平台使用的数据格式不一致,这时候就需要把数据转化为可发送的数据格式,这就类似于json的序列化,而接收端就是Unmarshaling,也就类似于反序列化。而Thread Marshaling就是把一些数据的所有权从一个线程交给另外一个线程。
- 在C#中同步上下文是用System.ComponentModel下的SynchronizationContext抽象类来实现的,可以通过实例化SynchronizationContext的子类,调用它的Post方法来实现同步上下文,这一般在富客户端应用比较常用(WPF、WinForm),它可以让主线程的一些事件的操作交给UI线程,这样UI线程可以做一下事件的调用,主线程也不会进入假死。
线程池(Thread Pool)
- 线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中
- 当开始一个线程的时候,将花费几百微秒来组织一个心得局部变量栈,而使用线程池可以节省这种组织开销,即通过预先创建一个可循环使用线程的池来减少这一开销。
- 线程池对于高效的并行编程和细粒度并发是必不可少的,它允许在不被线程启动的开销淹没的情况下运行短期操作(这种短期操作开销很小)。
使用线程池需要注意的地方
- 不可以设置线程池的Name
- 池线程都是后台线程
- 阻塞池线程可能使性能降级
- 可以自由地更改池线程的优先级,当线程释放回池时优先级将还原
- 可以使用Thread.CurrentThread.IsThreadPoolThread属性来判断是否执行在线程池上。