开发环境
1、操作系统: Windows 10 X64 2、SDK:Visual Studio 2019
1.问题描述
随机生成100万个1-100,000,000之间的整数,并存入文件data.dat中,要求用二进制的方式进行存储(可以规定存储的策略,方便后继读取。然后,生成n(默认n=10)个线程平行地非重复地读取这个文件,并在内存中从小到大进行排序(相当于把数据文件分成n等分)。当所有的线程都把数据排序好时,主线程对数据进行归并排序,并逐次把排序后的数据存储到新的文件sort.data。
2.完整代码
这里我们用c#控制台应用程序。
using System;
using System.Diagnostics;
using System.Threading;
using System.IO;
using System.Text;
namespace 随机数排序
{
class Program
{
static int N = 10,M=1000,Max=1000000000;
static int[] arr=new int[N*M];
static string path = ".\\data.dat";//data文件名
static string tpath = ".\\sort.dat";//sort文件名
static void creat_nums()//产生并存放随机数
{
if (File.Exists(path))
File.Delete(path);//若已经存在文件,则删除
FileStream fs = File.OpenWrite(path);
BinaryWriter bw = new BinaryWriter(fs, Encoding.Default);
for(int i=0;i<N*M;i++)
{
int t = GetRandomByGuid();
//Console.WriteLine("{0}", t);//测试用
bw.Write(t);
}
fs.Close();//要关闭此进程,否则无法对该文件进行其他操作
}
static int GetRandomSeed()//获得种子
{
byte[] bytes = new byte[4];
System.Security.Cryptography.RNGCryptoServiceProvider rng = new System.Security.Cryptography.RNGCryptoServiceProvider();
rng.GetBytes(bytes);
return BitConverter.ToInt32(bytes, 0);
}
static int GetRandomByGuid()//获得单个随机数
{
Random random = new Random(GetRandomSeed());
return random.Next(0, Max);
}
public static void Quick_Sort(int[] arr, int begin, int end)//快排
{
if (begin > end)
return;
int tmp = arr[begin];
int i = begin;
int j = end;
while (i != j)
{
while (arr[j] >= tmp && j > i)
j--;
while (arr[i] <= tmp && j > i)
i++;
if (j > i)
{
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}
arr[begin] = arr[i];
arr[i] = tmp;
Quick_Sort(arr, begin, i - 1);
Quick_Sort(arr, i + 1, end);
}
public static void read_nums(int n)//读入
{
int reclen, currp;
FileStream fs = File.OpenRead(path);
BinaryReader br = new BinaryReader(fs, Encoding.Default);
reclen = (int)(fs.Length / N);//每个记录的长度
currp = n * reclen;
fs.Seek(currp, SeekOrigin.Begin);//将文件流指针定位在指定的位置
for (int i = 0; i < M; i++)
{
arr[n * M + i] = br.ReadInt32();
}
}
public static void fuck(object obj)//用于传给Thread的函数
{
int x = (int)obj;
read_nums(x);
Quick_Sort(arr, x * M, (x + 1) * M - 1);
}
static int findmin(int[]a , int len)
{
int min = a[0] ,n = 0;
for(int i=0;i<len;i++)
{
if (min > a[i])
{
n = i;
min = a[i];
}
}
return n;
}//十个数里面取最小,返回下标
static void mergeall()//大归并
{
int[] mov=new int[10];
if (File.Exists(tpath))
File.Delete(tpath);
FileStream fs = File.OpenWrite(tpath);
BinaryWriter bw = new BinaryWriter(fs, Encoding.Default);
int[] a = new int[N];
for(int i=0;i<N;i++)
a[i] = arr[i * M];
for (int i = 0; i < N*M; i++)
{
int t;
t = findmin(a, N);
bw.Write(arr[ t * M+mov[t]]);
Console.WriteLine("{0}", arr[t * M + mov[t]]);//测试用
mov[t]++;
if (mov[t]==M)
a[t] =int.MaxValue;
else
a[t] = arr[t * M + mov[t]];
}
fs.Close();//要关闭此进程,否则无法对该文件进行其他操作
}
static void Main(string[] args)
{
int flag;
Console.WriteLine("Enter 0 if you want to diy parameters:");
flag =int.Parse(Console.ReadLine());
if (flag==0)
{
Console.WriteLine("Please enter threads number,random data scale and max random number:");
N = int.Parse(Console.ReadLine());
M = int.Parse(Console.ReadLine());
Max = int.Parse(Console.ReadLine());
}
creat_nums();
Thread[] threads= new Thread[N];
for (int i = 0; i < threads.Length; i++)
{
threads[i] = new Thread(new ParameterizedThreadStart(fuck));
threads[i].Start(i);
}
bool sign = true;
while(sign)
{
for (int i = 0; i < threads.Length; i++)
{
if (threads[i].IsAlive)
sign = true;
else
sign = false;
}
}
mergeall();
}
}
}
3.模块解析
1.全局变量
static int N = 10,M=1000,Max=1000000000; static int[] arr=new int[N*M];
静态变量N,M,Max
分别是线程数,随机数个数,随机数的上限(后面可以自己输入值,但是最好让N可以整除M,当然,假如要实现这样的功能也不会太麻烦,这里略去😁)。arr[N*M]
是用于存放多线程读取元素的数组。
2.随机数产生
static int GetRandomSeed()//获得种子 { byte[] bytes = new byte[4]; System.Security.Cryptography.RNGCryptoServiceProvider rng = new System.Security.Cryptography.RNGCryptoServiceProvider(); rng.GetBytes(bytes); return BitConverter.ToInt32(bytes, 0); } static int GetRandomByGuid()//获得单个随机数 { Random random = new Random(GetRandomSeed()); return random.Next(0, Max); }
这里使用的是RNGCryptoServiceProvider
来产生种子,相比于直接new Random
可以以一个并不慢很多的速度在短时间内产生大量不同的随机数,更加接近于真实随机数。也可以用Guid().GetHashCode()
。
3.二进制存储数据
static void creat_nums()//产生并存放随机数 { if (File.Exists(path)) File.Delete(path);//若已经存在文件,则删除 FileStream fs = File.OpenWrite(path); BinaryWriter bw = new BinaryWriter(fs, Encoding.Default); for(int i=0;i<N*M;i++) { int t = GetRandomByGuid(); //Console.WriteLine("{0}", t);//测试用 bw.Write(t); } fs.Close();//要关闭此进程,否则无法对该文件进行其他操作 }
由于是二进制格式,所以这里使用的是BinaryWriter
,然后将数据以四字节二进制存入即可。
以及在最后必须调用fs.Close()
来关闭OpenWrite()
的进程,否则在read_nums
中会无法对data文件进行操作,原因是该文件一直被OpenWrite()
占用,直到其被关闭。
4.多线程处理数据
public static void read_nums(int n)//读入 { int reclen, currp; FileStream fs = File.OpenRead(path); BinaryReader br = new BinaryReader(fs, Encoding.Default); reclen = (int)(fs.Length / N);//每个记录的长度 currp = n * reclen; fs.Seek(currp, SeekOrigin.Begin);//将文件流指针定位在指定的位置 for (int i = 0; i < M; i++) { arr[n * M + i] = br.ReadInt32(); } }
由于要实现多线程非重复读取,所以要对每个线程读取的范围进行规定,即下标从n*M
到n*M+M-1
,然后用br.ReadInt32()
将数据一个个读取即可。
Thread[] threads= new Thread[N]; for (int i = 0; i < threads.Length; i++) { threads[i] = new Thread(new ParameterizedThreadStart(fuck)); threads[i].Start(i); } bool sign = true; while(sign) { for (int i = 0; i < threads.Length; i++) { if (threads[i].IsAlive) sign = true; else sign = false; } }
public static void fuck(object obj)//用于传给Thread的函数 { int x = (int)obj; read_nums(x); Quick_Sort(arr, x * M, (x + 1) * M - 1); }
而多线程的实现我这里是使用了线程动态数组threads
,然后由于ParameterizedThreadStart
委托只能接受参数为object obj
的方法,所以要创建一个fuck
😃函数来初始化实例。再调用Start(i)
方法启动线程并把实参传入即可。
而sign
是指示threads
线程程是否仍在工作的标志,这里的循环时为了确保mergeall()
的执行一定是在多线程对数据的读取排序之后,以防止其对未处理完的数据进行归并。(或许存在更简便的手段来达到同样的目的,maybe异步?怎奈还没学那么多,也懒得多想😎)
5.归并
static void mergeall()//大归并 { int[] mov=new int[10]; if (File.Exists(tpath)) File.Delete(tpath); FileStream fs = File.OpenWrite(tpath); BinaryWriter bw = new BinaryWriter(fs, Encoding.Default); int[] a = new int[N]; for(int i=0;i<N;i++) a[i] = arr[i * M]; for (int i = 0; i < N*M; i++) { int t; t = findmin(a, N); bw.Write(arr[ t * M+mov[t]]); //Console.WriteLine("{0}", arr[t * M + mov[t]]);//测试用 mov[t]++; if (mov[t]==M) a[t] =int.MaxValue; else a[t] = arr[t * M + mov[t]]; } fs.Close(); }
我们要归并的是N
个已经排序好了的数组,我的思路是先把这些数组的头元素放入数组a
中,然后用findmin(a, N)
方法找出N个头元素中最小的元素并且返回其下标。聪明的读者不难发现,这样找出来的数就是当前所有数里面最小的。然后我们根据下标将那个最小的数存入sort文件或者弹出,并且让mov[t]
自增,并且更新a[t]
。
mov
数组的作用是存储每个线程的数组中已经被选出来多少个元素,即移动了几次。然后当mov[t]==M
时,说明该线程的数组已经遍历完,若再往下访问会导致溢出或者访问到其他区域的元素,所以我们要给a[t]
赋一个很大的值来防止该数组继续被访问。