Сканирование реестра. Рекурсия и потоки. C#

133
11 марта 2018, 20:18

Доброго времени суток!

Пишу программу, в которой мне нужно просканировать весь реестр. Решил делать это рекурсивно, как делал с файлами и папками, их сканировало около 20 секунд. В результате вышел где-то такой код:

class AllRegistryPermissions
    {
        public Dictionary<string, StringDictionary> HKCRKeys;
        public Dictionary<string, StringDictionary> HKCUKeys;
        public Dictionary<string, StringDictionary> HKLMKeys;
        static List<string> FPK;
        static public void GetRegistry(Dictionary<string, StringDictionary> Keys, RegistryKey ParentKey)
        {
            string Name = ParentKey.Name;
            if (!((Properties.Settings.Default.RFilterPathList.Contains(Name)) || (FPK.Any(t => Name.Contains(t)))))  //фильтрация по пути и по ключевым словам
            {
                if (ParentKey.ValueCount != 0)
                {
                    Keys.Add(ParentKey.Name, new StringDictionary());
                    foreach (string name in ParentKey.GetValueNames())
                    {
                        Keys[ParentKey.Name].Add(name, ParentKey.GetValue(name, "!!! Cant identify value !!!").ToString());
                    }
                }
                else Keys.Add(ParentKey.Name, null);
                if (ParentKey.SubKeyCount != 0)
                {
                    foreach (string name in ParentKey.GetSubKeyNames())
                    {
                        try
                        {
                            if (ParentKey.OpenSubKey(name) != null) GetRegistry(Keys, ParentKey.OpenSubKey(name));
                        }
                        catch (System.Security.SecurityException) { }
                    }
                }
            }
        }
        public AllRegistryPermissions()
        {
            FPK = Properties.Settings.Default.RFilterKeywordList.Cast<string>().ToList();
            HKCRKeys = new Dictionary<string, StringDictionary>();
            GetRegistry(HKCRKeys, Registry.ClassesRoot);
            HKCUKeys = new Dictionary<string, StringDictionary>();
            GetRegistry(HKCUKeys, Registry.CurrentUser);
            HKLMKeys = new Dictionary<string, StringDictionary>();
            GetRegistry(HKLMKeys, Registry.LocalMachine);
        }
    }

Сканировал-то он нормально, но на сканирование одной только ветки HKCR ушло пять минут. Результат меня явно не устроил (так как тот же Systracer сканирует все за 20-30 секунд) и я задумался о многопоточности. После долгого колдовства (хотелось бы считать, что я перерос из хэоуволдщика хотя бы в быдлокодера) код стал выглядеть так:

