进程(Process)包含运行程序所需要的资源。 正在运行的应用程序被视为进程,进程可以有多个线程。
进程之间是相对独立的,一个进程无法访问另一个进程的数据(除非利用分布式计算方式),
一个进程运行的失败也不会影响其他进程的运行,Windows系统就是利用进程把工作划分为多个独立的区域的。
进程可以理解为程序的基本边界。是应用程序的一个运行例程,是应用程序的一次动态执行过程。
简要总结:应用程序视为进程,进程独立,进程失败互不影响。
线程(Thread)是进程中的基本执行单元,是操作系统分配CPU时间的基本单位。
一个进程可以包含若干个线程, 在进程入口执行的第一个线程被视为这个进程的主线程。在.NET应用程序中,都是以Main()方法作为入口的, 当调用此方法时系统就会自动创建一个主线程。
应用程序域(AppDomain)是程序运行的逻辑区域,它视为轻量级的进程,程序集在应用程序域中运行的,进程可以有多个应用程序域,应用程序域可以包含多个程序集。在应用程序域中包含了多个上下文context,使用上下文 context CLR
就能够把某些特殊对象的状态放置在不同容器当中。
上下文:在应用程序域当中,存在更细粒度的用于承载.NET对象的实体,那就.NET上下文Context。
公共语言运行时 CLR Common Language Runtime
是一个运行时环境,保证应用和底层操作系统之间必要的分离,是.NET Framework的主要执行引擎。是可由面向CLR的多种编程语言使用的“运行时”。CLR的核心功能(内存管理、程序集加载、安全性、异常处理和线程同步等)由面向CLR的所有语言使用。
1 分离 2执行引擎 3 运行时
程序集是包含一个或多个类型定义文件和资源文件的集合.它允许我们分析可重用类型的逻辑表示和物理表示.
主要包括线程内核对象、线程环境块、1M大小的用户模式栈、内核模式栈。其中用户模式栈对于普通的系统线程那1M是预留的,在需要的时候才会分配,但是对于CLR线程,那1M是一开始就分类了内存空间的。
补充一句,CLR线程是直接对应于一个Windows线程的。
线程的开销
- 线程内核对象(thread kernel object)
- 操作系统为创建的每个线程都会分配并初始化这种数据结构。数据结构包含一组对线程进行描述的属性,还包含线程的上下文,包含模拟CPU寄存器的集合的内存块。
- 线程环境块(thread environment block,TEB)
- TEB是在用户模式下分配和初始化的内存块。
- TEB包含线程的异常处理链首。线程进入每个try块都在链首插入一个节点,线程退出try块从链中删除该节点。
- TEB还包含线程的“线程本地存储”数据,以及由GDI和OpenGL图形使用的一些数据结构。
- 用户模式栈(user-mode stack)
- 用户模式栈存储传给方法的局部变量和实参。包含一个地址,指出当前方法返回,线程应该从什么地方接着执行。
- Windows默认为每个线程的用户模式栈分配1MB的空间。
- 内核模式栈(kernel-mode stack)
- 应用程序代码向操作系统中的内核模式函数传递实参时,还会使用内核模式栈。针对传给内核的任何实参,都会从用户模式栈复制到内核模式栈。
- DLL线程连接(attach)和线程分离(detach)通知
- 任何时候在进程中创建线程,都会调用进程中加载的所有非托管DLL的DLLMain方法,并向该方法传递DLL_THREAD_ATTACH标志。
- 任何时候在进程中线程终止,都会调用进程中加载的所有非托管DLL的DLLMain方法,并向该方法传递DLL_THREAD_DETACH标志
Windows系统采用时间轮询机制,CPU计算机资源以时间片(大约30ms)的形式分配给执行线程。
计算机资源(CPU核心和CPU寄存器)一次只能调度一个线程,具体的调度流程:
线程调度的过程,就是一次线程切换,一次切换就涉及到线程上下文等数据的搬入搬出。
线程的创建和消费也是很昂贵的,这也是建议使用线程池的一个主要原因。
使用专用线程执行异步的计算限制操作
以下介绍使用专用线程执行异步的计算限制操作,但是建议避免使用此技术,而用线程池来执行异步的计算限制操作。
如果执行的代码要求线程处于一种特定的状态,而这种状态对于线程池线程来说是非同寻常的,就可以考虑创建专用线程。
例如:
简要 1 非普通优先级运行 2 表现为前台线程 3 长时间运行 4 使用abort提前终止
hread构造函数传递方法有两种方式:
public delegate void ThreadStart();
public delegate void ParameterizedThreadStart (object obj);
所以Thread可以传递零个或一个参数,但是没有返回值。
static void Main()
{
Thread t = new Thread ( () => Print ("Hello from t!") );
t.Start();
}
static void Print (string message)
{
Console.WriteLine (message);
}
static void Main()
{
Thread t = new Thread (Print);
t.Start ("Hello from t!");
}
static void Print (object messageObj)
{
string message = (string) messageObj;
Console.WriteLine (message);
}
Lambda简洁高效,但是在捕获变量的时候要注意,捕获的变量是否共享。
如:
for (int i = 0; i < 10; i++)
new Thread (() => Console.Write (i)).Start();
//输出:
//0223447899
因为每次循环中的i都是同一个i,是共享变量,在输出的过程中,i的值会发生变化。
解决方法-局部域变量
for (int i = 0; i < 10; i++)
{
int temp = i;
new Thread (() => Console.Write (temp)).Start();
}
这时每个线程都指向新的域变量temp(此时每个线程都有属于自己的花括号的域变量)在该线程中temp不受其他线程影响。
总结:thread构造传参有ThreadStart和ParameterizedThreadStart,都无返回值,所以可以传递有参或无参的无返回值变量,Lambda注意捕获共享变量,局部变量重赋值
线程Start()之后,线程的IsAlive属性就为true,直到该线程结束(当线程传入的方法结束时,该线程就结束)。
CLR使每个线程都有自己独立的内存栈,所以每个线程的本地变量都相互独立。
如:
static void Main()
{
new Thread (Go).Start(); // 创建一个新线程,并调用Go方法
Go(); // 在主线程上调用Go方法
}
static void Go()
{
// 声明一个本地局部变量 cycles
for (int cycles = 0; cycles < 5; cycles++) Console.Write ('N');
}
//输出:
//NNNNNNNNNN (共输出10个N)
在新线程和主线程上调用Go方法时分别创建了变量cycles,这时cycles在不同的线程栈上,所以相互独立不受影响。
如果不同线程指向同一个实例的引用,那么不同的线程共享该实例。
如:
class ThreadTest
{
//全局变量
int i;
static void Main()
{
ThreadTest tt = new ThreadTest(); // 创建一个ThreadTest类的实例
new Thread (tt.Go).Start();
tt.Go();
}
// Go方法属于ThreadTest的实例
void Go()
{
if (i==1) { ++i; Console.WriteLine (i); }
}
}
//输出:
//2
新线程和主线程上调用了同一个实例的Go方法,所以变量i共享。
静态变量也可以被多线程共享
class ThreadTest
{
static int i; // 静态变量可以被线程共享
static void Main()
{
new Thread (Go).Start();
Go();
}
static void Go()
{
if (i==1) { ++i; Console.WriteLine (i); }
}
}
//输出:
//2
如果将Go方法的代码位置互换
static void Go()
{
if (i==1) { Console.WriteLine (i);++i;}
}
//输出:
//1
//1(有时输出一个,有时输出两个)
如果新线程在Write之后,done=true之前,主线程也执行到了write那么就会有两个done。
不同线程在读写共享字段时会出现不可控的输出,这就是多线程的线程安全问题。
解决方法: 使用排它锁来解决这个问题--lock
class ThreadSafe
{
static bool done;
static readonly object locker = new object();
static void Main()
{
new Thread (Go).Start();
Go();
}
static void Go()
{
//使用lock,确保一次只有一个线程执行该代码
lock (locker)
{
if (!done) { Console.WriteLine ("Done"); done = true; }
}
}
}
当多个线程都在争取这个排它锁时,一个线程获取该锁,其他线程会处于blocked状态(该状态时不消耗cpu),等待另一个线程释放锁时,捕获该锁。这就保证了一次
只有一个线程执行该代码。
Join可以实现暂停另一个线程,直到调用Join方法的线程结束。
static void Main()
{
Thread t = new Thread (Go);
t.Start();
t.Join();
Console.WriteLine ("Thread t has ended!");
}
static void Go()
{
for (int i = 0; i < 1000; i++) Console.Write ("y");
}
//输出:
//yyyyyy..... Thread t has ended!
线程t调用Join方法,阻塞主线程,直到t线程执行结束,再执行主线程。
Sleep:暂停该线程一段时间
Thread.Sleep (TimeSpan.FromHours (1)); // 暂停一个小时
Thread.Sleep (500); // 暂停500毫秒
Join是暂停别的线程,Sleep是暂停自己线程。
上面的例子是使用Thread类的构造函数,给构造函数传入一个ThreadStart委托。来实现的。
public delegate void ThreadStart();
然后调用Start方法,来执行该线程。委托执行完该线程也结束。
上面的例子是使用Thread类的构造函数,给构造函数传入一个ThreadStart委托。来实现的。
public delegate void ThreadStart();
然后调用Start方法,来执行该线程。委托执行完该线程也结束。
如:
class ThreadTest
{
static void Main()
{
Thread t = new Thread (new ThreadStart (Go));
t.Start(); // 执行Go方法
Go(); // 同时在主线程上执行Go方法
}
static void Go()
{
Console.WriteLine ("hello!");
}
}
多数情况下,可以不用new ThreadStart委托。直接在构造函数里传入void类型的方法。
Thread t = new Thread (Go);
使用lambda表达式
static void Main()
{
Thread t = new Thread ( () => Console.WriteLine ("Hello!") );
t.Start();
}
默认情况下创建的线程都是Foreground,只要有一个Foregournd线程在执行,应用程序就不会关闭。
Background线程则不是。一旦Foreground线程执行完,应用程序结束,background就会强制结束。
可以用IsBackground来查看该线程是什么类型的线程。
总结:前台线程执行完,程序结束,后台线程被强制结束
public static void Main()
{
try
{
new Thread (Go).Start();
}
catch (Exception ex)
{
// 不能捕获异常
Console.WriteLine ("Exception!");
}
}
static void Go() { throw null; } //抛出 Null异常
此时并不能在Main方法里捕获线程Go方法的异常,如果是Thread自身的异常可以捕获。
正确捕获方式:
public static void Main()
{
new Thread (Go).Start();
}
static void Go()
{
try
{
// ...
throw null; // 这个异常会被下面捕获
// ...
}
catch (Exception ex)
{
// ...
}
}
总结:不能通过捕获线程的异常捕获方法内异常
使用线程的理由
简要 1 可响应性:1、 GUI线程能灵敏相应 2、多核能真正提示性能
线程的主要几点性能影响:
简要: 1 、创建,销毁昂贵 2 、线程上下文切换性能开销 3 、GC回收挂起所有线程
把需要执行的代码提交到线程池,线程池内部会安排一个空闲的线程来执行你的代码。
ThreadPool.QueueUserWorkItem(t => Console.WriteLine("Hello thread pool"));
每个CLR都有一个线程池,线程池在CLR内可以多个AppDomain共享,线程池是CLR内部管理的一个线程集合,初始是没有线程的,在需要的时候才会创建。线程池的主要结构图如下图所示,基本流程如下:
简要 : 1、内部维护请求队列缓存请求执行代码 2 新任务时用空闲或新线程 3 使用完不会销毁 4 大量闲线程,结束部分。
设置线程池的最大活跃线程数,调用方法ThreadPool.SetMaxThreads
线程池使得线程可以充分有效地被利用,减少了任务启动的延迟,也不用大量的去创建线程,避免了大量线程的创建和销毁对性能的极大影响。
线程池解决问题:大量线程创建和销毁性能影响。
线程池的不足:
简要总结 1 不支持取消,挂起 2 无返回值,不知道结束时间 3 不支持优先级。
使用线程池创建线程的方法:
任务Task基于线程池,可支持返回值,支持比较强大的任务执行计划定制等功能。
Task的用法
无返回值的方式
方式1: var t1 = new Task(() => TaskMethod("Task 1"));
t1.Start();
Task.WaitAll(t1);//等待所有任务结束
任务的状态:Start之前为:CreatedStart之后为:WaitingToRun方式2: Task.Run(() => TaskMethod("Task 2"));
方式3: Task.Factory.StartNew(() => TaskMethod("Task 3")); 直接异步的方法
或者 var t3=Task.Factory.StartNew(() => TaskMethod("Task 3"));
Task.WaitAll(t3);//等待所有任务结束
任务的状态:Start之前为:Running Start之后为:Running
传递Action委托,或对应的委托方法,启动任务
Task的使用:
//标记为长时间运行任务,则任务不会使用线程池,而在单独的线程中运行。
Task.Factory.StartNew(() => TaskMethod("Task 5"), TaskCreationOptions.LongRunning);
async/await的实现方式:
async static void AsyncFunction()
{
await Task.Delay(1);
for (int i = 0; i < 10; i++)
{
Console.WriteLine(string.Format("AsyncFunction:i={0}", i));
}
}
待返回值:
Task task = CreateTask("Task 1");
int result = task.Result;
//定制一个延续任务计划
//创建一个任务
Task task = new Task(() =>{ });
Task cwt = task.ContinueWith(t =>
{
Console.WriteLine("任务完成后的执行结果:{0}", t.Result.ToString());
});
TPL(Task Parallel Library) 并行编程
并行的特点Task Parallel Library 内部有 Task实例, 所有任务完成返回,少量但时间任务不要使用并行,因为里面也有性能开销,任务的调度,创建方法的委托
如果多线程同时运行,访问相同的数据,就很容易出现问题,必须实现同步机制。
任务并行性和数据并行性。
Parallel.For()和Parallel.ForEach()方法在每次迭代中调用相同的代码,而Parallel.Invoke()方法允许同时调用不同的方法。Parallel.For用于任务并行性,而Parallel.ForEach用于数据并行性。
ParallelLoopResult result = Parallel.For(0, 10, i =>
{
Console.WriteLine("{0},task:{1},thread:{2}", i
, Task.CurrentId, Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(10);
// Task.Delay(100);//是一个异步方法,用于释放线程供其他任务使用。
});
程序的执行依次是:0-2-3-1-4-6-8-7-5-9;共有五个任务和五个线程。任务不一定映射到一个线程上,线程也可以被不同的任务重用
Task.Delay是一个异步方法,用于释放线程提供其他任务使用。
也可以提前中断Parallel.For()方法吗,而不是完成所有迭代。For()方法有一个重载接受Action
ParallelLoopResult asyncResult = Parallel.For(10, 40, async (int i,ParallelLoopState pls ) => {
Console.WriteLine("{task:"}
await Task.Delay(10);
if (i > 10)
pls.Break();});
Parallel.ForEach()方法遍历实现了IEnumerable的集合,其方式类似于foreach语句,但以异步方法遍历。
string[] data = { "aaa", "bbb", "ccc", "ddd", "eee", "fff", "asss" };
ParallelLoopResult resule = Parallel.ForEach(data, s =>
{ Console.WriteLine(s); });
通过Parallel.Invoke()方法调用多个方法
Parallel.Invoke(Foo,Bar);
扩展方法:
lambda表达式创建委托和表达式树的匿名方法
() => TaskMethod("Task 3") //只有一个语句省略花括号{},没有参数可以写小括号()
Async方式,使用Async标记为异步方法,用Await标记表示方法内需要耗时的操作。主线程碰到await时会立即返回,继续以非阻塞形式执行主线程下面的逻辑。当await耗时操作完成时,继续执行Async1下面的逻辑
Net提供的异步方式基本上一脉相承的,如:
1. net4.5的Async,抛去语法糖就是Net4.0的Task+状态机。
2. net4.0的Task, 退化到3.5即是(Thread、ThreadPool)+实现的等待、取消等API操作。
Task.WaitAll 是阻塞当前线程等待其它任务完毕的意思;Task.WhenAll 是创建一个异步(不阻塞当前线程)的Task,
//等待两个任务执行完成(同时执行),再执行主线程
Task.WaitAll(task1, task2);
System.ICloneable接口支持克隆,即用与现有实例相同的值创建类的新实例
volatile的作用是: 作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值.
GUI应用程序(主要是Winform、WPF)引入了一个特殊线程处理模型,UI控件元素只能由创建它的线程访问或修改,微软这样处理是为了保证UI控件的线程安全。
UI线程中执行一个耗时的计算操作,会导致UI假死的原因
Windows是基于消息机制的,我们在UI上所有的键盘、鼠标操作都是以消息的形式发送给各个应用程序的。GUI线程内部就有一个消息队列,GUI线程不断的循环处理这些消息,并根据消息更新UI的呈现。如果这个时候,你让GUI线程去处理一个耗时的操作(比如花10秒去下载一个文件),那GUI线程就没办法处理消息队列了,UI界面就处于假死的状态。
线程里处理事件完成后,需要更新UI控件的状态:
① 使用GUI控件提供的方法,Winform是控件的Invoke方法,WPF中是控件的Dispatcher.Invoke方法
//1.Winform:Invoke方法和BeginInvoke
this.label.Invoke(method, null);
//2.WPF:Dispatcher.Invoke
this.label.Dispatcher.Invoke(method, null);
② 使用.NET中提供的BackgroundWorker执行耗时计算操作,在其任务完成事件RunWorkerCompleted 中更新UI控件
using (BackgroundWorker bw = new BackgroundWorker())
{
bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler((ojb,arg) =>
{
this.label.Text = "anidng";
});
bw.RunWorkerAsync();
}
③ 看上去很高大上的方法:使用GUI线程处理模型的同步上下文来送封UI控件修改操作,这样可以不需要调用UI控件元素
.NET中提供一个用于同步上下文的类SynchronizationContext,利用它可以把应用程序模型链接到他的线程处理模型,其实它的本质还是调用的第一步①中的方法。
实现代码分为三步,第一步定义一个静态类,用于GUI线程的UI元素访问封装:
public static class GUIThreadHelper
{
public static System.Threading.SynchronizationContext GUISyncContext
{
get { return _GUISyncContext; }
set { _GUISyncContext = value; }
}
private static System.Threading.SynchronizationContext _GUISyncContext =
System.Threading.SynchronizationContext.Current;
///
/// 主要用于GUI线程的同步回调
///
///
public static void SyncContextCallback(Action callback)
{
if (callback == null) return;
if (GUISyncContext == null)
{
callback();
return;
}
GUISyncContext.Post(result => callback(), null);
}
///
/// 支持APM异步编程模型的GUI线程的同步回调
///
public static AsyncCallback SyncContextCallback(AsyncCallback callback)
{
if (callback == null) return callback;
if (GUISyncContext == null) return callback;
return asynresult => GUISyncContext.Post(result => callback(result as IAsyncResult), asynresult);
}
}
第二步,在主窗口注册当前SynchronizationContext:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
CLRTest.ConsoleTest.GUIThreadHelper.GUISyncContext = System.Threading.SynchronizationContext.Current;
}
第三步,就是使用了,可以在任何地方使用
GUIThreadHelper.SyncContextCallback(() =>
{
this.txtMessage.Text = res.ToString();
this.btnTest.Content = "DoTest";
this.btnTest.IsEnabled = true;
});
委托异步可以返回任意类型个数的值。
使用委托异步的方式:
如:
如:
static void Main()
{
Func method = Work;
IAsyncResult cookie = method.BeginInvoke ("test", null, null);
//
// ... 此时可以同步处理其他事情
//
int result = method.EndInvoke (cookie);
Console.WriteLine ("String length is: " + result);
}
static int Work (string s) { return s.Length; }
使用回调函数来简化委托的异步调用,回调函数参数为IAsyncResult类型
static void Main()
{
Func method = Work;
method.BeginInvoke ("test", Done, method);
// ...
//并行其他事情
}
static int Work (string s) { return s.Length; }
static void Done (IAsyncResult cookie)
{
var target = (Func) cookie.AsyncState;
int result = target.EndInvoke (cookie);
Console.WriteLine ("String length is: " + result);
}
使用匿名方法
Func f = s => { return s.Length; };
f.BeginInvoke("hello", arg =>
{
var target = (Func)arg.AsyncState;
int result = target.EndInvoke(arg);
Console.WriteLine("String length is: " + result);
}, f);
委托异步:1 声明委托 2 调用BeginInvoke返回IAsyncResult 3 调用EndInvoke传递IAsyncResulte值得最终值
还可以使用回调函数:2参数(回调函数传递IAsyncResult )3参数(传递值AsyncState)
委托可以有任意个传入和输出参数。以Action,Func来举例。
EndInvoke做了三件事情:
Task泛型允许有返回值。
如:
static void Main()
{
// 创建Task并执行
Task task = Task.Factory.StartNew
( () => DownloadString ("http://www.baidu.com") );
// 同时执行其他方法
Console.WriteLine("begin");
//等待获取返回值,并且不会阻塞主线程
Console.WriteLine(task.Result);
Console.WriteLine("end");
}
static string DownloadString (string uri)
{
using (var wc = new System.Net.WebClient())
return wc.DownloadString (uri);
task运行有返回值:泛型声明类型,Result获取
在临界资源的门口加一把锁,来控制多个线程对临界资源的访问。(概念)
但在实际开发中,根据资源类型不同、线程访问方式的不同,有多种锁的方式或控制机制(基元用户模式构造和基元内核模式构造)。.NET提供了两种线程同步的构造模式,需要理解其基本原理和使用方式。
基元线程同步构造分为:基元用户模式构造和基元内核模式构造,两种同步构造方式各有优缺点,而混合构造(如lock)就是综合两种构造模式的优点。
基元用户模式比基元内核模式速度要快,她使用特殊的cpu指令来协调线程,在硬件中发生,速度很快。但也因此Windows操作系统永远检测不到一个线程在一个用户模式构造上阻塞了。举个例子来模拟一下用户模式构造的同步方式:
缺点?线程2会一直使用CPU时间(假如当前系统只有这两个线程在运行),也就意味着不仅浪费了CPU时间,而且还会有频繁的线程上下文切换,对性能影响是很严重的。
当然她的优点是效率高,适合哪种对资源占用时间很短的线程同步。.NET中为我们提供了两种原子性操作,利用原子操作可以实现一些简单的用户模式锁(如自旋锁)。
System.Threading.Interlocked:易失构造,它在包含一个简单数据类型的变量上执行原子性的读或写操作。
Thread.VolatileRead 和 Thread.VolatileWrite:互锁构造,它在包含一个简单数据类型的变量上执行原子性的读和写操作。
以上两种原子性操作的具体内涵这里就细说了(有兴趣可以去研究文末给出的参考书籍或资料),针对题目11,来看一下题目代码:
int a = 0;
System.Threading.Tasks.Parallel.For(0, 100000, (i) =>
{
a++;
});
Console.Write(a);
上面代码是通过并行(多线程)来更新共享变量a的值,结果肯定是小于等于100000的,具体多少是不稳定的。解决方法,可以使用我们常用的Lock,还有更有效的就是使用System.Threading.Interlocked提供的原子性操作,保证对a的值操作每一次都是原子性的:
System.Threading.Interlocked.Add(ref a, 1);//正确
下面的图是一个简单的性能验证测试,分别使用Interlocked、不用锁、使用lock锁三种方式来测试。不用锁的结果是95,这答案肯定不是你想要的,另外两种结果都是对的,性能差别却很大。
为了模拟耗时操作,对代码稍作了修改,如下,所有的循环里面加了代码Thread.Sleep(20);。如果没有Thread.Sleep(20);他们的执行时间是差不多的。
System.Threading.Tasks.Parallel.For(0, 100, (i) =>
{
lock (_obj)
{
a++; //不正确
Thread.Sleep(20);
}
});
这是针对用户模式的一个补充,先模拟一个内核模式构造的同步流程来理解她的工作方式:
看上去是不是非常棒!彻底解决了用户模式构造的缺点,但内核模式也有缺点的:将线程从用户模式切换到内核模式(或相反)导致巨大性能损失。调用线程将从托管代码转换为内核代码,再转回来,会浪费大量CPU时间,同时还伴随着线程上下文切换,因此尽量不要让线程从用户模式转到内核模式。
简单总结,切换导致性能损失,托管代码会转化为内核代码,并有上下文切换。
她的优点就是阻塞线程,不浪费CPU时间,适合那种需要长时间占用资源的线程同步。
内核模式构造的主要有两种方式,以及基于这两种方式的常见的锁:
用户模式和内核模式区别,用户是不停询问资源是否可用 内核是被要求睡眠,线程可用唤醒。
既然内核模式和用户模式都有优缺点,混合构造就是把两者结合,充分利用两者的优点,把性能损失降到最低。大概的思路很好理解,就是如果是在没有资源竞争,或线程使用资源的时间很短,就是用用户模式构造同步,否则就升级到内核模式构造同步,其中最典型的代表就是Lock了。
lock的本质就是使用的Monitor,lock只是一种简化的语法形式,实质的语法形式如下:
bool lockTaken = false;
try
{
Monitor.Enter(obj, ref lockTaken);
//...
}
finally
{
if (lockTaken) Monitor.Exit(obj);
}
那lock或Monitor需要锁定的那个对象是什么呢?注意这个对象才是锁的关键,在此之前,需要先回顾一下引用对象的同步索引块(AsynBlockIndex),这是前面文章中提到过的引用对象的标准配置之一(还有一个是类型对象指针TypeHandle),它的作用就在这里了。
同步索引块是.NET中解决对象同步问题的基本机制,该机制为每个堆内的对象(即引用类型对象实例)分配一个同步索引,她其实是一个地址指针,初始值为-1不指向任何地址。
因此,锁对象要求必须为一个引用对象(在堆上)。
先还是尽量避免线程同步,不管使用什么方式都有不小的性能损失。一般情况下,大多使用Lock,这个锁是比较综合的,适应大部分场景。在性能要求高的地方,或者根据不同的使用场景,可以选择更符合要求的锁。
在使用Lock时,关键点就是锁对象了,需要注意以下几个方面:
(1)原子操作(Interlocked):所有方法都是执行一次原子读取或一次写入操作。 (用户锁)
(2)lock语句:避免锁定public类型,否则实例将超出代码控制的范围,定义private对象来锁定。(混合锁)
(3)Monitor实现线程同步
通过Monitor.Enter() 和 Monitor.Exit()实现排它锁的获取和释放,获取之后独占资源,不允许其他线程访问。
还有一个TryEnter方法,请求不到资源时不会阻塞等待,可以设置超时时间,获取不到直接返回false。 (混合锁)
(4)ReaderWriterLock
当对资源操作读多写少的时候,为了提高资源的利用率,让读操作锁为共享锁,多个线程可以并发读取资源,而写操作为独占锁,只允许一个线程操作。
(5)事件(Event)类实现同步(内核锁)
事件类有两种状态,终止状态和非终止状态,终止状态时调用WaitOne可以请求成功,通过Set将时间状态设置为终止状态。
(6)信号量(Semaphore)(内核锁)
信号量是由内核对象维护的int变量,为0时,线程阻塞,大于0时解除阻塞,当一个信号量上的等待线程解除阻塞后,信号量计数+1。
线程通过WaitOne将信号量减1,通过Release将信号量加1,使用很简单。
(7)互斥体(Mutex)
独占资源,用法与Semaphore相似。
(8)跨进程间的同步
通过设置同步对象的名称就可以实现系统级的同步,不同应用程序通过同步对象的名称识别不同同步对象。
一 volatile关键字
它只能在变量一级做同步,volatile的含义就是告诉处理器, 不要将我放入工作内存, 请直接在主存操作我。 当多线程同时访问该变量时,都将直接操作主存,从本质上做到了变量共享。
二、lock关键字
lock是一种比较好用的简单的线程同步方式,它是通过为给定对象获取互斥锁来实现同步的。它可以保证当一个线程在关键代码段的时候,另一个线程不会进来,它只能等待,等到那个线程对象被释放,也就是说线程出了临界区。
三、System.Threading.Interlocked
对于整数数据类型的简单操作,可以用 Interlocked 类的成员来实现线程同步,存在于System.Threading命名空间。Interlocked类有以下方法:Increment , Decrement ,Exchange 和CompareExchange 。使用Increment 和Decrement 可以保证对一个整数的加减为一个原子操作。Exchange 方法自动交换指定变量的值。CompareExchange 方法组合了两个操作:比较两个值以及根据比较的结果将第三个值存储在其中一个变量中。
int i = 0 ;
System.Threading.Interlocked.Increment( ref i);
System.Threading.Interlocked.Decrement( ref i);
System.Threading.Interlocked.Exchange( ref i, 100 );
System.Threading.Interlocked.CompareExchange( ref i, 10 , 100 );
四、Monitor
Monitor类提供了与lock类似的功能,不过与lock不同的是,它能更好的控制同步块,当调用了Monitor的Enter(Object o)方法时,会获取o的独占权,直到调用Exit(Object o)方法时,才会释放对o的独占权,可以多次调用Enter(Object o)方法,只需要调用同样次数的Exit(Object o)方法即可,Monitor类同时提供了TryEnter(Object o,[int])的一个重载方法,该方法尝试获取o对象的独占权,当获取独占权失败时,将返回false。
但使用 lock 通常比直接使用 Monitor 更可取,一方面是因为 lock 更简洁,另一方面是因为 lock 确保了即使受保护的代码引发异常,也可以释放基础监视器。这是通过 finally中调用Exit来实现的。事实上,lock 就是用 Monitor 类来实现的。下面两段代码是等效的:
lock (x)
{
DoSomething();
}
等效于
object obj = ( object )x;
System.Threading.Monitor.Enter(obj);
try
{
DoSomething();
}
finally
{
System.Threading.Monitor.Exit(obj);
}
用法:
Code
private static object m_monitorObject = new object ();
[STAThread]
static void Main( string [] args)
{
Thread thread = new Thread( new ThreadStart(Do));
thread.Name = " Thread1 " ;
Thread thread2 = new Thread( new ThreadStart(Do));
thread2.Name = " Thread2 " ;
thread.Start();
thread2.Start();
thread.Join();
thread2.Join();
Console.Read();
}
static void Do()
{
if ( ! Monitor.TryEnter(m_monitorObject))
{
Console.WriteLine( " Can't visit Object " + Thread.CurrentThread.Name);
return ;
}
try
{
Monitor.Enter(m_monitorObject);
Console.WriteLine( " Enter Monitor " + Thread.CurrentThread.Name);
Thread.Sleep( 5000 );
}
finally
{
Monitor.Exit(m_monitorObject);
}
}
当线程1获取了m_monitorObject对象独占权时,线程2尝试调用TryEnter(m_monitorObject),此时会由于无法获取独占权而返回false
Monitor还提供了三个静态方法Monitor.Pulse(Object o),Monitor.PulseAll(Object o)和Monitor.Wait(Object o ) ,用来实现一种唤醒机制的同步。
五、Mutex
在使用上,Mutex与上述的Monitor比较接近,不过Mutex不具备Wait,Pulse,PulseAll的功能,因此,我们不能使用Mutex实现类似的唤醒的功能。不过Mutex有一个比较大的特点,Mutex是跨进程的,因此我们可以在同一台机器甚至远程的机器上的多个进程上使用同一个互斥体。尽管Mutex也可以实现进程内的线程同步,而且功能也更强大,但这种情况下,还是推荐使用Monitor,因为Mutex类是win32封装的,所以它所需要的互操作转换更耗资源。
六、ReaderWriterLock
在考虑资源访问的时候,惯性上我们会对资源实施lock机制,但是在某些情况下,我们仅仅需要读取资源的数据,而不是修改资源的数据,在这种情况下获取资源的独占权无疑会影响运行效率,因此.Net提供了一种机制,使用ReaderWriterLock进行资源访问时,如果在某一时刻资源并没有获取写的独占权,那么可以获得多个读的访问权,单个写入的独占权,如果某一时刻已经获取了写入的独占权,那么其它读取的访问权必须进行等待,参考以下代码:
Code
private static ReaderWriterLock m_readerWriterLock = new ReaderWriterLock();
private static int m_int = 0;
[STAThread]
static void Main(string[] args)
{
Thread readThread = new Thread(new ThreadStart(Read));
readThread.Name = "ReadThread1";
Thread readThread2 = new Thread(new ThreadStart(Read));
readThread2.Name = "ReadThread2";
Thread writeThread = new Thread(new ThreadStart(Writer));
writeThread.Name = "WriterThread";
readThread.Start();
readThread2.Start();
writeThread.Start();
readThread.Join();
readThread2.Join();
writeThread.Join();
Console.ReadLine();
}
private static void Read()
{
while (true)
{
Console.WriteLine("ThreadName " + Thread.CurrentThread.Name + " AcquireReaderLock");
m_readerWriterLock.AcquireReaderLock(10000);
Console.WriteLine(String.Format("ThreadName : {0} m_int : {1}", Thread.CurrentThread.Name, m_int));
m_readerWriterLock.ReleaseReaderLock();
}
}
private static void Writer()
{
while (true)
{
Console.WriteLine("ThreadName " + Thread.CurrentThread.Name + " AcquireWriterLock");
m_readerWriterLock.AcquireWriterLock(1000);
Interlocked.Increment(ref m_int);
Thread.Sleep(5000);
m_readerWriterLock.ReleaseWriterLock();
Console.WriteLine("ThreadName " + Thread.CurrentThread.Name + " ReleaseWriterLock");
}
}
在程序中,我们启动两个线程获取m_int的读取访问权,使用一个线程获取m_int的写入独占权,执行代码后,输出如下:
可以看到,当WriterThread获取到写入独占权后,任何其它读取的线程都必须等待,直到WriterThread释放掉写入独占权后,能获取到数据的访问权,应该注意的是,上述打印信息很明显显示出,可以多个线程同时获取数据的读取权,这从ReadThread1和ReadThread2的信息交互输出可以看出。
七、SynchronizationAttribute
当我们确定某个类的实例在同一时刻只能被一个线程访问时,我们可以直接将类标识成Synchronization的,这样,CLR会自动对这个类实施同步机制,实际上,这里面涉及到同步域的概念,当类按如下设计时,我们可以确保类的实例无法被多个线程同时访问
1). 在类的声明中,添加System.Runtime.Remoting.Contexts.SynchronizationAttribute属性。
2). 继承至System.ContextBoundObject
需要注意的是,要实现上述机制,类必须继承至System.ContextBoundObject,换句话说,类必须是上下文绑定的。
[System.Runtime.Remoting.Contexts.Synchronization]
public class SynchronizedClass : System.ContextBoundObject
{}
八、MethodImplAttribute
如果临界区是跨越整个方法的,也就是说,整个方法内部的代码都需要上锁的话,使用MethodImplAttribute属性会更简单一些。这样就不用在方法内部加锁了,只需要在方法上面加上 [MethodImpl(MethodImplOptions.Synchronized)] 就可以了,MehthodImpl和MethodImplOptions都在命名空间System.Runtime.CompilerServices 里面。但要注意这个属性会使整个方法加锁,直到方法返回,才释放锁。因此,使用上不太灵活。如果要提前释放锁,则应该使用Monitor或lock。我们来看一个例子:
Code
[MethodImpl(MethodImplOptions.Synchronized)]
public void DoSomeWorkSync()
{
Console.WriteLine( " DoSomeWorkSync() -- Lock held by Thread " +
Thread.CurrentThread.GetHashCode());
Thread.Sleep( 1000 );
Console.WriteLine( " DoSomeWorkSync() -- Lock released by Thread " +
Thread.CurrentThread.GetHashCode());
}
public void DoSomeWorkNoSync()
{
Console.WriteLine( " DoSomeWorkNoSync() -- Entered Thread is " +
Thread.CurrentThread.GetHashCode());
Thread.Sleep( 1000 );
Console.WriteLine( " DoSomeWorkNoSync() -- Leaving Thread is " +
Thread.CurrentThread.GetHashCode());
}
[STAThread]
static void Main( string [] args)
{
MethodImplAttr testObj = new MethodImplAttr();
Thread t1 = new Thread( new ThreadStart(testObj.DoSomeWorkNoSync));
Thread t2 = new Thread( new ThreadStart(testObj.DoSomeWorkNoSync));
t1.Start();
t2.Start();
Thread t3 = new Thread( new ThreadStart(testObj.DoSomeWorkSync));
Thread t4 = new Thread( new ThreadStart(testObj.DoSomeWorkSync));
t3.Start();
t4.Start();
Console.ReadLine();
}
九、同步事件和等待句柄
用lock和Monitor可以很好地起到线程同步的作用,但它们无法实现线程之间传递事件。如果要实现线程同步的同时,线程之间还要有交互,就要用到同步事件。同步事件是有两个状态(终止和非终止)的对象,它可以用来激活和挂起线程。
同步事件有两种:AutoResetEvent和 ManualResetEvent。它们之间唯一不同的地方就是在激活线程之后,状态是否自动由终止变为非终止。AutoResetEvent自动变为非终止,就是说一个AutoResetEvent只能激活一个线程。而ManualResetEvent要等到它的Reset方法被调用,状态才变为非终止,在这之前,ManualResetEvent可以激活任意多个线程。
可以调用WaitOne、WaitAny或WaitAll来使线程等待事件。它们之间的区别可以查看MSDN。当调用事件的 Set方法时,事件将变为终止状态,等待的线程被唤醒。
来看一个例子,这个例子是MSDN上的。因为事件只用于一个线程的激活,所以使用 AutoResetEvent 或 ManualResetEvent 类都可以。
可以看出,对于线程1和2,也就是调用没有加属性的方法的线程,当线程2进入方法后,还没有离开,线程1有进来了,这就是说,方法没有同步。我们再来看看线程3和4,当线程3进来后,方法被锁,直到线程3释放了锁以后,线程4才进来。
Code
static AutoResetEvent autoEvent;
static void DoWork()
{
Console.WriteLine(" worker thread started, now waiting on event");
autoEvent.WaitOne();
Console.WriteLine(" worker thread reactivated, now exiting");
}
[STAThread]
static void Main(string[] args)
{
autoEvent = new AutoResetEvent(false);
Console.WriteLine("main thread starting worker thread");
Thread t = new Thread(new ThreadStart(DoWork));
t.Start();
Console.WriteLine("main thrad sleeping for 1 second");
Thread.Sleep(1000);
Console.WriteLine("main thread signaling worker thread");
autoEvent.Set();
Console.ReadLine();
}
在主函数中,首先创建一个AutoResetEvent的实例,参数false表示初始状态为非终止,如果是true的话,初始状态则为终止。然后创建并启动一个子线程,在子线程中,通过调用AutoResetEvent的WaitOne方法,使子线程等待指定事件的发生。然后主线程等待一秒后,调用AutoResetEvent的Set方法,使状态由非终止变为终止,重新激活子线程。
简单总结:
1 volatile操作主存,变量共享
2 lock关键字获取互斥锁,一个线程在,另一个等待释放
3 Interlocked 简单数据类型的线程同步
4 Monitor更好控制同步快,但lock简洁并保证异常会释放(enter获取,Exit释放,TryEnter获取失败false)
5 Mutex不能唤醒,但跨进程,因为win32封装,更耗资源
6 ReaderWriterLock 共享读取权,独占写入权
7 SynchronizationAttribute,对类实现同步机制,要继承ContextBoundObject,添加特性
8 MethodImplAttribute 对整个方法加锁
9 同步事件和等待句柄,同步并传递事件,同步事件是有两个状态(终止和非终止)的对象,它可以用来激活和挂起线程。
AutoResetEvent,自动变为非终止,ManualResetEvent,Reset方法被调用变为非终止。
因为GUI应用程序引入了一个特殊的线程处理模型,为了保证UI控件的线程安全,这个线程处理模型不允许其他子线程跨线程访问UI元素。解决方法还是比较多的,如:
上面几个方式在文中已详细给出。
应用程序必须运行完所有的前台线程才可以退出,或者主动结束前台线程,不管后台线程是否还在运行,应用程序都会结束;而对于后台线程,应用程序则可以不考虑其是否已经运行完毕而直接退出,所有的后台线程在应用程序退出时都会自动结束。
通过将 Thread.IsBackground 设置为 true,就可以将线程指定为后台线程,主线程就是一个前台线程。
常用的如如SemaphoreSlim、ManualResetEventSlim、Monitor、ReadWriteLockSlim,lock是一个混合锁,其实质是Monitor['mɒnɪtə]。
lock的锁对象要求为一个引用类型。她可以锁定值类型,但值类型会被装箱,每次装箱后的对象都不一样,会导致锁定无效。
对于lock锁,锁定的这个对象参数才是关键,这个参数的同步索引块指针会指向一个真正的锁(同步块),这个锁(同步块)会被复用。
多线程是实现异步的主要方式之一,异步并不等同于多线程。实现异步的方式还有很多,比如利用硬件的特性、使用进程或纤程等。在.NET中就有很多的异步编程支持,比如很多地方都有Begin***、End***的方法,就是一种异步编程支持,她内部有些是利用多线程,有些是利用硬件的特性来实现的异步编程。
优点:减小线程创建和销毁的开销,可以复用线程;也从而减少了线程上下文切换的性能损失;在GC回收时,较少的线程更有利于GC的回收效率。
缺点:线程池无法对一个线程有更多的精确的控制,如了解其运行状态等;不能设置线程的优先级;加入到线程池的任务(方法)不能有返回值;对于需要长期运行的任务就不适合线程池。
Mutex是一个基于内核模式的互斥锁,支持锁的递归调用,而Lock是一个混合锁,一般建议使用Lock更好,因为lock的性能更好。
public void DeadLockTest(int i)
{
lock (this) //或者lock一个静态object变量
{
if (i > 10)
{
Console.WriteLine(i--);
DeadLockTest(i);
}
}
}
不会的,因为lock是一个混合锁,支持锁的递归调用,如果你使用一个ManualResetEvent或AutoResetEvent可能就会发生死锁。
public static class Singleton where T : class,new()
{
private static T _Instance;
private static object _lockObj = new object();
///
/// 获取单例对象的实例
///
public static T GetInstance()
{
if (_Instance != null) return _Instance;
lock (_lockObj)
{
if (_Instance == null)
{
var temp = Activator.CreateInstance();
System.Threading.Interlocked.Exchange(ref _Instance, temp);
}
}
return _Instance;
}
}
int a = 0;
System.Threading.Tasks.Parallel.For(0, 100000, (i) =>
{
a++;
});
Console.Write(a);
输出结果不稳定,小于等于100000。因为多线程访问,没有使用锁机制,会导致有更新丢失。具体原因和改进在文中已经详细的给出了。
小总结
join和wait是实例方法,slepp是静态方法
参考文献:
http://www.cnblogs.com/lonelyxmas/p/9509298.html
https://blog.csdn.net/younghaiqing/article/details/56671607
https://www.cnblogs.com/mushroom/p/4575417.html
http://www.cnblogs.com/anding
https://www.cnblogs.com/1175429393wljblog/p/8183439.html
http://www.cnblogs.com/JoeSnail/p/6433290.html