多线程的概念看:VC API常用函数简单例子大全四-CSDN博客里的第三十九个函数。
现在直接看个例子,一个C#多线程的例子:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ThreadConls
{
class Program
{
static void Main(string[] args)
{
Program prg = new Program();
//线程的执行方法
System.Threading.ThreadStart tdStart = new System.Threading.ThreadStart(prg.OutNumber);
//创建一个线程对象
System.Threading.Thread thread = new System.Threading.Thread(tdStart);
//启动线程
thread.Start();
for (int i = 0; i < 10; i++)
{
//主线程睡眠500毫秒
System.Threading.Thread.Sleep(500);
Console.WriteLine("Main:{0}", i);
}
}
//线程函数
public void OutNumber()
{
for (int i = 10; i > 0; i--)
{
Console.WriteLine("OutNumber:{0}", i);
//这个线程睡眠500毫秒
System.Threading.Thread.Sleep(500);
}
}
}
}
Thread.Sleep()是使线程休眠的方法,如果它的参数为System.Threading.Timeout.Infinite将会使线程无限期的休眠下去,直到其它线程调用Interrupt方法,比如在主函数中调用thread.Interrupt();而 thread.Abort();方法可以使线程终止。
上面的的使线程休眠方法,只能是线程自己调用。也就是说,一个线程无法调用Sleep方法使另一个线程睡眠。
而 thread.Suspend();方法则没这个限制,当然这个方法只能使线程挂起,它不指定挂起多少时间。一个线程可以调用thread.Suspend();方法使另一个线程暂停。调用 thread.Resume()方法可以使唤醒线程使线程继续执行。thread.Resume()对应着thread.Suspend()方法
线程同步
为什么要线程同步呢?比如一个线程正读取一个文件,而另一个线程正好往这个文件里写入数据,这样就容易发生错误,得不到正确的数据。那么变量也是一样,一个线程在修改变量的值,而另一个线程正读取这个变量。这样读取的变量值就不具有唯一性,可能是修改前的值,有可能是修改后的值。为了防止这种情况发生。就需要线程同步了。
从某一方面来说,线程同步禁止了多线程的功能,使多线程的优势丧失了。如非必要,不要使用。
看一个没有使用线程同步的例子:按道理我输出的数永远不会为负数。但结果不是。其实也看得出来是什么原因。
我只是故意找了一种这样的情况。一个简单的示例。说明线程同步的用处。不使用线程同步,会使你的判断出错,根据num变量。
代码示例:
class Program
{
public static int num = 0;
static void Main(string[] args)
{
//创建一个线程对象
System.Threading.Thread thread = new System.Threading.Thread(OutNumber);
//启动线程,执行线程函数
thread.Start();
Random ram = new Random();
while (true)
{
//产生-100至100以内的随机数
num = ram.Next(-100, 100);
//睡眠100毫秒
System.Threading.Thread.Sleep(100);
}
}
public static void OutNumber()
{
while (true)
{
//num大于0才输出
if (num > 0)
{
//睡眠200毫秒
System.Threading.Thread.Sleep(200);
Console.WriteLine(num);
}
}
}
}
使用ManualResetEvent类可以解决上面的问题,ManualResetEvent类的构造函数的参数为true,则表明这个对象初始为有信号状态。
为false无信号。
ManualResetEvent类的WaitOne();可以阻塞当前线程,依据ManualResetEvent对象当前有无信号。
也就是说,当调用WaitOne函数的对象没有信号时,WaitOne函数就会等待,一直到有信号。
而ManualResetEvent类的Set和Reset可以设置对象有无信号,Set设置为有信号。Reset设置为无信号。
看使用了ManualResetEvent类的例子,避免了输出负数:
class Program
{
//创建ManualResetEvent对象
public static System.Threading.ManualResetEvent restEvent = new System.Threading.ManualResetEvent(false);
public static int num = 0;
static void Main(string[] args)
{
//创建一个线程对象
System.Threading.Thread thread = new System.Threading.Thread(OutNumber);
//启动线程,执行线程函数
thread.Start();
Random ram = new Random();
while (true)
{
restEvent.WaitOne();//等待信号来临
//产生-100至100以内的随机数
num = ram.Next(-100, 100);
//睡眠100毫秒
System.Threading.Thread.Sleep(100);
}
}
public static void OutNumber()
{
while (true)
{
//设置为无信号
restEvent.Reset();
//num大于0才输出
if (num > 0)
{
//睡眠200毫秒
System.Threading.Thread.Sleep(200);
Console.WriteLine(num);
}
//设置为有信号
restEvent.Set();
}
}
}
另还有相关的类AutoResetEvent
接下来看另一种需要用到线程同步的情况,多个线程执行同一个函数。看下面例子:
class Program
{
static void Main(String[] args)
{
//创建两个线程对象,执行的都是threadFun函数
System.Threading.Thread thread1 = new System.Threading.Thread(threadFun);
System.Threading.Thread thread2 = new System.Threading.Thread(threadFun);
thread1.Start();
thread2.Start();
Console.ReadLine();
}
public static void threadFun()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("线程ID号为{0}的输出:{1}",
System.Threading.Thread.CurrentThread.ManagedThreadId, i);
System.Threading.Thread.Sleep(100);
}
//做一些其它事.......
}
}
我也不想举一些实际应用中的例子了,比如例子中有一个什么问题,然后用通过解决这个问题的方式来学习,或者引出一些知识点。
这里我举的例子,仅仅说明一些规则,比如Monitor类能保证一段代码在同一时间只能被一个线程执行。
另:上面那个输出负数的问题,也可以用Monitor类来解决。而且使用更方便。
看下例:
class Program
{
private static Object obj = new Object();
static void Main(String[] args)
{
//创建两个线程对象
System.Threading.Thread thread1 = new System.Threading.Thread(threadFun);
System.Threading.Thread thread2 = new System.Threading.Thread(threadFun);
thread1.Start();
thread2.Start();
Console.ReadLine();
}
public static void threadFun()
{
//锁定对象
System.Threading.Monitor.Enter(obj);
for (int i = 0; i < 10; i++)
{
Console.WriteLine("线程ID号为{0}的输出:{1}",
System.Threading.Thread.CurrentThread.ManagedThreadId, i);
System.Threading.Thread.Sleep(100);
}
//解锁
System.Threading.Monitor.Exit(obj);
//做一些其它事.......
}
}
Monitor.Enter锁定一个对象,当对象被锁定后,别的线程,再调用Monitor.Enter锁定同样的对象,就会被阻塞,因为对象已经被锁定了。
只能等锁定对象的线程调用Monitor.Exit(obj);解锁对象后,别的线程才可以接着锁定,进入里面执行代码。这样就可以保证包含在这两个函数的之间代码,在同一时间只能被一个线程执行。
而这个锁定对象只是一个标记,用这个对象作为可不可以执行代码的标记。
另外跟Monitor.Enter和Monitor.Exit类似的还有lock,这个lock实际上是对前者的一个包装。
比如一个函数里的代码段如下:
lock(obj) //锁定obj对象,其后大括号中的代码在同一时间只能被一个线程访问。
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("线程ID号为{0}的输出:{1}",
System.Threading.Thread.CurrentThread.ManagedThreadId, i);
System.Threading.Thread.Sleep(100);
}
}
上面的代码跟下面这个是一样的。
//锁定对象
System.Threading.Monitor.Enter(obj);
for (int i = 0; i < 10; i++)
{
Console.WriteLine("线程ID号为{0}的输出:{1}",
System.Threading.Thread.CurrentThread.ManagedThreadId, i);
System.Threading.Thread.Sleep(100);
}
//解锁
System.Threading.Monitor.Exit(obj);
需要了解的一些东西:
如果有三个线程执行了lock(obj);按执行lock(obj);的顺序,我们把这三个线程取名为第一线程,第二线程,第三线程。
当第一线程最先锁定了obj后,第二线程执行lock(obj);会发现obj已经被锁定了,那么这个线程就会到“就绪队列”中去,等待着obj被解锁。只要obj一被解锁,就轮到它来锁定对象,执行代码了。第三个线程也是如此(等待着第二个线程解锁obj)。
按照执行lock(obj)的先后顺序,就绪队列中的线程也是有个先后顺序,它们的顺序跟执行lock(obj)的顺序一致。然后一个个按顺序等待着锁定obj,执行lock对应的代码。
与“就绪队列”对应的还有个“等待队列”。等待队列是比就绪队列低一级的,等待队列中的线程,它们是等待着进入就绪列队,而就绪队列就是等待着锁定对象,执行代码。
什么情况下,线程会进入等待队列呢,调用Monitor.Wait方法,当然并不是任何线程都可以调用Monitor.Wait方法的,它只能是获得对象锁的线程。
为什么会有这个限制,看一下Monitor.Wait它的作用就知道了,Wait使当前线程放弃对象锁的拥有权,相当于临时解锁了(Monitor.Exit)。
并且使当前线程进入等待队列,阻塞当前线程。等待队列中的线程顺序跟线程调用Monitor.Wait先后顺序一致。就如同就绪队列一样。
但是如何使处于等待队列中的第一个线程,进入就绪队列呢。像就绪队列获得对象锁,只要另一个拥有对象锁的线程调用Monitor.Exit
释放就可以了。(lock语句执行完了,它暗地里也会调用的)。
使等待队列中的线程进入就绪队列中,有一个专门的函数可以做到。Monitor.Pluse函数。每调用一次,就可以等待队列中排在第一的线程进入就绪队列。当它再次拥有对象锁的时候,它就会从Monitor.Wait处开始执行。
而Monitor.PluseAll可以使等待队列中的所有线程进入就绪队列。
看下面这个例子就明白了:
class Program
{
private static Object obj = new Object();
static void Main(String[] args)
{
//创建两个线程对象
System.Threading.Thread thread1 = new System.Threading.Thread(thread1Fun);
System.Threading.Thread thread2 = new System.Threading.Thread(thread2Fun);
thread1.Start();
thread2.Start();
Console.ReadLine();
}
public static void thread1Fun()
{
//锁住obj
lock (obj)
{
Console.WriteLine("这是thread1的输出!");
//临时解锁,并阻塞当前线程,直到再次获得对象锁。
System.Threading.Monitor.Wait(obj);
Console.WriteLine("thread1获得对象锁了!");
}
//做一些其它事.......
}
public static void thread2Fun()
{
lock (obj)
{
Console.WriteLine("我有机会输出了,是thread1临时解锁了!");
//通知thread1(队列第一个),我要解锁了,你可以进入就绪队列
System.Threading.Monitor.Pulse(obj);
}
//做一些其它事
}
}
可以做个测试,如果把System.Threading.Monitor.Pulse(obj);这句删了,那么Console.WriteLine("thread1获得对象锁了!");就永远不会执行。因为这个线程调用了System.Threading.Monitor.Wait(obj);进入了等待队列中后,没有其它线程通知它进入就绪队列。
那么即使对象解锁了,它也拥有不了对象锁。它一直处于等待队列中等待。
而从("thread1获得对象锁了!");这个字符串在("我有机会输出了,是thread1临时解锁了!");之后输出就可以看得出。
线程1临时解锁了,让线程2有机会输出字符串了,然后线程2再调用Pulse通知线程1进入就绪队列中。这样线程1才能输出第二个字符串。
线程池
直接看一个简单的例子吧,内地里也是创建了线程。像是封装了。
class Program
{
static void Main(String[] args)
{
System.Threading.ThreadPool.QueueUserWorkItem(OutNumber1, 10);
System.Threading.ThreadPool.QueueUserWorkItem(OutNumber2, 15);
Console.ReadLine();
}
static void OutNumber1(object obj)
{
int num = (int)obj;
for (int i = 0; i < num; i++)
{
Console.WriteLine("线程ID{0}的输出:{1}", System.Threading.Thread.CurrentThread.ManagedThreadId, i);
System.Threading.Thread.Sleep(150);
}
}
static void OutNumber2(object obj)
{
int num = (int)obj;
for (int i = 0; i < num; i++)
{
Console.WriteLine("线程ID{0}的输出:{1}", System.Threading.Thread.CurrentThread.ManagedThreadId,i);
System.Threading.Thread.Sleep(150);
}
}
}
线程池更进一步的了解参考其它资料吧。
定时器的例子:
class Program
{
public static int time = 0;
static void Main(String[] args)
{
//每隔1秒执行一次OutTime函数
System.Threading.Timer timer = new System.Threading.Timer(OutTime, 0, 0, 1000);
Console.ReadLine();
}
static void OutTime(Object obj)
{
time++;
Console.WriteLine("已过去了{0}秒", time);
}
}
Timer构造函数,第一个参数指定要执行的函数名,第二个是传进去的对象,对应着执行函数的Object参数。可以自行定义。
第三个是创建好的定时器后,延迟多久执行函数。最后一个参数是每隔多久执行一次OutTime函数。