class AllRegistryPermissions
    {
        public ConcurrentDictionary<string, StringDictionary> HKCRKeys;
        public ConcurrentDictionary<string, StringDictionary> HKCUKeys;
        public ConcurrentDictionary<string, StringDictionary> HKLMKeys;
        static CountdownEvent ce;
        static int maxtasks = 0;
        static int cantopen = 0;
        static int cantopensec = 0;
        static List<string> FPK;
        static public void GetRegistry(ConcurrentDictionary<string, StringDictionary> Keys, RegistryKey ParentKey)
        {
            string Name = ParentKey.Name;
            if (!((Properties.Settings.Default.RFilterPathList.Contains(Name)) || (FPK.Any(t => Name.Contains(t)))))
            {
                int close = 0;
                if (ParentKey.ValueCount != 0) 
                {
                    ce.AddCount();
                    if (ce.CurrentCount > maxtasks) maxtasks = ce.CurrentCount;
                    Task.Run(() =>
                    {                        
                        Keys.GetOrAdd(ParentKey.Name, new StringDictionary());
                        foreach (string name in ParentKey.GetValueNames())
                        {
                            Keys[ParentKey.Name].Add(name, ParentKey.GetValue(name, "!!! Cant identify value !!!").ToString());
                        }
                        close++;
                        if (close == 2) ParentKey.Close();
                        ce.Signal();
                    });
                }
                else
                {
                    Keys.GetOrAdd(ParentKey.Name, (StringDictionary)null);
                    close++;
                }
                if (ParentKey.SubKeyCount != 0)
                {
                    ce.AddCount();
                    if (ce.CurrentCount > maxtasks) maxtasks = ce.CurrentCount;
                    Task.Run(() =>
                    {
                        foreach (string name in ParentKey.GetSubKeyNames())
                        {
                            try { if (ParentKey.OpenSubKey(name) != null) GetRegistry(Keys, ParentKey.OpenSubKey(name));
                                else cantopen++; }
                            catch (System.Security.SecurityException) {
                                cantopensec++; }
                        }
                        close++;
                        if (close == 2) ParentKey.Close();
                        ce.Signal();
                    });
                }
                else close++;
            }
        }
        public AllRegistryPermissions()
        {
            FPK = Properties.Settings.Default.RFilterKeywordList.Cast<string>().ToList();
            ce = new CountdownEvent(3);
            Task hkcrTask = Task.Run(() =>
            {
                HKCRKeys = new ConcurrentDictionary<string, StringDictionary>();
                GetRegistry(HKCRKeys, Registry.ClassesRoot);
                ce.Signal();
            });
            Task hkcuTask = Task.Run(() =>
            {
                HKCUKeys = new ConcurrentDictionary<string, StringDictionary>();
                GetRegistry(HKCUKeys, Registry.CurrentUser);
                ce.Signal();
            });
            Task hklmTask = Task.Run(() =>
            {
                HKLMKeys = new ConcurrentDictionary<string, StringDictionary>();
                GetRegistry(HKLMKeys, Registry.LocalMachine);
                ce.Signal();
            });            
             ce.Wait();
            Console.WriteLine("maximal tasks numbers: " + maxtasks);
            Console.WriteLine("can't open {0} subkeys", cantopen);
            Console.WriteLine("can't open {0} subkeys sec", cantopensec);
        }
    }

Все бы хорошо, но чувствую, что это вообще не хорошо и не правильно. С многопоточностью я первый раз работаю и сколько потоков нормально запускать в одно время не знаю. Погуглив малость увидел, что 1-4. Мой код одновременно запускал максимум 130000 потоков и я подозреваю, что это очень не хорошо (я думал компьютер взорвется). Но конструктор отработал за минуту. Собственно вопрос, как мне решить проблему? Как-то переделывать код, регулировать количество потоков, менять сам подход или переписывать код на другом языке (слышал, что C++ шустрее в этом плане)? В программе два экземпляра этого класса будут испрользоваться, чтобы сравнить и обнаружить, какие ключи были удалены, добавлены или изменены.

Answer 1

Касаемо вашего кода:

  • Он не рабочий, так как нет нормальной обработки исключений. Вы конечно в одном месте воткнули try catch, но этого не достаточно. Читаете внимательно документацию.
  • RegistryKey.OpenSubKey это обертка над апишным вызовом, который соответственно выделяет некоторый объект ОС. И этот объект нужно после использования освобождать. По этому тип RegistryKey является IDisposable. Вы мало того что не освобождаете RegistryKey, вы вдобавок вызываете OpenSubKey два раза подрядят.
  • Статические переменные это конечно удобный механизм, но придумывали его не для эмуляции глобальных переменных. Если уж делаете статические методы таким образом как у вас организовано, то передавайте данные в виде аргументов. Иначе вы можете получить очень большие печали, которые на начальном уровне знаний будет трудно выловить. Вдобавок ваши счетчики ни как не защищены от параллельного доступа(читайте про Interlocked и Volatile)
  • Вы используете задачи(Task), а не потоки(Thread). Между этими вещами большая разница. Соответственно у вас 130000 задач а не 130000 потоков. И если у вас было 130000 потоков то скорее всего действительно все схлопнулось(ну по крайне мере в 32 битном процессе)
  • В ваше случае не нужно создавать такое дикое количество задач, это приведет только излишнему выделению объектов. Вам достаточно на каждую ветку по потоку(смотрите как я сделал в примере ниже)

