C#多线程的使用
想要创建C#的一个多线程,需要三步
一、编写入口函数
二、创建入口委托
三、创建线程
编写入口函数
作者认为入口函数就是用户想要用多线程实现的函数,比如运行一台电脑,我们想要下载一个游戏的同时听音乐,下载一个游戏就是一个入口函数,同样听音乐也是一个入口函数,这个入口函数需要用户自己定义及编写
创建入口委托
ThreadStart entryPoint(用户自己定义) = new ThreadStart(入口函数);
创建线程
Thread WorkThread = new Thread(entryPoint);
上面这个是相较于麻烦一点的创建线程的办法,同样可以采用另外一种简便的方法创建线程,使用匿名函数创建线程
Thread 用户自己定义名称 = new Thread(delegate()
{
入口函数代码
});
Thread ThreadA = new Thread(delegate ()
{
for (int i = 0; i <= 1000; i++)
{
if (i % 10 == 0)
{
Console.Write('A');
}
}
});
Thread ThreadB = new Thread(delegate ()
{
for (int i = 0; i < 1000; i++)
{
if (i % 10 == 0)
{
Console.Write('B');
}
}
});
ThreadA.Start();
ThreadB.Start();
线程的优先级
线程的优先级总共分为五个。普通线程的优先级默认 Normal;高一点的优先级:AboveNormal或Highest;低一点的优先级:BelowNormal或Lowest
优先级的设置代码
线程名称.Priority = ThreadPriority.AboveNormal/Highest/BelowNormal/Lowest;
//线程A
Thread ThreadA = new Thread(delegate ()
{
for (int i = 0; i <= 100000; i++)
{
if (i % 100 == 0)
{
Console.Write('A');
}
}
});
//线程B
Thread ThreadB = new Thread(delegate ()
{
for (int i = 0; i < 100000; i++)
{
if (i % 100 == 0)
{
Console.Write('B');
}
}
});
//改变线程优先级
ThreadA.Priority = ThreadPriority.AboveNormal; //将ThreadA的优先级比Normal高
ThreadB.Priority = ThreadPriority.BelowNormal; //将ThreadB的优先级比Normal低
ThreadA.Start();
ThreadB.Start();
//主线程
for (int i = 0; i < 100000; i++)
{
if (i % 100 == 0)
{
Console.Write('C');
}
}
运行结果
系统有限执行优先级较高的线程,但这只意味着优先级较高的线程占有更多的CPU时间,并不意味着一定要先执行完优先级较高的线程,才会执行优先级较低的线程。
线程的插入
线程的插入使用Join()方法,作者认为使用Join方法就是能够将两个交替执行的线程合并为顺序执行的线程,举一个例子,比如我要下两部电影《A》和《B》,我两部先同时下载,但是我突然想先看《A》,我就使用Join方法,先执行下载《A》。等下载完之后在下载《B》
//线程A
Thread ThreadA = new Thread(delegate ()
{
for (int i = 0; i <= 100000; i++)
{
if (i % 100 == 0)
{
Console.Write('A');
}
}
});
//线程B
Thread ThreadB = new Thread(delegate ()
{
for (int i = 0; i < 100000; i++)
{
if (i % 100 == 0)
{
Console.Write('B');
}
}
ThreadA.Join();
for(int i =0;i<100000;i++)
{
if(i%100==0)
{
Console.Write('b');
}
}
});
ThreadA.Start();
ThreadB.Start();
运行结果
线程的状态
线程有以下几种状态:
Unstarted:线程尚未开始运行
Running:线程正在正常运行
Suspended:线程已经被挂起
SuspendRequested:正在请求挂起线程,但还未来得及响应
WaitSleepJoin:由于调用Wait()、Sleep()、Join()方法而使线程处于阻塞状态
Stopped:线程已经停止
StopRequested:已调用了Abort()方法,但还未收到ThreadAbortException异常
Aborted:线程处于Stopped状态中
Background:线程在后台执行
线程的同步
在引入线程同步概念之前,先引入一个无关线程和相关线程的概念。
无关线程:线程运行的时候相互之间没有任何联系,各自独立运行,互不干扰;
相关线程:线程运行的时候相互之间存在联系,比如一个线程等待另一个线程的运算结果,两个线程共享一个资源等。
临界资源:多个线程共享的资源称为临界资源
临界区:访问临界资源的代码称为临界区
//缓冲区,只能容纳一个字符
private static char buffer;
private static int count;
static void Main(string[] args)
{
//线程:写者
Thread Writer = new Thread(delegate ()
{
string sentence = "这是一个测试多线程同步的例子。";
count = sentence.Length;
for (int i = 0; i < sentence.Length; i++)
{
buffer = sentence[i]; //向缓冲区写入数据
Thread.Sleep(26);
}
});
//线程:读者
Thread Reader = new Thread(delegate ()
{
for (int i = 0; i < count; i++)
{
char ch = buffer;
Console.Write(ch);
Thread.Sleep(20);
}
});
Writer.Start();
Reader.Start();
互锁
上面的例子,在实现过程中,有可能像缓冲区写入数据的时候写了好几次,而读取数据只读了一次,这样就可能造成数据的丢失。为了解决这种情况,引入互锁,作者的理解:我们写一个数据写入到缓冲区,我们先检查一下缓冲区数据是否已经满了,如果满了就等待,将读取数据将缓冲区的数据读取走之后才可以重新写入数据。读取数据之后将缓冲区的数据清零,为了标记缓冲区是否已满,需要引入一个计数器来标记缓冲区。
通过System.Threading命名空间的Interlocked类控制计数器,从而实现进程的同步
Read() :读取计数器的值
Increment() :使计数器增加1
Decrement() :使计数器减小1
Add():使计数器增加指定的数值
Exchange() :把计数器设定为指定的数值
CompareExchange():先把计数器与某个值比较,若相等,则把计数器设定为指定的数值。
//缓冲区,只能容纳一个字符
private static char buffer;
private static int count;
private static long numberOfUsedSpace = 0; //标识量(缓冲区已使用的空间,初始值为0)
static void Main(string[] args)
{
//线程:写者
Thread Writer = new Thread(delegate ()
{
string sentence = "这是一个测试多线程同步的例子。";
count = sentence.Length;
for (int i = 0; i < sentence.Length; i++)
{
while(Interlocked.Read(ref numberOfUsedSpace)==1) //检查缓冲区是否满了,如果满了,就继续等待
{
Thread.Sleep(10);
}
buffer = sentence[i]; //向缓冲区写入数据
Thread.Sleep(26);
Interlocked.Increment(ref numberOfUsedSpace); //写入数据后将缓冲区标记为满
}
});
//线程:读者
Thread Reader = new Thread(delegate ()
{
for (int i = 0; i < count; i++)
{
while(Interlocked.Read(ref numberOfUsedSpace)==0) //检查缓冲区是否为空,如果为空,继续等待
{
Thread.Sleep(10);
}
char ch = buffer;
Console.Write(ch);
Thread.Sleep(20);
Interlocked.Decrement(ref numberOfUsedSpace); //读取数据后将缓冲区标记为空
}
});
Writer.Start();
Reader.Start();
管程
另外一种线程同步的办法是采用管程(Monitor)的办法。这种办法是采用操作临界资源的办法来实现线程的同步。
理解:以上面程序为例子,我们将临界资源设置一个锁,当写入数据程序想要获取临界资源的时候,先检查临界资源是否有人在用,有过有人用,就继续等待,如果没有人用,写入就获取到临界资源并进行上锁,当读取数据程序想要读取临界资源的数据时候,发现临界资源已经上锁,读取数据线程就进行睡眠,进行等待,之后写入数据使用完之后,会唤醒读取数据线程获取临界资源。
使用管程不用创建对象
Monitor的部分方法:
Enter(): 获取临界资源的独占锁,若不成功,睡眠在临界资源上
TryEnter():试图获取临界资源的独占锁,若不成功,立即返回
Pulse():唤醒睡眠在临界资源上的线程
PulseAll():唤醒睡眠在临界资源上的所有线程
Wait():释放独占锁并让当前线程睡眠在临界资源上
Exit():释放独占锁,退出临界区
//缓冲区,只能容纳一个字符
private static char buffer;
private static int count;
private static object lockForBuffer = new object(); //用于同步的对象
static void Main(string[] args)
{
//线程:写者
Thread Writer = new Thread(delegate ()
{
string sentence = "这是一个测试多线程同步的例子。";
count = sentence.Length;
for (int i = 0; i < sentence.Length; i++)
{
try
{
//进入临界区
Monitor.Enter(lockForBuffer);
//向缓冲区写入数据
buffer = sentence[i];
//唤醒睡眠在临界资源上的线程
Monitor.Pulse(lockForBuffer);
//让当前线程睡眠在临界资源上
Monitor.Wait(lockForBuffer);
}
catch(System.Threading.ThreadInterruptedException)
{
Console.WriteLine("线程Writer被中止");
}
finally
{
//退出临界区
Monitor.Exit(lockForBuffer);
}
}
});
//线程:读者
Thread Reader = new Thread(delegate ()
{
for (int i = 0; i < count; i++)
{
try
{
//进入临界区
Monitor.Enter(lockForBuffer);
//从缓冲区读取数据
char ch = buffer;
Console.Write(ch);
//唤醒睡眠在临界资源上的线程
Monitor.Pulse(lockForBuffer);
//让当前线程睡眠在临界资源上
Monitor.Wait(lockForBuffer);
}
catch(System.Threading.ThreadInterruptedException)
{
Console.WriteLine("线程Reader被中止");
}
finally
{
//退出临界区
Monitor.Exit(lockForBuffer);
}
}
});
Writer.Start();
Reader.Start();
同样可以使用更加简洁的lock语句
lock(缓冲区对象)
{
//临界区代码
.........;
.........;
}
//等同于
try
{
Monitor Enter(要锁定的对象)
//临界区代码
........;
........;
}
finally
{
Monitor.Exit(要锁定的对象);
}
//缓冲区,只能容纳一个字符
private static char buffer;
private static int count;
private static object lockForBuffer = new object(); //用于同步的对象
static void Main(string[] args)
{
//线程:写者
Thread Writer = new Thread(delegate ()
{
string sentence = "这是一个测试多线程同步的例子。";
count = sentence.Length;
for (int i = 0; i < sentence.Length; i++)
{
lock (lockForBuffer)
{
buffer = sentence[i];
Monitor.Pulse(lockForBuffer);
Monitor.Wait(lockForBuffer);
}
}
});
//线程:读者
Thread Reader = new Thread(delegate ()
{
for (int i = 0; i < count; i++)
{
lock(lockForBuffer)
{
char ch = buffer;
Console.Write(ch);
Monitor.Pulse(lockForBuffer);
Monitor.Wait(lockForBuffer);
}
}
});
Writer.Start();
Reader.Start();
互斥体(Mutex类)
许多线程需要共享资源,但是使用资源只需要自己独享。比如打印机每次只能打印一份文件。这种称为互斥。
线程互斥实质上也是同步,可以看作一种特殊的线程同步。线程的互斥常用Mutex类
Mutex的部分方法:
WaitOne() : 请求互斥体的所属权,只有请求到所属权后线程才能进入临界区
ReleaseMutex() :释放互斥体的所属权
OpenExisting():打开现有的已命名互斥体
Mutex类是非静态方法,使用时必须创建一个Mutex对象。
互斥体又两种类型:
局部互斥体:只能在创建它的程序中使用
系统互斥体:能被系统中不同的应用程序共享
//创建互斥体
Mutex fileMutex = new Mutex(false(创建者是否具有初始所属权),"MutexForTimeRecordFile(互斥体的系统名称)")'
//创建第一个程序,在主函数中输入下面代码
using System;
using System.Threading;
using System.IO;
namespace ConsoleApp10
{
class Program
{
static void Main(string[] args)
{
Thread ThreadA = new Thread(delegate () {
Mutex fileMutex = new Mutex(false, "MutexForTimeRecordFile"); //创建互斥体
string fileName = @"C:/ALL_TEST/TimeRecord.txt";
for (int i = 1; i <= 10; i++)
{
try
{
fileMutex.WaitOne(); //请求互斥体的所属权,若成功,进入临界区,不成功,等待
File.AppendAllText(fileName, "ThreadA:" + DateTime.Now + "\r\n"); //在临界区中操作临界资源,即向文件中写入数据
}
catch(System.Threading.ThreadInterruptedException)
{
Console.WriteLine("线程A被中断");
}
finally
{
//释放互斥体的所属权
fileMutex.ReleaseMutex();
}
Thread.Sleep(1000);
}
});
ThreadA.Start();
}
}
}
//创建第二个程序,在主函数中输入下面代码
using System;
using System.Threading;
using System.IO;
namespace ConsoleApp10
{
class Program
{
static void Main(string[] args)
{
Thread ThreadB = new Thread(delegate () {
Mutex fileMutex = new Mutex(false, "MutexForTimeRecordFile"); //创建互斥体
string fileName = @"C:/ALL_TEST/TimeRecord.txt";
for (int i = 1; i <= 10; i++)
{
try
{
fileMutex.WaitOne(); //请求互斥体的所属权,若成功,进入临界区,不成功,等待
File.AppendAllText(fileName, "ThreadB:" + DateTime.Now + "\r\n"); //在临界区中操作临界资源,即向文件中写入数据
}
catch(System.Threading.ThreadInterruptedException)
{
Console.WriteLine("线程A被中断");
}
finally
{
//释放互斥体的所属权
fileMutex.ReleaseMutex();
}
Thread.Sleep(1000);
}
});
ThreadB.Start();
}
}
}
运行结果:
死锁
用一个例子就可以讲解死锁的概念:一对情侣去一个餐厅吃饭,但是餐厅只剩下一个刀子和一个叉子给他们用,只有同时有刀子和叉子才可以吃饭。
不死锁的情况:男生拿起刀子和叉子,吃点;然后同时放下刀子和叉子,女孩拿起刀子和叉子,吃点;放下刀子和叉子,男生拿起刀子和叉子…
死锁的情况:男生拿起刀子,女生拿起叉子。男生等叉子,女生等刀子,一直等等…死锁了~
线程池
通过Thread类创建的线程并销毁线程代价相对于来说很高,过多的线程会消耗掉大量的内存和CPU资源。改善这种状况,引入线程池概念。程序中包含若干个简单的且不需要特殊控制的线程,可以使用线程池(ThreadPool)
ThreadPool使用方法:
GetMaxThreads():获取线程池中线程数目的上限
GetMinThreads():获取线程池中线程数目的下限
SetMaxThreads():设置线程池中线程数目的上限
SetMinThreads():设置线程池中线程数目的下限
QueueUserWorkItem():将工作任务排入线程池
using System;
using System.Threading;
namespace ConsoleApp11
{
class Program
{
//用于记录已经运行完毕的线程数目
static int finishedThreadCount = 0;
//用来保存每个线程的计算结果
static int[] result = new int[10];
static void Main(string[] args)
{
//向线程池中排入9个工作线程
for(int i =1;i<=9;i++)
{
ThreadPool.QueueUserWorkItem(new WaitCallback(WorkFunction), i);
}
//等待9个工作线程运行完毕(注意,这是个空循环)
while (finishedThreadCount < 9) ;
//输出计算结果
for (int i = 0; i <= 9; i++)
{
Console.WriteLine("线程{0}:{0}!={1}", i, result[i]);
}
}
//工作函数 :求n的阶乘
public static void WorkFunction(object n )
{
//计算阶乘
int fac = 1;
for(int i =1;i<=(int)n;i++)
{
fac *= i;
}
result[(int)n] = fac;
finishedThreadCount++;
}
}
}
以下几种情况不适合使用ThreadPool而使用单独的Thread
1.线程执行需要很长时间
2.需要为线程指定详细的优先级
3.在执行过程中需要对线程进行操作,比如睡眠、挂起等。