MultiThreading programming #1

Thread


What is Thread ?

  • 线程是一个可执行路径,每一个线程可以独立于其他线程执行
  • 每个线程在均进程(Process)内执行,在操作系统中,进行提供了程序运行的独立环境
  • 单线程应用,在进程的独立环境中只跑一个线程,该线程具有独占权
  • 多线程应用,单个进程中跑多个线程,多个线程共享当前的执行环境(尤其是内存)
    • 共享:多个线程共同占有某种资源,如一个线程在后台读取数据,另一个线程在数据到达后进行展示。

下面是c#中最简单的异步编程实例:

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
27
28
29
30
class Program
{
static void Main(string[] args)
{
Thread.CurrentThread.Name = "Main Thread...";
//开辟一个新线程
Thread thread = new Thread(WriteY);
thread.Name = "Y Thread...";
thread.Start();
Thread.Sleep(1);
//同时在主线程上做一些工作
System.Console.WriteLine(Thread.CurrentThread.Name);
for (int i = 0; i < 1000; i++)
{
System.Console.Write("x");
}


}

static void WriteY()
{
System.Console.WriteLine(Thread.CurrentThread.Name);
for (int i = 0; i < 1000; i++)
{
System.Console.Write("y");
}

}
}
  • 在单核计算机上,操作系统必须为每个线程分配时间片(Windows下通常为20ms)来模拟并发,从而在本例中,会输出重复的x块与y块
  • 在多核或多处理器的计算机上,使用c#创建的多线程可以真正意义上并行执行。然而,在本例中由于控制台程序处理并发请求机制的微妙性,仍然会得到重复的x块与y块
    first_example
  • c#中线程的一些属性:
    • 线程一旦开始执行,属性IsAlive就变为true,线程结束就变为false
    • 线程结束的条件:线程构造器中传入的委托结束了执行
    • 线程一旦结束,便无法重启
    • 每个线程都有一个Name属性,通常用于调试,Name只能设置一次,多次更改会抛出异常
    • 静态属性Thread.CurrentThread,指向当前执行的线程

Join and Sleep

  • 在线程A中调用另一个线程B实例的Join方法,线程A便会等待线程B执行结束后继续执行。
  • 调用Jion方法时,可在参数中设置一个超时(毫秒/TimeSpan)
    • 使用有超时的重载方法时返回值bool类型,true:线程结束;false:超时
  • Thread.Sleep方法会暂停当前的线程,并等待一段时间
    • Thread.Sleep(0)会导致县城立即放弃本身当前时间片,自动将cpu转交给其他线程
    • Thread.YieldThread.Sleep做同样的事情,不同的是Thread.Yield只会把执行交给同一处理器的其他线程
    • 当等待SleepJoin时,线程处于阻塞状态
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
27
28
29
30
31
32
33
34
class Program
{
static Thread thread1;
static Thread thread2;
static void Main(string[] args)
{
thread1 = new Thread(ThreadProc);
thread1.Name = "t1";
thread1.Start();

thread2 = new Thread(ThreadProc);
thread2.Name = "t2";
thread2.Start();
}


private static void ThreadProc()
{
System.Console.WriteLine($"\nCurrent thread:{Thread.CurrentThread.Name}");
if(Thread.CurrentThread.Name=="t1"&&
((thread2.ThreadState&ThreadState.Unstarted)==0))
if(thread2.Join(2000))
System.Console.WriteLine("t2 has been terminated");
else
System.Console.WriteLine("overtime");

//TimeSpan(0,0,4) 0h,0m,4s
Thread.Sleep(new TimeSpan(0,0,4));
System.Console.WriteLine($"\nCurrent thread:{Thread.CurrentThread.Name}");
System.Console.WriteLine($"Thread1:{thread1.ThreadState}");
System.Console.WriteLine($"Thread2:{thread2.ThreadState}");

}
}

在一次执行中,输出结果如下:
join_sleep
在此次运行过程中,线程t2首先进入ThreadProc,之后开始Sleep,时间片交给线程t1,if判断成立,t1进入阻塞状态等待t2运行结束。t2Sleep结束后开始运行,运行结束后,t1继续运行直到结束

Blocking

  • 如果线程的执行由于某种原因导致暂停,那么就认为该线程被阻塞了
    • SleepJoin
  • 被阻塞的线程会立即将其时间片交给其他线程,从此不再消耗处理器时间,知道满足阻塞结束条件为止
  • 可以通过ThreadState属性来判断线程是否处于阻塞状态
  • ThreadState是一个flags enum,通过按位与/或来合并数据项
    thread_state
    ThreadState变化图如下:
    transform
    常用的状态只有四个:UnstartedRunningWaitSleepJoinStopped
1
2
3
4
5
6
7
8
public static ThreadState Foo(ThreadState ts)
{
return ts & (
ThreadState.Stopped |
ThreadState.Unstarted |
ThreadState.WaitSleepJoin
);
}
  • 当遇到下列四种情况解除阻塞:

    • 阻塞条件被满足
    • 操作超时(如果设置了超时)
    • 通过Thread.Interrupt()进行打断
    • 通过Thread.Abort()进行中止
  • 上下文切换

    • 当线程阻塞或解除阻塞时,操作系统执行上下文切换。这会产生少量开销,通常为1或2微秒
  • I/O密集型与CPU密集型

    • 花费大部分时间等待某事发生的操作称为I/O密集型,通常此事指输入/输出,但不是硬性要求,如Thread.Sleep()被视为I/O密集型
    • 相反,一个花费大部分时间执行CPU密集型的操作成为CPU密集型
  • 阻塞与忙等待

    • 阻塞是在当前线程上同步的等待,Console.ReadLine()Thread.Sleep()Thread.Join()都是阻塞操作
    • 忙等待以周期性的在一个循环里打转,也是同步的
      while(DataTime.Now<nextStartTime)
    • 还有一种异步的操作,在操作完成后触发回调
    • 如果条件很快得到满足(在几微秒之内),短暂的忙等待更为适合,因为他避免了上下文切换的开销