Пример рабочего кода:

Класс для чтения:

class RegData
{
    public Dictionary<string, StringDictionary> HKCRKeys;
    public Dictionary<string, StringDictionary> HKCUKeys;
    public Dictionary<string, StringDictionary> HKLMKeys;
    public RegData()
    {
        HKLMKeys = new Dictionary<string, StringDictionary>();
        HKCRKeys = new Dictionary<string, StringDictionary>();
        HKCUKeys = new Dictionary<string, StringDictionary>();
    }
    public void ReadSingleThread()
    {
        Clear();
        var FPK = Properties.Settings.Default.RFilterPathList.Cast<string>().ToList();
        //GetRegistry(HKCRKeys, Registry.ClassesRoot, FPK);
        GetRegistry(HKCUKeys, Registry.CurrentUser, FPK);
        GetRegistry(HKLMKeys, Registry.LocalMachine, FPK);
    }
    public void ReadMultiThread()
    {
        Clear();
        var FPK = Properties.Settings.Default.RFilterPathList.Cast<string>().ToList();
        //var hkcrThread = RunThread(HKCRKeys, Registry.ClassesRoot, FPK);
        var hkcuThread = RunThread(HKCUKeys, Registry.CurrentUser, FPK);
        var hklmThread = RunThread(HKLMKeys, Registry.LocalMachine, FPK);
        //hkcrThread.Join();
        hkcuThread.Join();
        hklmThread.Join();
    }
    private void Clear()
    {
        HKLMKeys.Clear();
        HKCRKeys.Clear();
        HKCUKeys.Clear();
    }
    private Thread RunThread(Dictionary<string, StringDictionary> keys, RegistryKey parentKey, List<string> FPK)
    {
        var thread = new Thread(() => {
            try {
                GetRegistry(keys, parentKey, FPK);
            }
            catch (Exception e) {
                Console.WriteLine($"Thread proc for {parentKey.Name} fault: {e.Message}");
            }
        });
        thread.Start();
        return thread;
    }
    //###########
    //
    // Static
    //
    //###########
    static private void GetRegistry(Dictionary<string, StringDictionary> keys, RegistryKey parentKey, List<string> FPK)
    {
        string Name = parentKey.Name;
        if (Properties.Settings.Default.RFilterPathList.Contains(Name) || FPK.Any(t => Name.Contains(t))) {
            return;
        }
        AddValues(keys, parentKey);
        AddSubKeys(keys, parentKey, FPK);
    }
    static private void AddValues(Dictionary<string, StringDictionary> keys, RegistryKey parentKey)
    {
        string[] valueNames = null;
        if (!TryGetValueNames(parentKey, out valueNames)) {
            keys.Add(parentKey.Name, null);
            return;
        }
        var values = new StringDictionary();
        keys.Add(parentKey.Name, values);
        foreach (string name in valueNames) {
            values.Add(name, parentKey.GetValue(name, "error value").ToString());
        }
    }
    static private void AddSubKeys(Dictionary<string, StringDictionary> keys, RegistryKey parentKey, List<string> FPK)
    {
        string[] subKeyNames = null;
        if (!TryGetSubKeyNames(parentKey, out subKeyNames)) {
            return;
        }
        foreach (string subKeyName in subKeyNames) {
            RegistryKey subkey = null;
            if (TryOpenSubKey(parentKey, subKeyName, out subkey)) {
                using (subkey) {
                    GetRegistry(keys, subkey, FPK);
                }
            }
        }
    }
    static private bool TryGetValueNames(RegistryKey parentKey, out string[] valueNames)
    {
        valueNames = null;
        try {
            if (parentKey.ValueCount != 0) {
                valueNames = parentKey.GetValueNames();
            }
        }
        catch (Exception e) {
            Console.WriteLine($"GetValueNames of {parentKey.Name} fail: {e.Message}");
        }
        return valueNames != null;
    }
    static private bool TryGetSubKeyNames(RegistryKey parentKey, out string[] subKeyNames)
    {
        subKeyNames = null;
        try {
            if (parentKey.SubKeyCount != 0) {
                subKeyNames = parentKey.GetSubKeyNames();
            }
        }
        catch (Exception e) {
            Console.WriteLine($"GetSubKeyNames of {parentKey.Name} fail: {e.Message}");
        }
        return subKeyNames != null;
    }
    static private bool TryOpenSubKey(RegistryKey parentKey, string subKeyName, out RegistryKey subKey)
    {
        subKey = null;
        try {
            subKey = parentKey.OpenSubKey(subKeyName);
            if (subKey == null) {
                Console.WriteLine($"OpenSubKey({subKeyName}) of {parentKey.Name} generic fail");
            }
        }
        catch (System.Security.SecurityException) {
            // антиспам
        }
        catch (Exception e) {
            Console.WriteLine($"OpenSubKey({subKeyName}) of {parentKey.Name} fail: {e.Message}");
        }
        return subKey != null;
    }
}

