线程的容器——线程池
1、概念:线程池,就是一个可以容纳多个线程的容器,其中的线程可以反复利用。
2、定义或使用线程池
ThreadPool.QueueUserWorkItem();
3、为什么要引入线程池?
虽然说创建和使用线程是比较简单,但是线程的创建和销毁需要耗费一定的开销,过多使用线程反而会造成内存资源的浪费,影响性能,便有了线程池这个概念。
4、线程池并不是在CLR初始化的时候就建立的,而是在程序需要创建线程来执行任务的时候才会初始化一个线程,并线程在完成了任务后就会以挂起的状态回到线程池,不会自行销毁,等到再需要的时候重新激活。这样就可以减少线程创建和销毁所带来的耗费。并且线程池里面的线程默认为后台线程,每个线程所占用的内存为1M。
比较创建线程和调用线程池里面的线程完成一个任务所需要的时间,体现线程池的一个优点。(分别使用创建线程和调用线程池里面的线程两种方法进行循环1000次)
class Program
{
static void Main(string[] args)
{
//创建线程来执行任务
Stopwatch sw = new Stopwatch();//计时完成整个线程的时间
sw.Start();
for (int i = 1; i < 1000; i++)
{
Thread thread1 = new Thread(()=> {
int count = 0;
count++;
});
thread1.Start();
}
sw.Stop();
Console.WriteLine("运行创建线程所需时间"+sw.ElapsedMilliseconds);
sw.Restart();
//调用线程池中的线程
for (int i = 0; i < 1000; i++)
{
ThreadPool.QueueUserWorkItem(s =>
{
int count = 0;
count++;
//查看当前线程的ID
Console.WriteLine("当前线程ID为:" + Thread.CurrentThread.ManagedThreadId);
});
}
sw.Stop();
Console.WriteLine("运用线程池中线程所需要的时间:"+sw.ElapsedMilliseconds);
Console.ReadKey();
}
}
运行结果:
从运行结果中可以看出来,通过创建线程来完成相应任务所需要耗费的时间与从线程池当中调用线程来完成相应任务的时间相比要多很多。由此可以看出调用线程池相对与创建新的线程的优势。
并且从上面的运行结果中可以看出,在线程池当中被调用的线程也只有几个,都是几个线程在重复被调用,重复执行相应的任务。
线程池当中QueueUserWorkItem()方法参数详解
ThreadPool.QueueUserWorkItem(“回调方法名”,“Object state”)方法中参数有两个:
第一个为创建的回调方法,第二个为传递给回调方法的参数。
实例如下:
static void Main(string[] args)
{
Console.WriteLine("主线程ID ={0}",Thread.CurrentThread.ManagedThreadId);
//第一个参数为线程池线程执行的回调方法,第二个参数表示传递给回调方法参数
ThreadPool.QueueUserWorkItem(CallBackWorkItem);
ThreadPool.QueueUserWorkItem(CallBackWorkItem, "work");
Console.WriteLine("主线程退出");
Console.ReadLine();
}
//CallBackWorkItem回调方法创建
static void CallBackWorkItem(object state)//线程池当中QueueUserWorkItem()第二个参数的传递
{
Console.WriteLine("子线程执行");
if (state != null)
{
Console.WriteLine("ID = {0}:{1}" , Thread.CurrentThread.ManagedThreadId, state.ToString());
}
else
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
}
}
运行结果:
由运行结果可以看出来,线程的执行顺序并不是由程序员决定的,而是由CPU来决定的。
线程池调用非静态类方法,参数为自定义
//线程池调用非静态方法,参数为自定义类
class Program
{
static void Main(string[] args)
{
ThreadDemoClass democlass = new ThreadDemoClass();
ThreadPool.QueueUserWorkItem(democlass.Run1);
ThreadPool.QueueUserWorkItem(democlass.Run1,"张三");
UserInfo userinfo = new UserInfo();
userinfo.Name = "李四";
userinfo.Age = 19;
//使用委托绑定线程池要执行的方法(有参数,自定义类型的参数)
//将方法排入队列,在线程池变为可用是执行
ThreadPool.QueueUserWorkItem(democlass.Run2,userinfo);
Console.WriteLine("Main Thread working......");
Console.WriteLine("Main Thread id is " + Thread.CurrentThread.ManagedThreadId.ToString());
Console.ReadLine();
}
}
//创建一个新的类,并在类里面创建两个不一样方法
public class ThreadDemoClass
{
public void Run1(object obj)
{
string name = obj as string;
Console.WriteLine("child thread working.......");
Console.WriteLine("My name is " + name);
Console.WriteLine("child thread ID is " + Thread.CurrentThread.ManagedThreadId.ToString());
}
public void Run2(object obj)
{
UserInfo userinfo = (UserInfo)obj;
Console.WriteLine("child thread working.......");
Console.WriteLine("My name is " + userinfo.Name);
Console.WriteLine("I am "+ userinfo.Age + " years old this year.");
Console.WriteLine("child thread ID is " + Thread.CurrentThread.ManagedThreadId.ToString());
}
}
//定义一个自定义参数类
public class UserInfo
{
public string Name { get; set; }
public int Age { get; set; }
}
运行结果:
使用线程池建立的线程也可以选择传递参数或不传递参数,并且参数的值可以是值类型也可以是引用类型(包括自定义类型),看上面的两次运行结果可以知道,6,11,12线程在两次的执行当中都有运用到,说明第一次请求线程池的时候,线程池建立一个线程,当它执行完任务之后再以挂起的状态回到线程池,在后面的请求的时候,再次唤醒了这几个线程来执行任务。
并且多次运行后发现每次运行的输出内容的顺序不一样。(不只是线程池,前面的线程也一样)这就是非线程的安全问题,相当于没有秩序,不用排队,CPU为谁服务就执行谁。
概念:非线程安全是指多线程操作同一个对象可能会出现的问题。而线程安全则是多线程操作同一个对象不会有问题。
线程安全: 即多线程访问同一个对象时,实施加锁机制,即当一个线程访问某类的数据时,其他线程无法进行访问,知道当上一个线程完成访问后,其他线程才可以进行使用。这样就不会出现数据不一致或者数据污染。
线程不安全: 即不提供数据访问保护,前后有可能出现多个线程同时更改一个数据,造成所得到的数据是脏数据。
以下为一个非线程安全的一个实例:
class Program
{
static int num = 1;
static void Main(string[] args)
{
Stopwatch sw = new Stopwatch();
sw.Start();
for (int i = 0; i < 5; i++)
{
Thread thread = new Thread(Run);
thread.Start();
}
num++;
Console.WriteLine("num is "+ num);
Console.WriteLine("Main thread ID is "+ Thread.CurrentThread.ManagedThreadId );
sw.Stop();
Console.WriteLine("The Execution time is " + sw.ElapsedMilliseconds + "ms");
Console.ReadLine();
}
static void Run()
{
num++;
Console.WriteLine("num is "+ num);
Console.WriteLine("Child thread ID is "+ Thread.CurrentThread.ManagedThreadId);
}
}
运行结果:
从上面的结果我们可以看到,num的值是连续递增的,输出也是没有顺序的,而且每一次输出的值可能还是不一样的,这就是因为异步线程同时访问一个成员时造成的,所以这样的问题对于我们来说时不可控的。那么如果要做到线程安全,就需要用到线程同步的方法了。
线程同步的方法有很多,其中Thread.join()就是其中的一种。我们以上面的例子为例,在以下地方加入Thread.Join()。
for (int i = 0; i < 5; i++)
{
Thread thread = new Thread(Run);
thread.Start();
thread.Join();//简单线程同步处理
}
加入Thread.Join()后运行结果:
由以上的运行结果可以看得出来,num 的值在递增,并且输出的内容也是按顺序输出。这样就实现了简单的同步线程。Join()方法用于阻挡当前线程,直到它前面的线程完成。可是这同样实现了同步,但却也阻塞了主线程的继续执行,这样就跟单线程没有什么区别。所以就要换一种方法,那就是线程同步技术。
线程同步技术
线程同步技术是指多线程程序当中,为了保护后者线程,只有等待前者线程完成之后才能继续执行。所谓同步:是指在某一个时刻只有一个线程可以访问变量。如果不能确保对变量的访问时同步的,就会产生错误。C#为同步访问变量提供了一个非常简单的方式,即使用C#中的关键字Lock,它可以把一段代码定义为互斥段,而互斥端在同一时刻只允许一个线程进行访问,其他线程必须等待。Lock定义如下:
lock(expression)
{
statement_block;
}
expression:代表希望给跟踪的对象
statement_block:就是互斥代码段,这段代码在同一时刻只能被一个线程访问。
Lock使用注意事项:
1、Lock的参数必须时基于引用类型的对象,不要是基本类型,如bool,int等等,这些根本不能同步,因为lock的参数必须要是对象,如果是int类型,在转换的时候势必会发生装箱操作,使得每一次lock的参数都是一个新的不同的对象。
2、最好不要是public类型或者不受控制的的对象实例,因为这样会导致死锁。
3、最好不要字符串,使用lock同步,应保证lock是同一个对象,而字符串变量赋值并不是修改它,而是创建了新的对象,这样每个线程或者每个循环对象都不同,就无法达到同步。
4、常用的做法就是创建一个object对象,并且永不赋值,应lock一个不影响其他操作的私有对象。
下面就以上一段使用Join方法实现同步的代码使用lock方法进行修改,同样实现线程同步。
class Program
{
//实现线程同步的方法,常用就是创建静态object类型的参数作为lock的对象
private static object locker = new object();//建立一个lock的参数object对象,用来实现同步
static int num = 1;
static void Main(string[] args)
{
Stopwatch sw = new Stopwatch();
sw.Start();
for (int i = 0; i < 5; i++)
{
Thread thread = new Thread(Run);
thread.Start();
//thread.Join();//简单线程同步处理
}
num++;
Console.WriteLine("num is " + num);
Console.WriteLine("Main thread ID is " + Thread.CurrentThread.ManagedThreadId);
sw.Stop();
Console.WriteLine("The Execution time is " + sw.ElapsedMilliseconds + "ms");
Console.ReadLine();
}
//定义一个静态的方法
static void Run()
{
//添加lock方法,在需要不断被线程的进行访问的方法中添加
lock (locker)
{
num++;
Console.WriteLine("num is " + num);
Console.WriteLine("Child thread ID is " + Thread.CurrentThread.ManagedThreadId);
}
}
}
运行结果:
由运行结果可以看出来,使用lock方法一样也可以来实现线程的同步。以上是多线程对静态方法的调用,如果是对非静态的方法调用需要对以上代码做出以下修改:
1、将静态方法该为非静态方法,同时lock参数的object类型也需要由改为非静态
//将原来的静态方法static改为public
public void Run()
{
lock (locker)
{
num++;
Console.WriteLine("num is " + num);
Console.WriteLine("Child thread ID is " + Thread.CurrentThread.ManagedThreadId);
}
}
//将原来的static删掉
private object locker = new object();
static int num = 1;
static void Main(string[] args)
2、将非静态方法进行实例化并调用。
static void Main(string[] args)
{
//非静态方法进行实例化
Program program = new Program();
Stopwatch sw = new Stopwatch();
sw.Start();
for (int i = 0; i < 5; i++)
{
//调用实例化的方法
Thread thread = new Thread(program.Run);
thread.Start();
//thread.Join();//简单线程同步处理
}
以书店卖书为例子
1、先初步创建一个书店的类(class),并定义好一个卖书的流程,并建立线程来执行卖书的过程。
class Program
{
static void Main(string[] args)
{
//通过实例化来调用BookStore类当中的Sale方法
BookStore sale = new BookStore();
Thread t1 = new Thread(sale.Sale);
Thread t2 = new Thread(sale.Sale);
t1.Start();
t2.Start();
Console.ReadKey();
}
}
//定义了一个类BookStore
class BookStore
{
public int num = 1;
//定义了一个售卖的方法
public void Sale()
{
int temp = num;
if (temp > 0)
{
Thread.Sleep(1000);
num -= 1;
Console.WriteLine("售出一本书,还剩{0}本。",num);
}
else
{
Console.WriteLine("书本已经售完!");
}
}
}
运行结果:
从运行结果来看,两个线程同步访问共享资源,没有考虑到同步的问题,结果不正确(因为线程是同步的,线程t1还没有结束,线程t2就进来了,这个时候temp还没有执行-1的动作,还是大于0的,因此两个线程就都执行了第一个分支)。
考虑线程同步,改进后的代码:保证在同一时刻只有一个线程访问共享资源,以及保证前面的线程执行完成之后,后面的才能够继续访问共享资源。
修改后的代码:主要在公共资源部分加锁(lock)
class BookStore
{
//定义一个私有的Object类型的参数作为lock的对象。
private object locker = new object();
//书本所剩的数量
public int num = 1;
public void Sale()
{
//使用lock方法来对共享资源执行锁机制,
lock (locker)
{
int temp = num;
if (temp > 0)
{
Thread.Sleep(1000);
num -= 1;
Console.WriteLine("售出一本书,还剩{0}本。", num);
}
else
{
Console.WriteLine("书本已经售完!");
}
}
}
}