线程(Thread)是进程中的基本执行单元,是操作系统分配CPU时间的基本单位,一个进程可以包含若干个线程,在进程入口执行的第一个线程被视为这个进程的主线程。在.NET应用程序中,都是以Main()方法作为入口的,当调用此方法时系统就会自动创建一个主线程。线程主要是由CPU寄存器、调用栈和线程本地存储器(Thread Local Storage,TLS)组成的。CPU寄存器主要记录当前所执行线程的状态,调用栈主要用于维护线程所调用到的内存与数据,TLS主要用于存放线程的状态信息。(即线程是CPU调度的基本单位)
当前CPU运行速度太快,硬件处理速度跟不上,所以操作系统进行分时间片管理。这样,从宏观角度来说是多线程并发的,因为CPU速度太快,察觉不到,看起来是同一时刻执行了不同的操作。但是从微观角度来讲,同一时刻只能有一个线程在处理。(单核CPU情况),所以对于使用多线程来处理,能够是程序执行更加高效,并且能够实现并发量。但是在使用多线程时候,并不是越多越好,频繁的去创建多线程容易造成内存的损耗,当要并发的代码段十分简单,创建多个线程需要进行上下文切换等,并不会比单个线程块
所以说线程是一把双刃剑。如:
(1)线程也是程序,所以线程需要占用内存,线程越多,占用内存也越多。
(2)多线程需要协调和管理,所以需要占用CPU时间以便跟踪线程。
(3)线程之间对共享资源的访问会相互影响,必须解决争用共享资源的问题。
(4)线程太多会导致控制太复杂,最终可能造成很多程序缺陷。
一、在默认的情况下,C#程序具有一个线程,此线程执行程序中以Main方法开始和结束的代码,Main()方法直接或间接执行的每一个命令都有默认线程(主线程)执行,当Main()方法返回时此线程也将终止。
二、在C#中,线程是使用Thread类处理的,该类在System.Threading命名空间中。使用Thread类创建线程时,只需要提供线程入口,线程入口告诉程序让这个线程做什么。通过实例化一个Thread类的对象就可以创建一个线程。创建新的Thread对象时,将创建新的托管线程。Thread类接收一个ThreadStart委托或ParameterizedThreadStart委托的构造函数。且有参的委托其参数类型必须为object类型
//C#创建多线程,可以使用静态成员方法,也可以使用成员方法。
MyThread mythread = new MyThread();
Thread th1 = new Thread(new ThreadStart(mythread.TestThreadFunc2));
Thread th2 = new Thread(new ThreadStart(MyThread.TestThreadFunc1));
Thread th3 = new Thread(new ParameterizedThreadStart(mythread.TestThreadFunc3));
th1.Start();
th2.Start();
th3.Start("parame");
Console.ReadLine();
}
class MyThread
{
public static void TestThreadFunc1()
{
for(int i = 0; i < 5; i++)
{
Console.WriteLine("I am a static func thread......");
}
}
public void TestThreadFunc2()
{
for(int i = 0; i < 5; i++)
{
Console.WriteLine("I am a sample func thread....");
}
}
public void TestThreadFunc3(object name )
{
for(int i = 0; i < 5; i++)
{
Console.WriteLine("I am a param func thread....{0}", name);
}
}
}
属性 | 说明 |
---|---|
CurrentContext | 获取线程正在其中执行的当前上下文。 |
CurrentThread | 获取当前正在运行的线程。 |
ExecutionContext | 获取一个 ExecutionContext 对象,该对象包含有关当前线程的各种上下文的信息。 |
IsAlive | 获取一个值,该值指示当前线程的执行状态。 |
IsBackground | 获取或设置一个值,该值指示某个线程是否为后台线程。 |
IsThreadPoolThread | 获取一个值,该值指示线程是否属于托管线程池。 |
ManagedThreadId | 获取当前托管线程的唯一标识符。 |
Name | 获取或设置线程的名称。 |
Priority | 获取或设置一个值,该值指示线程的调度优先级。 |
ThreadState | 获取一个值,该值包含当前线程的状态。 |
mythread1.Count = 10;
mythread2.Count = 20;
Thread frontThread = new Thread(new ThreadStart(mythread1.RunLoop));
frontThread.Name = "前台线程";
Thread backThread = new Thread(new ThreadStart(mythread2.RunLoop));
backThread.Name = "后台线程";
backThread.IsBackground = true;
backThread.Start();
frontThread.Start();
}
class MyThread
{
public int Count { get; set; }
public void RunLoop()
{
string threadName = Thread.CurrentThread.Name;
for(int i = 0; i < Count; i++)
{
Console.WriteLine("{0} Count : {1}", threadName, i);
}
}
}
可以看到,后台程序没有执行完,程序就结束运行了。当我们把isBackground注释掉(默认的都为前台进程)。
程序需要等待所有前台线程执行完才结束。后台线程一般用于处理不重要的事情,应用程序结束时,后台线程是否执行完成对整个应用程序没有影响。如果要执行的事情很重要,需要将线程设置为前台线程。
方法名 | 功能 |
---|---|
Abort() | 终止本线程 |
GetDomain() | 返回当前线程正在其中运行的当前域。 |
GetDomainId() | 返回当前线程正在其中运行的当前域Id。 |
Interrupt() | 中断处于 WaitSleepJoin 线程状态的线程。 |
Join() | 已重载。 阻塞调用线程,直到某个线程终止时为止。 |
Resume() | 继续运行已挂起的线程。 |
Start() | 执行本线程。 |
Suspend() | 挂起当前线程,如果当前线程已属于挂起状态则此不起作用 |
Sleep() | 把正在运行的线程挂起一段时间。 |
下面对Interrupt函数进行代码演示。
sleeper = new Thread(new ThreadStart(SleepThread));
interrupter = new Thread(new ThreadStart(InterruptThread));
sleeper.Start();
interrupter.Start();
}
public static Thread sleeper;
public static Thread interrupter;
static public void SleepThread()
{
for(int i = 0; i < 50; i++)
{
Console.WriteLine("SleepThread:{0}", i);
if(i == 10 || i == 20 || i == 30)
{
Console.WriteLine("在循环 {0} 处睡眠",i);
try
{
Thread.Sleep(1000);
}
catch
{
}
}
}
}
static public void InterruptThread()
{
while(true)
{
if(sleeper.ThreadState == ThreadState.WaitSleepJoin)
{
Console.WriteLine("中断睡眠状态,继续工作");
sleeper.Interrupt();
}
}
}
本来当i=10,20 30时,线程需要进入睡眠态,但是通过interrupt函数强制其不睡眠,继续进入Running态
1.Lock(expression)
{
statement_block
}
expression代表你希望跟踪的对象:
如果你想保护一个类的实例,一般地,你可以使用this;
如果你想保护一个静态变量(如互斥代码段在一个静态方法内部),一般使用类名就可以了
而statement_block就算互斥段的代码,这段代码在一个时刻内只可能被一个线程执行。
如下代码中运行就会出现资源的不同步现象
static void Main(string[] args)
{
BookShop bookShop = new BookShop(1);
Thread th1 = new Thread(bookShop.Sale);
Thread th2 = new Thread(bookShop.Sale);
Thread th3 = new Thread(bookShop.Sale);
th1.Start();
th2.Start();
th3.Start();
}
}
class BookShop
{
public int BookNumbers { get; set; }
public BookShop(int count)
{
this.BookNumbers = count;
}
public void Sale()
{
int tmp = BookNumbers;
if(tmp > 0)
{
Thread.Sleep(1000);
BookNumbers -= 1;
Console.WriteLine("售出一本书 还剩下{0}本书", BookNumbers);
}
else
{
Console.WriteLine("售空");
}
}
}
运行结果如图
在多线程对BookNumbers访问时候,没有进行资源的同步互斥,导致结果出现异常,可以使用Lock进行处理,保证同一时间只能有一个线程对其进行访问
lock(this)
{
int tmp = BookNumbers;
if (tmp > 0)
{
Thread.Sleep(1000);
BookNumbers -= 1;
Console.WriteLine("售出一本书 还剩下{0}本书", BookNumbers);
}
else
{
Console.WriteLine("售空");
}
}
执行结果就正常了
lock的本质是将包围的语句块标记为临界区,这样一次只有一个线程进入临界区并执行代码。lock()语句中必须为引用类型在给lock传递参数时首先要避免使用public对象,因为有可能外部程序也在对这个对象加锁,这样就可能造成死锁
lock(this)、lock(typeof(myType))、lock(“string”)。而上述三种情况微软都是不建议我们使用的。
在上诉代码中,我还是使用了lock(this),msdn的原话是lock(this) 可能会有问题,因为不受控制的代码也可能会锁定该对象。这可能导致死锁。
lock(this)就是锁定当前执行方法的实例对象,当内部进行加锁后,外部其他线程也对其进行加锁,就会造成死锁。
MyLockDemo myLockDemo = new MyLockDemo();
object ject = new object();
Thread th = new Thread(new ParameterizedThreadStart(myLockDemo.LockFunc));
th.Start(ject);
lock(myLockDemo)
{
Console.WriteLine("主线程锁定myLockDemo,立马对ject锁定");
lock(ject)
{
Console.WriteLine("主线程对myLockDemo,ject都锁定");
}
}
}
class MyLockDemo
{
public void LockFunc(object obj)
{
lock(obj)
{
Console.WriteLine("子线程对obj加锁,并且立马加锁MyLockDemo");
lock(this)
{
Console.WriteLine("完成对obj MyLockDemo都加锁");
}
}
}
}
private void button1_Click(object sender, EventArgs e)
{
Thread th = new Thread(new ThreadStart(Test));
th.IsBackground = true;
th.Start();
}
public void Test()
{
for(int i = 0; i < 10000; i++)
{
this.textBox1.Text = i.ToString();
}
}
点击测试,创建线程给Textbox赋值。
此时会报出异常,线程间操作无效,不是创建控件的textBox1访问它。
对此解决方案:
1.在窗体的加载事件中,将C#内置控件(Control)类的CheckForIllegalCrossThreadCalls属性设置为false,屏蔽掉C#编译器对跨线程调用的检查。
Control.CheckForIllegalCrossThreadCalls = false;
使用上述的方法虽然可以保证程序正常运行并实现应用的功能,但是在实际的软件开发中,做如此设置是不安全的(不符合.NET的安全规范),在产品软件的开发中,此类情况是不允许的。如果要在遵守.NET安全标准的前提下,实现从一个线程成功地访问另一个线程创建的空间,要使用C#的方法回调机制。
2.使用回调函数
//定义回调
private delegate void setTextValueCallBack(int value);
//声明回调
private setTextValueCallBack setCallBack;
private void Test()
{
for (int i = 0; i < 10000; i++)
{
//使用回调
textBox1.Invoke(setCallBack, i);
}
}
///
/// 定义回调使用的方法
///
///
private void SetValue(int value)
{
this.textBox1.Text = value.ToString();
}
public SetBoxText setCallBack;
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
setCallBack = new SetBoxText(SetValue);
Console.WriteLine("{0}Start", Thread.CurrentThread);
Action action = this.Test;
action.BeginInvoke(null, null);
Console.WriteLine("Caculate over......");
}
public void Test()
{
for(int i = 0; i < 100; i++)
{
Console.WriteLine("is Caculator....");
textBox1.Invoke(setCallBack, i);
}
}
public void SetValue(int value)
{
this.textBox1.Text = value.ToString();
}
在该代码中,使用BeginInvoke进行异步调用,发现程序执行结果并不是想象的,应该当计算结束后,才会提示运行结束,而运行结果如图。
解决无序的问题,在C#中.NET框架中已经帮我们实现了回调
BeginInvoke第二个参数就是回调。之后我们对代码进行改造,实现同步调用。
Action action = this.Test;
AsyncCallback callback = p =>
{
Console.WriteLine($"到这里计算已经完成了。{Thread.CurrentThread.ManagedThreadId.ToString("00")}。");
};
action.BeginInvoke(callback, null);
.NET Framework提供了包含ThreadPool类的System.Threading 空间,这是一个可直接访问的静态类,该类对线程池是必不可少的。它是公共“线程池”设计样式的实现。对于后台运行许多各不相同的任务是有用的。对于单个的后台线种而言有更好的选项。
线程的最大数量。这是完全无须知道的。在.NET中ThreadPool的所有要点是它自己在内部管理线程池中线程。多核机器将比以往的机器有更多的线程。微软如此陈述“线程池通常有一个线程的最大数量,如果所有的线程都忙,增加的任务被放置在队列中直到它们能被服务,才能作为可用的线程。
线程池类型能被用于服务器和批处理应用程序中,线程池有更廉价的得到线程的内部逻辑,因为当需要时这些线程已被形成和刚好“连接”,所以线程池风格代码被用在服务器上。
MSDN表述:“线程池经常用在服务器应用程序中,每一个新进来的需求被分配给一个线程池中的线程,这样该需求能被异步的执行,没有阻碍主线程或推迟后继需求的处理。
在多线程中,线程把大部分时间花费在等待状态,等待某个事件发生,然后才能给予相应,一般使用ThreadPool线程池来解决。另一种情况,线程都处于休眠状态,只是周期性的被唤醒,一般使用Timer定时器来解决。
ThreadPool类提供一个线程池,该线程池可用于发送工作项、处理异步 I/O、代表其他线程等待以及处理计时器。该类提供一个由系统维护的线程池(可以看作一个线程的容器),该容器需要Windows 2000以上系统支持。
方法 | 描述 |
---|---|
BindHandle | 将操作系统句柄绑定到ThreadPool |
GetAvailableThreads | 检索由GetMaxThreads方法返回的最大线程池线程数和当前活动线程数之间的差值 |
GetMaxThreads | 检索可以同时处于活动状态的线程池请求的数目。所有大于此数目的请求将保持排队状态,直到线程池线程变为可用 |
GetMinThreads | 检索线程池在新请求预测中维护的空闲线程数 |
QueueUserWorkItem | 将方法排入队列以便执行。此方法在有线程池线程变得可用时执行 |
RegisterWaitForSingleObject | 注册正在等待WaitHandle的委托 |
SetMaxThreads | 设置可以同时处于活动状态的线程池的请求数目。所有大于此数目的请求将保持排队状态,直到线程池线程变为可用 |
UnsafeQueueNativeOverlapped | 将重叠的 I/O 操作排队以便执行 |
UnsafeQueueUserWorkItem | 注册一个等待 WaitHandle 的委托 |
UnsafeRegisterWaitForSingleObject | 将指定的委托排队到线程池 |
int workThreads;
int workIo;
ThreadPool.GetMaxThreads(out workThreads, out workIo);
Console.WriteLine("设置前最大工作线程数:{0},最大IO数:{1}", workThreads, workIo);
ThreadPool.SetMaxThreads(20, 10);
ThreadPool.GetMaxThreads(out workThreads, out workIo);
Console.WriteLine("设置后最大工作线程数:{0},最大IO数:{1}", workThreads, workIo);
ThreadPool.GetMinThreads(out workThreads, out workIo);
Console.WriteLine("设置前最小工作线程数:{0},最大IO数:{1}", workThreads, workIo);
if(ThreadPool.SetMinThreads(10, 30))
{
//设置成功
ThreadPool.GetMinThreads(out workThreads, out workIo);
Console.WriteLine("设置后最小工作线程数:{0},最大IO数:{1}", workThreads, workIo);
}
即使是在所有线程都处于空闲状态时,线程池也会维持最小的可用线程数,以便队列任务可以立即启动。将终止超过此最小数目的空闲线程,以节省系统资源。
通过上述方法,来设置最大最小辅助线程数目和异步Io数,
设置最小线程数时候,函数设置不成功返回false。
加入队列
ThreadPoolFunc poolFunc = new ThreadPoolFunc();
ThreadPool.QueueUserWorkItem(new WaitCallback(poolFunc.Func1), 3);
ThreadPool.QueueUserWorkItem(new WaitCallback(poolFunc.Func2), 2);
Console.ReadLine();
}
class ThreadPoolFunc
{
public void Func1(object o)
{
while(true)
{
Console.WriteLine("I am Func1.....");
}
}
public void Func2(object o)
{
while(true)
{
Console.WriteLine("I am Func2:{0}", (int)o);
}
}
ThreadPool的线程都是后台线程,当主线程结束了之后,会自动结束线程。
thread和threadpool,这都是异步操作,threadpool其实就是thread的集合,具有很多优势,不过在任务多的时候全局队列会存在竞争而消耗资源。thread默认为前台线程,主程序必须等线程跑完才会关闭,而threadpool相反。
Task简单的看就是任务,其背后实现与使用了线程池,但它的性能优于ThradPool,因为它使用的队列不是全局队列,而使用的是本地队列。使得资源的竞争变少。同时Task类还提供了丰富的API来控制管理线程, 在多核CPU情况下Task的性能优于Thread与ThreadPool,而在单核情况下三者性能没有太大区别。
var task = new Task(() =>
{
Console.WriteLine("I am new Task...");
});
task.Start();
var facTask = Task.Factory.StartNew(() =>
{
Console.WriteLine("I am fac task.");
});
var testTask = new Task(() =>
{
Console.WriteLine("task start");
System.Threading.Thread.Sleep(2000);
});
Console.WriteLine(testTask.Status);
testTask.Start();
Console.WriteLine(testTask.Status);
Console.WriteLine(testTask.Status);
testTask.Wait();
Console.WriteLine(testTask.Status);
Console.WriteLine(testTask.Status);
var testTask = new Task(() =>
{
while(true)
{
Console.WriteLine("task start");
System.Threading.Thread.Sleep(1000);
}
});
testTask.Start();
testTask.Wait();
while(true)
{
Thread.Sleep(1000);
Console.WriteLine("Main Thread...");
}
Wait方法,对单个Task等待,等待任务执行完。
var testTask = new Task(() =>
{
Console.WriteLine("task start");
System.Threading.Thread.Sleep(2000);
});
testTask.Start();
var factoryTeak = Task.Factory.StartNew(() =>
{
Console.WriteLine("factory task start");
});
Task.WaitAny(testTask);
Console.WriteLine("end");
使用ContinueWith(Func
var testTask = new Task(() =>
{
Console.WriteLine("task start");
System.Threading.Thread.Sleep(2000);
});
testTask.Start();
var resultTest = testTask.ContinueWith<string>((Task) => {
Console.WriteLine("testTask end");
return "end";
});
Console.WriteLine(resultTest.Result);
async/await特性是与Task紧密相关的。它们也是用于异步操作,若声明一个async的函数没有使用await,则编译器也会报出警告,会将其当做同步方法。
在C# GUI编程时,如有些比较耗时的操作时,通过新开一个线程去处理,而不是在主线程中去处理,以免造成UI刷新阻塞线程。
本处,最近我在编写C#文件管理器时,根据Windows API获取历史文件访问时,这个操作比较费时,若使用同步方法,则在获取过程中会造成UI界面卡死,等待时间过长,出现任务无响应。
使用async/await方式,能够避免使用传统方法Thread类造成的回调地狱。所以C#更偏向使用async/await 方式来进行异步操作。
GetValueAsync();
Console.WriteLine("Main End...");
Console.ReadKey();
}
static async Task GetValueAsync()
{
await Task.Run(() =>
{
Thread.Sleep(1000);
for(int i = 0; i < 5; i++)
{
Console.WriteLine("Format task:{0}",i);
}
});
Console.WriteLine("Task END.");
}
若在方法中缺少await关键字,在编译器中会报出警告,此时执行流不会遇到流阻塞点,会继续执行下去。
执行流不会在Task.Run()这里停下返回,而是直接向下执行。但是新的线程还是会被创建出来。但是这种情况,程序就不会去等待Task.Run()完成了。
C#中规定Main()函数是不能够为异步方法,这说明了任何异步调用都是同步调用的子调用。
async/await是不会主动创建线程(Task)的,创建线程的工作还是交给程序员来完成;async/await说白了就只是用来提供阻塞调用点的关键字而已。
使用CancellationTokenSource来取消Task。
var tokenSource = new CancellationTokenSource();
var task = new Task(() =>
{
for(int i = 0; i < 6; i++)
{
Console.WriteLine("Thread Run....");
Thread.Sleep(1000);
}
}, tokenSource.Token);
//task.Start();
Console.WriteLine(task.Status);
tokenSource.Cancel();
Console.WriteLine(task.Status);
Console.Read();
var parentTask = new Task(() => {
var childTask = new Task(() => {
System.Threading.Thread.Sleep(2000);
Console.WriteLine("childTask to start");
});
childTask.Start();
Console.WriteLine("parentTask to start");
});
parentTask.Start();
parentTask.Wait();
Console.WriteLine("end");
Console.ReadLine();
var parentTask = new Task(() => {
var childTask = new Task(() => {
System.Threading.Thread.Sleep(2000);
Console.WriteLine("childTask to start");
}, TaskCreationOptions.AttachedToParent);
childTask.Start();
Console.WriteLine("parentTask to start");
});
parentTask.Start();
parentTask.Wait();
Console.WriteLine("end");
表示线程同步事件,收到信号时,必须手动重置该事件。 无法继承此类。
其构造函数为
public ManualResetEvent (bool initialState);
initialState 表示是否初始化,如果为 true,则将初始状态设置为终止(不阻塞);如果为 false,则将初始状态设置为非终止(阻塞)。
有两个方法
//将事件状态设置为终止状态,从而允许继续执行一个或多个等待线程。
public bool Set ();
//将事件状态设置为非终止,从而导致线程受阻。
public bool Reset ();