Пример использования:

static void Main(string[] args)
{
    TestSingleThread();
    TestMultiThread();
    TestSingleThread();
    TestMultiThread();
    TestSingleThread();
    TestMultiThread();
    Console.ReadKey();
}
static void TestSingleThread()
{
    var sw = new Stopwatch();
    sw.Start();
    var t = new RegData();
    t.ReadSingleThread();
    sw.Stop();
    Console.WriteLine($"Singlethread done: {sw.ElapsedMilliseconds}");
}
static void TestMultiThread()
{
    var sw = new Stopwatch();
    sw.Start();
    var t = new RegData();
    t.ReadMultiThread();
    sw.Stop();
    Console.WriteLine($"Multithread done: {sw.ElapsedMilliseconds}");
}

Касаемо скорости:

Я тестировал ваш код и свой, и они по скорости были примерно одинаковые. У меня в среднем чтение в одно-поточном режиме занимало 10-20 секунд, в много-поточном скорость не сильно увеличивается за счет того что у меня ветка HKEY_LOCAL_MACHIN очень большая, а остальнные маленькие.

Во общем мне ни как не получилось достич таких плохих показателей как у вас. Я маленько погуглил и нашел это и это. Если кратко то...

  • ОС может кэшировать обращение к реестру и поэтому через какое то кол-во обращений доступ должен ускорится. Попробуйте запускать код не сколько раз может показатели сильно улутшаться
  • В первой ссылки говорится что с веткой HKEY_CLASSES_ROOT могут быть проблемы скорости из-за длинных имен. Я пока тестировал у меня по скрости она не отличалась от остальных. Но у вас может быть будут другие результаты, попробуйте просканировать исключая эту ветку.

Касаемо С++:

Плюсы конечно мощный язык и в целом код на нем будет быстрее. Но у вас идет очень интенсивный вызов функций ОС, и тут что шарп, что плюсы не дадут каких то больших разниц в скоростях. Вдобавок плюсы сложный язык и написать на нем, то вы уже написали займет у вас намного больше времени и сил, а выгода возможно буде копеечной.

READ ALSO
Open XML: как получить тип элемента?

Open XML: как получить тип элемента?

Мне необходимо распарсить слайды презентации PowerPoint (pptx), а именно получить три значения для каждого элемента (shape) на слайде:

161
Xamarin android: header в recyclerview

Xamarin android: header в recyclerview

Пытаюсь вывести список с категориями, первая категория выводится, а последующие нет

148
Группировка cs-файлов в решении

Группировка cs-файлов в решении

В решении aspnet core 2

182
C# WPF Биндинг DataGrid и struct

C# WPF Биндинг DataGrid и struct

Доброго времени сутокВозникла проблема с привязкой структуры к DataGrid, раньше это делал, но вот сейчас ничего не получается, подскажите в чем...

198