线程
进程Process
线程Thread
线程中的指令:一个方法(委托)
线程中的数据:相关的对象
特性 | 描述 |
---|---|
CurrentPrincipal | 获取或者设定线程的当前安全性 |
CurrentThread | 获取对当前正在运行的线程的一个引用(static属性) |
IsAlive | 如果线程已经被启动并且尚在生命周期内,则返回True |
IsBackground | 如果目标线程是在后台执行的,则为此属性赋值为True |
Name | 获取或者设定这个线程的名字 |
Priority | 获取或者设定这个线程的名字 |
ThreadState | 获取线程的当前状态 |
Systen.Threading.Thread方法
方法 | 描述 |
---|---|
Abort() | 撤销这个线程 |
Interrupt() | 如果线程处于WaitSleepJoin状态,则中断它 |
Join() | 等待一个线程的结束 |
Resume() | 将被挂起的线程重新开始 |
Sleep() | 让线程休眠一定时间 |
Start() | 启动一个线程 |
Suspend() | 挂起一个线程 |
GetDomain() | 返回当前线程正在其中运行的当前域 |
GetDomainId() | 返回当前线程正在其中运行的当前域Id |
Thread 类有一个构造方法,格式如下:
Public Thread(ThreadStart fun);
其中ThreadStart是一个委托:
public delegate void TreadStart();
下面是创建一个Thread对象并启用这个线程的一般方法:
Thread thread = new Thread(new ThreadStart(obj.fun));
thread.start();
有时,使用匿名函数及Lambda表达式更方便。
启动:调用线程对象的Start()
停止:线程函数会一直执行下去,直至它结束。
Abort()终止
Suspend()挂起
Resume()恢复
Sleep()毫秒数
线程的状态
成员 | 描述 |
---|---|
Aborted | 线程已经被中断并且撤销 |
AbortRequested | 线程正在被请求中断 |
Background | 线程充当后台线程的角色,并且正在执行 |
Runing | 线程正在运行 |
Stopped | 线程停止运行(这个状态只限于内部使用) |
StopRequested | 线程正在被要求停止(这个状态只限于内部使用) |
Suspended | 线程已经被挂起 |
SuspendedRequested | 线程已经被要求挂起 |
Unstarted | 线程还没有被启动 |
WaitSleepJoin | 线程在一次Wait()、Sleep()以及Join()调用中被锁定 |
线程的优先级
成员 | 描述 |
---|---|
Highest | 线程具有最高优先级 |
AboveNormal | 线程的优先级高于普通优先级 |
Normal | 线程具有平均优先级 |
BelowNormal | 线程的优先级低于普通优先级 |
Lowest | 线程具有最低优先级 |
线程的同步
使用join()方法
将单独的执行线程合并成一个线程。
获取线程状态的实例:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Runtime.InteropServices;
namespace Thread_Example
{
internal class Program
{
static void Main(string[] args)
{
//获取正在运行的线程
Thread thread = Thread.CurrentThread;
//设置线程的名字
thread.Name = "主线程";
//获取当前线程的唯一标识符
int id = thread.ManagedThreadId;
//获取当前线程的状态
ThreadState state = thread.ThreadState;
//获取当前线程的优先级
ThreadPriority priority = thread.Priority;
string strMsg = string.Format("Thread ID:{0}\n" + "Thread Name:{1}\n" +
"Thread State:{2}\n" + "Thread Priority:{3}\n", id, thread.Name,
state, priority);
Console.WriteLine(strMsg);
Console.ReadKey();
}
}
}
前台线程和后台线程
前台线程:只有所有的前台线程都结束,应用程序才能结束。默认情况下创建的线程都是前台线程.
后台线程:只要所有的前台线程结束,后台线程自动结束。通过Thread.IsBackground设置后台线程。必须在调用Start方法之前设置线程的类型,否则一旦线程运行,将无法改变其类型。
通过BeginXXX方法运行的线程都是后台线程。
前后台程序示例:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Runtime.InteropServices;
namespace Thread_Example
{
internal class Program
{
static void Main(string[] args)
{
// 演示前台、后台线程
// 实例化一个前台线程
BackGroundTest background = new BackGroundTest(10);
// 创建前台线程
Thread fThread = new Thread(new ThreadStart(background.RunLoop));
// 给前台线程命名
fThread.Name = "前台线程";
// 实例化一个后台线程
BackGroundTest background1 = new BackGroundTest(20);
// 创建后台线程
Thread bThread = new Thread(new ThreadStart(background1.RunLoop));
// 给后台线程命名
bThread.Name = "后台线程";
// 设置为后台线程
bThread.IsBackground = true;
// 启动线程
fThread.Start();
bThread.Start();
// Console.ReadKey();
}
}
class BackGroundTest
{
private int Count;
public BackGroundTest(int count)
{
this.Count = count;
}
public void RunLoop()
{
//获取当前线程的名称
string threadName = Thread.CurrentThread.Name;
for (int i = 0; i<Count; i++)
{
Console.WriteLine("{0}计数:{1}",threadName,i.ToString());
//线程休眠500毫秒
Thread.Sleep(1000);
}
Console.WriteLine("{0}完成计数", threadName);
}
}
}
运行结果:前台线程执行完,后台线程未执行完,程序自动结束。
把
bThread.IsBackground = true;
注释掉,运行结果:主线程执行完毕后(Main函数),程序并未结束,而是要等所有的前台线程结束以后才会结束。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Runtime.InteropServices;
namespace Thread_Example
{
internal class Program
{
static void Main(string[] args)
{
// 演示前台、后台线程
// 实例化一个前台线程
BackGroundTest background = new BackGroundTest(10);
// 创建前台线程
Thread fThread = new Thread(new ThreadStart(background.RunLoop));
// 给前台线程命名
fThread.Name = "前台线程";
// 实例化一个后台线程
BackGroundTest background1 = new BackGroundTest(20);
// 创建后台线程
Thread bThread = new Thread(new ThreadStart(background1.RunLoop));
// 给后台线程命名
bThread.Name = "后台线程";
// 设置为后台线程
// bThread.IsBackground = true;
// 启动线程
fThread.Start();
bThread.Start();
// Console.ReadKey();
}
}
class BackGroundTest
{
private int Count;
public BackGroundTest(int count)
{
this.Count = count;
}
public void RunLoop()
{
//获取当前线程的名称
string threadName = Thread.CurrentThread.Name;
for (int i = 0; i<Count; i++)
{
Console.WriteLine("{0}计数:{1}",threadName,i.ToString());
//线程休眠500毫秒
Thread.Sleep(1000);
}
Console.WriteLine("{0}完成计数", threadName);
}
}
}
后台线程一般用于处理不重要的事情,应用程序结束时,后台线程是否执行完成对整个应用程序没有影响。如果要执行的事情很重要,需要将线程设置为前台线程。
线程同步
在多线程编程中,可能会有许多线程并发的执行一段代码。在某些情况下,我们希望A中的代码块(B)同步的执行,即同一时刻只有一个线程执行代码块B,这就需要用到锁(lock)。lock 关键字可以用来确保代码块完成运行,而不会被其他线程中断。它可以把一段代码定义为互斥段(critical section),互斥段在一个时刻内只允许一个线程进入执行,而其他线程必须等待,以达到安全访问。
所谓同步:是指在某一时刻只有一个线程可以访问变量。
如果不能确保对变量的访问是同步的,就会产生错误。
c#为同步访问变量提供了一个非常简单的方式,即使用c#语言的关键字Lock,它可以把一段代码定义为互斥段,互斥段在一个时刻内只允许一个线程进入执行,而其他线程必须等待。在c#中,关键字Lock定义如下:
lock(expression)
{
statement_block;
}
expression代表你希望跟踪的对象:
如果你想保护一个类的实例,一般地,你可以使用this;
如果你想保护一个静态变量(如互斥代码段在一个静态方法内部),一般使用类名就可以了。
而statement_block就算互斥段的代码,这段代码在一个时刻内只可能被一个线程执行。
例子:现有十个苹果,张三和李四同时吃这些苹果
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Runtime.InteropServices;
namespace Thread_Example
{
internal class Program
{
private static int apple = 10; //10个苹果
static void Main(string[] args)
{
Thread t1 = new Thread(() => EatApple("张三"));
Thread t2 = new Thread(() => EatApple("李四"));
t1.IsBackground = true;
t2.IsBackground = true;
t1.Start();
t2.Start();
Console.ReadKey();
}
private static void EatApple(string name)
{
while (true)
{
apple -= 1;
Console.WriteLine(name + "正在吃苹果");
Thread.Sleep(3000);
Console.WriteLine(name + "吃完了,还剩" + apple + "个苹果\n");
if (apple <= 0)
break;
}
}
}
}
把共同访问的代码加上锁之后:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Runtime.InteropServices;
namespace Thread_Example
{
internal class Program
{
private static int apple = 1000; //10个苹果
private static object locker = new object();//创建锁
static void Main(string[] args)
{
Thread t1 = new Thread(() => EatApple("张三"));
Thread t2 = new Thread(() => EatApple("李四"));
t1.IsBackground = true;
t2.IsBackground = true;
t1.Start();
t2.Start();
Console.ReadKey();
}
private static void EatApple(string name)
{
while (true)
{
lock (locker)//加锁
{
apple -= 1;
Console.WriteLine(name + "正在吃苹果");
Thread.Sleep(50);
Console.WriteLine(name + "吃完了,还剩" + apple + "个苹果\n");
if (apple <= 1) //变为1 不然会吃-1个苹果
{
break;
}
}
}
}
}
}
注意:
- lock(this) 锁定 当前实例对象,如果有多个类实例的话,lock锁定的只是当前类实例,对其它类实例无影响。
- lock(typeof(Model))锁定的是model类的所有实例。
- lock(obj)锁定的对象是全局的私有化静态变量。外部无法对该变量进行访问。
- lock 确保当一个线程位于代码的临界区时,另一个线程不进入临界区。如果其他线程试图进入锁定的代码,则它将一直等待(即被阻止),直到该对象被释放。
所以,lock的结果好不好,还是关键看锁的谁,如果外边能对这个谁进行修改,lock就失去了作用。所以一般情况下,使用私有的、静态的并且是只读的对象。
- lock的是必须是引用类型的对象,string类型除外。
- lock推荐的做法是使用静态的、只读的、私有的对象。
- 保证lock的对象在外部无法修改才有意义,如果lock的对象在外部改变了,对其他线程就会畅通无阻,失去了lock的意义。
不能锁定字符串,锁定字符串尤其危险,因为字符串被公共语言运行库 (CLR)“暂留”。 这意味着整个程序中任何给定字符串都只有一个实例,就是这同一个对象表示了所有运行的应用程序域的所有线程中的该文本。因此,只要在应用程序进程中的任何位置处具有相同内容的字符串上放置了锁,就将锁定应用程序中该字符串的所有实例。通常,最好避免锁定 public 类型或锁定不受应用程序控制的对象实例。例如,如果该实例可以被公开访问,则 lock(this) 可能会有问题,因为不受控制的代码也可能会锁定该对象。这可能导致死锁,即两个或更多个线程等待释放同一对象。出于同样的原因,锁定公共数据类型(相比于对象)也可能导致问题。而且lock(this)只对当前对象有效,如果多个对象之间就达不到同步的效果。lock(typeof(Class))与锁定字符串一样,范围太广了。
lock的用法格式:
Object locker = new Object();
lock(locker)
{
//此处放置同步执行的代码
}
相当于:
private static object locker = new object();//创建锁
Monitor.Enter(locker); //排它锁
{
//此处放置同步执行的代码
}
Monitor.Exit(locker); //释放指定对象上的排他锁
Monitor的常用属性和方法:
Enter(Object) 在指定对象上获取排他锁。
Exit(Object) 释放指定对象上的排他锁。
Pulse 通知等待队列中的线程锁定对象状态的更改。
PulseAll 通知所有的等待线程对象状态的更改。
TryEnter(Object) 试图获取指定对象的排他锁。
TryEnter(Object, Boolean) 尝试获取指定对象上的排他锁,并自动设置一个值,指示是否得到了该锁。
Wait(Object) 释放对象上的锁并阻止当前线程,直到它重新获取该锁。
常用的方法有两个,Monitor.Enter(object)方法是获取锁,Monitor.Exit(object)方法是释放锁,这就是Monitor最常用的两个方法,在使用过程中为了避免获取锁之后因为异常,致锁无法释放,所以需要在try{} catch(){}之后的finally{}结构体中释放锁(Monitor.Exit())
TryEnter(Object)和TryEnter() 方法在尝试获取一个对象上的显式锁方面和 Enter() 方法类似。然而,它不像Enter()方法那样会阻塞执行。如果线程成功进入关键区域那么TryEnter()方法会返回true. 和试图获取指定对象的排他锁。
我们可以通过Monitor.TryEnter(monster, 1000),该方法也能够避免死锁的发生,Monitor.TryEnter(Object,Int32)。
设置1S的超时时间,如果在1S之内没有获得同步锁,则返回false,也就是说,在1秒中后,lockObj还未被解锁,TryEntry方法就会返回false,如果在1秒之内,lockObj被解锁,TryEntry返回true。我们可以使用这种方法来避免死锁
Monitor.Wait和Monitor()Pause()
Wait(object)方法:释放对象上的锁并阻止当前线程,直到它重新获取该锁,该线程进入等待队列。
Pulse方法:只有锁的当前所有者可以使用 Pulse 向等待对象发出信号,当前拥有指定对象上的锁的线程调用此方法以便向队列中的下一个线程发出锁的信号。接收到脉冲后,等待线程就被移动到就绪队列中。在调用 Pulse 的线程释放锁后,就绪队列中的下一个线程(不一定是接收到脉冲的线程)将获得该锁。
另外:Wait 和 Pulse 方法必须写在 Monitor.Enter 和Moniter.Exit 之间**。**