0%

真会C#?——异步编程1

线程Thread

  • 线程是一个可执行路径,它可以独立于其他线程执行,每个线程都在操作系统的进程内执行,而操作系统提供了程序运行的独立环境
  • 在C#中线程的创建方式是var thread = new Thread(WriteY);,他必须传入一个执行任务,这个执行任务可以是一个函数(委托),然后可以设置它的名字,调用Start()来执行线程中的任务,在这里会看到X和Y交替输出,这是thread线程和主线程并发执行的效果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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");
}
}
}
}
  • 线程的一些属性
    1. IsAlive:线程一旦开始执行,IsAlive就是true,线程结束就为false,结束的条件是传入的委托结束了执行
    2. Name:线程的名称,只能设置一次,一般用于调试。
    3. CurrentThread:返回当前执行的线程
    4. 注意:线程一旦结束,就无法再重启

Join和Sleep

  • Join:调用Join方法,就可以等待另一个线程结束,即thread2调用了Join方法后,其他的线程就会等待thread2执行结束后才能执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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的超时时间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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);
}
}
  • 注意:
    1. Thread.Sleep(0)这样调用会导致线程立即放弃当前的时间片,自动将cpu移交给其他线程。这个功能类似于Thread.Yield(),但是它只会把执行交给同一处理器上的其他线程。
    2. 当等待Sleep或Join的时候,线程处于阻塞的状态。
    3. Sleep(0)Yield()在高级性能调试中是一个很好的诊断工具,有助于发现线程安全问题,但在生产代码中不能随意地使用Yield()函数。

阻塞

  • 如果线程的执行由于某种原因导致暂停,则就认为该线程被阻塞了。被阻塞的线程会立即将其处理器的时间片生成给其他线程,从此就不再消耗处理器的时间,直到满足其阻塞条件为止。
  • 可以使用ThreadState属性来判断线程是否处于被阻塞状态,ThreadState是一个枚举,可以用来获取线程的状态。它可以通过按位的形式合并状态的选项。

Snipaste_2020-09-17_11-43-11

1
2
3
4
5
6
7
8
9
10
11
class Program
{
static void Main(string[] args)
{
//按位进行或操作
//比较有用的有四个状态:Unstarted、Running、WaitSleepJoin和Stopped
var state = ThreadState.Unstarted | ThreadState.Stopped | ThreadState.WaitSleepJoin;
Console.WriteLine($"{Convert.ToString((int)state, 2)}");

}
}

解除阻塞Unblocking

  • 当遇到下列四种情况的时候,就会解除阻塞:
  1. 阻塞条件被满足
  2. 操作超时(需要设置超时条件)
  3. 通过Thread.Interrupt()进行打断
  4. 通过Thread.Abort()进行中止

上下文切换

  • 当线程阻塞时或接触阻塞时,操作系统将执行上下文切换,这回产生少量开销。

阻塞vs忙等待

  • 阻塞和忙等待也被称为IO-boundCpu-bound
  • IO-bound操作的工作方式有两种:
    1. 在当前线程上同步等待
    2. 异步的操作
1
2
//例如
Console.ReadLine(), Thread.Sleep(), Thread.Join()...
  • 而Cpu-bound就类似于死循环
1
2
3
4
5
//例如
while(true);
//忙等待+阻塞
while(true)
Thread.Sleep(100);

线程安全

本地状态和共享状态

  • Local本地独立:CLR为每个线程分配自己的内存栈,以便使本地变量保持独立。
  • Shared共享:
    1. 如果多个线程都引用到同一个对象实例,那么它们就共享了数据。
    2. 被lambda表达式或匿名委托所捕获的本地变量,会被编译器转化为字段,所以也会被共享。
    3. 静态字段也会在线程间共享。
  • 下面的_done字段就是共享变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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()结果就会不一样
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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来解决。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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的参数,比如:
1
2
3
4
5
6
7
8
9
10
class Program
{
static void Main(string[] args)
{
new Thread(() =>
{
Console.WriteLine("Hello World");
}).Start();
}
}
  • 可以使用Thread的Start方法来传递任务的参数,因为传参委托的类型是ThreadStart,所以参数必须是object类型。
1
2
3
4
5
6
7
8
9
10
11
12
class Program
{
static void Main(string[] args)
{
new Thread(G).Start("Hello World");
}

static void G(object str)
{
Console.WriteLine((string)str);
}
}
  • 使用lambda表达式可以很简单地给Thread传递参数。但是线程开始后,可能会不小心修改了被捕获地变量,比如下面的例子,每一次运行都有不同的输出,而且输出有可能会有相同的值。
1
2
3
4
for (int i = 0; i < 10; i++)
{
new Thread(() => Console.Write(i)).Start();
}
  • 可以使用局部变量来解决这个问题
1
2
3
4
5
for (int i = 0; i < 10; i++)
{
int temp = i;
new Thread(() => Console.Write(temp)).Start();
}

线程的优先级

  • 线程的优先级(Priority属性)决定了相对于操作系统中其他活跃线程所占的执行时间。
  • 如果想让某线程的优先级比其他进程中的线程高,那就必须提升进程的优先级,这里可以使用Process类
1
2
3
4
using (Process p = Process.GetCurrentProcess())
{
p.PriorityClass = ProcessPriorityClass.High;
}
  • 但是,提高线程或进程的优先级可能会导致其他线程或线程处于饥饿状态,不能随便设置。

信号

  • 有时,你需要让某个线程一直处于等待状态,直到接收到其他线程发来的通知,才解除等待状态,这就叫做signaling。最简单的信号结构是MaunalResetEvent类对象。
  • 调用MaunalResetEvent类对象的WaitOne方法就会阻塞当前的线程,直到另一个线程通过调用Set方法来打开信号(发送信号)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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);
//向thread打开信号
signal.Set();
}
}
  • 信号打开后可以通过Reset方法来重新关闭信号。

同步上下文

  • Thread Marshaling:Marshaling的意思是假如要将一个平台上的数据发送给另一个平台,但是两个平台使用的数据格式不一致,这时候就需要把数据转化为可发送的数据格式,这就类似于json的序列化,而接收端就是Unmarshaling,也就类似于反序列化。而Thread Marshaling就是把一些数据的所有权从一个线程交给另外一个线程。
  • 在C#中同步上下文是用System.ComponentModel下的SynchronizationContext抽象类来实现的,可以通过实例化SynchronizationContext的子类,调用它的Post方法来实现同步上下文,这一般在富客户端应用比较常用(WPF、WinForm),它可以让主线程的一些事件的操作交给UI线程,这样UI线程可以做一下事件的调用,主线程也不会进入假死。

学习资料:B站杨旭

-------------本文结束感谢您的阅读-------------