none
Многопоточное использование сборки C++/CLI в C# RRS feed

  • Вопрос

  • Ядро программы на С,  скомпилировано в сборку для C# в C++/CLI:

    namespace MyClassLibrary { public ref class MyClass { //...

    public:

    void Kernel();

    //...

    } }

    В проекте на C# добавляю ссылку на сборку, создаю два экземпляра соответствующего типа, и пытаюсь запустить два экземпляра ядра в параллельных потоках:

    using MyClassLibrary;
    namespace MyExampleWindowsFormsApplication

    {

        public partial class Form1 : Form
        {      

            MyClass mylib1 = new MyClass();
            MyClass mylib2 = new MyClass();

            void RunKernel1()
            {
                mylib1.Kernel();
            }
            void RunKernel2()
            {
                mylib2.Kernel();
            }

            private unsafe void button_RunKernels_Click(object sender, EventArgs e)
            {
                Thread RunThread1 = new Thread(new ThreadStart(RunKernel1));
                RunThread1.Start();
                Thread RunThread2 = new Thread(new ThreadStart(RunKernel2));
                RunThread2.Start();
            }

    }

    }

    Становится ясно, что не создаётся два отдельных экземпляра ядер, и идёт параллельное обращение к одному ядру из разных потоков. 

    Подскажите, как создать два независимых экземпляра ядер и заставить их работать независимо друг от друга?










    6 ноября 2013 г. 16:07

Ответы

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

    Мне в голову пришли 2 варианта решения Вашей задачи.

    Первый: организовать параллельную работу не путем запуска нескольких потоков в рамках одного процесса, а путем запуска нескольких экземпляров приложения (нескольких процессов). У каждого из таких процессов гарантировано будет своя копия всех глобальных переменных.

    Второй (требует незначительной модификации кода C-функций): объединить все глобальные переменные в одну структуру. Функция, использующая ту или иную глобальную переменную, должна получить в параметре ссылку или указатель на такую структуру. Например, вместо такого кода:

    int a;
    int b;
    int c;
    
    void sum()
    {
    	c = a + b;
    }
    
    void sub()
    {
    	c = a - b;
    }
    

    Написать вот такой:

    struct Globals
    {
    	int a;
    	int b;
    	int c;
    };
    
    void sum(Globals *data)
    {
    	data->c = data->a + data->b;
    }
    
    void sub(Globals *data)
    {
    	data->c = data->a - data->b;
    }
    

    Тогда экземпляр такой структуры можно было бы сделать полем класса-обертки на C++/CLI

    public ref class MyClass
    {
    	// уникальные данные экземпляра
    	Globals *data;
    
    public:
    
    	MyClass()
    	{
    		data = new Globals;
    	}
    
    	void Kernel()
    	{
    		// метод работает со своим экземпляром данных
    		sum(data);
    		sub(data);
    	}
    };

    Необходимо, конечно, будет позаботиться об освобождении неуправляемой памяти, выделенной в конструкторе (реализовать деструктор и/или финализатор)

    8 ноября 2013 г. 4:18
  • Потоки все равно будут "крутиться" в рамках одного процесса и обращаться к одним и тем же глобальным переменным.
    8 ноября 2013 г. 10:20

Все ответы

  • Становится ясно, что не создаётся два отдельных экземпляра ядер, и идёт параллельное обращение к одному ядру из разных потоков. 


    А по каким признакам Вы определили, что идет обращение к одному объекту? Судя по Вашему коду, нет. Уточните сигнатуру метода Kernel. Он у Вас, случайно, не статический?
    6 ноября 2013 г. 16:32
  • Метод не статический, но кроме того, я использую интерфейс, а указатель на интерфейсный класс храню в статической переменной.

    C++/CLI:

    namespace MyClassLibrary { public ref class MyClass { //...

    public:

    interface class MyLibraryDelegate
    {
        //...
    };

    static MyLibraryDelegate^ mDelegate;
    void setDelegate(MyLibraryDelegate^ value)
    {
        mDelegate = value;
    }

    void Kernel();

    //...

    } }

    C#:

    using MyClassLibrary;
    namespace MyExampleWindowsFormsApplication

    {

        public partial class Form1 : Form
        {      

            MyClass mylib1 = new MyClass();
            MyClass mylib2 = new MyClass();

            MyLibraryDelegate my_delegate1 = new MyLibraryDelegate();
            MyLibraryDelegate my_delegate2 = new MyLibraryDelegate();

            void RunKernel1()
            {

    mylib1.setDelegate(my_delegate1);
                mylib1.Kernel();
            }
            void RunKernel2()
            {
         mylib2.setDelegate(my_delegate2);
                mylib2.Kernel();
            }

            private unsafe void button_RunKernels_Click(object sender, EventArgs e)
            {
                Thread RunThread1 = new Thread(new ThreadStart(RunKernel1));
                RunThread1.Start();
                Thread RunThread2 = new Thread(new ThreadStart(RunKernel2));
                RunThread2.Start();
            }

    public class MyLibraryDelegate{

    //...

    }

        }

    }


    Возможно что действительно дело в этом?

    Сейчас пересоберу...


    6 ноября 2013 г. 17:17
  • Статическое поле разделяется всеми экземплярами класса. В Вашем случае каждый вызов setDelegate перезаписывает значение этого поля и потоки, действительно, будут работать только с одним экземпляром (но не класса MyClass, а делегата).
    6 ноября 2013 г. 17:33
  • Попробуйте покопаться в Activator
    6 ноября 2013 г. 17:38
  • Исправил на:

    C++/CLI:

    namespace MyClassLibrary {
    public ref class MyClass
    {
    	//...
    
    	public:
    
    
    	interface class MyLibraryDelegate
    	{
        	    //...
    	    void fMyTrace(int lpHandle, String^ s);
    
                 //...
    	};
    
    	MyLibraryDelegate^ mDelegate;
    	void setDelegate(MyLibraryDelegate^ value)
    	{
        		mDelegate = value;
    	}
    
     	void Kernel();
    
    	//...
    
    }
    }

    "C":

    namespace MyClassLibrary 
    {
       extern "C" void MyTrace(LPVOID lpHandle, char * str)
      {
        String ^buffer_string = Marshal::PtrToStringAnsi(IntPtr((void *)str));		
        MyClass::mDelegate->fMyTrace((int)lpHandle, buffer_string);
      }
    }

    При обращении к интерфейсному методу (MyClass::mDelegate->fMyTrace((int)lpHandle, buffer_string);) получаю ошибку:

    Error: Нестатическая ссылка не член должна указываться относительно заданного объекта.

    Подскажите, как теперь обратиться к интерфейсной функции? 



    6 ноября 2013 г. 17:38
  • Причина ошибки не совсем ясна. Сложно судить о работоспособности программы по вырванным из контекста кусочкам. В любом случае такой вызов небезопасен, т.к. поле mDelegate к моменту вызова метода может быть еще не инициализировано. 
    6 ноября 2013 г. 18:13
  • Похоже что ссылка на интерфейс должна быть статической.
    6 ноября 2013 г. 23:03
  • Не заметил сразу, что static у mDelegate Вы убрали.

    Да, такой вызов

    MyClass::mDelegate->fMyTrace((int)lpHandle, buffer_string);

    теперь неправильный, т.к. обращаться к нестатическому полю (mDelegate) можно только через объект класса, а не через имя класса.

    7 ноября 2013 г. 9:44
  • Интерфейс (Делегаты) здесь не причём. Всё это я убрал.

    Не создаётся копия ядра!

    C#:

    using MyClassLibrary;
    namespace MyExampleWindowsFormsApplication
    {
        public partial class Form1 : Form
        {       
            MyClass mylib1 = new MyClass();
            MyClass mylib2 = new MyClass();
    
            int lpCallbackHandle1;
            int lpCallbackHandle2;
    
            void RunKernel1()
            {
               lpCallbackHandle1 = mylib1.Init();
               mylib1.Kernel();
            }
            void RunKernel2()
            {
               lpCallbackHandle2 = mylib2.Init();
                mylib2.Kernel();
            }
    
            private unsafe void button_RunKernels_Click(object sender, EventArgs e)
            {
                Thread RunThread1 = new Thread(new ThreadStart(RunKernel1));
                RunThread1.Start();
                Thread RunThread2 = new Thread(new ThreadStart(RunKernel2));
                RunThread2.Start();
            }
        }
    }

    C++/CLI:

    namespace MyClassLibrary { public ref class MyClass { public: void Kernel();

    int Init();

    } }

    cpp:

    LPVOID lpCallbackHandle;
    namespace MyClassLibrary 
    {
      int MyClass::Init()
      {
         return (int)&lpCallbackHandle;
      }
    }

    В результате, lpCallbackHandle1 == lpCallbackHandle2 !
    А должны быть не равны.

    7 ноября 2013 г. 11:34
  • А чего же Вы хотите, если Init() всегда возвращает одно и то же значение - адрес глобальной переменной emv_lpCallbackHandle.
    7 ноября 2013 г. 11:39
  • Я хочу:

    1)  Чтобы создавались две независимые копии ядра, в разных областях памяти.

    2) Чтобы ядра работали параллельно, не мешая друг другу.

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

    Это понятно?

    Когда вы создаёте две переменные одного тип, то каждая из них имеет собственный адрес, не смотря на то, что переменные одного типа.

    Это понятно?

    В C++/CLI класс для сборки можно объявить только как ссылку:

    namespace MyClassLibrary 
    {
     public ref class MyClass
     {
    	public:
     	void Kernel();
    	int Init();
     }
    }

    Такем образом операция:

      MyClass mylib1 = new MyClass();

    не создаёт новый экземпляр типа MyClass, а создаёт новую ссылку на существюющий зкземляр.

    Задача состоит в том, чтобы создавать новые экземпляры из типа, который размещён в сборке C++/CLI.





    7 ноября 2013 г. 15:40
  • То, что мне стало понятно из Вашего кода, я уже написал выше :)

    Повторю, вот этот код:

    LPVOID lpCallbackHandle;
    ...

    return (int)&lpCallbackHandle;

    всегда возвращает одно и то же значение - адрес глобальной переменной-указателя lpCallbackHandle (именно адрес, а не значение переменной).

    Если Вы хотите, чтобы я помог Вам решить проблему постарайтесь изложить задачу в терминах языка программирования и платформы .net - "класс", "переменная", "объект", "поток" и т.д. и не использовать термины той предметной области, для которой создаете программу (типа "ядро").

    7 ноября 2013 г. 15:58
  • Я создал C++/CLI библиотеку.

    Как создать её копию динамически? 

    7 ноября 2013 г. 18:11
  • Мне кажется, что лучше писать новые сообщения в данной теме, а не редактировать предыдущие. Иначе возникает ситуация, когда отвечаешь на один вопрос, а он уже поменялся или дополнился другими.

    К сути. Ключевое слово ref в описании класса говорит о том, что это "ссылочный" тип, экземпляры которого создаются в управляемой куче, а не в стеке. Операция new ВСЕГДА создает новый экземпляр любого типа, а вот операция присваивания, например, просто увеличивает счетчик ссылок на тот же экземпляр (объект) не создавая нового.

    У Вас по сути все написано верно: создаются 2 экземпляра класса, но, видимо, в Вашем классе есть какие-то общие данные. В первом варианте это было статическое поле, во втором единственная глобальная переменная. Сделайте данные уникальными для каждого экземпляра (не статическими и не глобальными, а полями класса - вот поля у каждого экземпляра, действительно, свои)

    7 ноября 2013 г. 18:37
  • Схематично как-то так:

    C++/CLI

    public ref class MyClass
    {
    	// уникальные данные экземпляра
    	int data;
    
    public:
    
    	// конструктор принимает данные для экземпляра
    	// и инициализирует ими свое поле
    	MyClass(int args)
    	{
    		data = args;
    	}
    
    	void Kernel()
    	{
    	        // метод работает со своим
                    // экземпляром данных
    		data += 2;
    	}
    };

    C#

    // один экземпляр класса со своими данными для обработки
    MyClass mylib1 = new MyClass(123);
    
    // второй экземпляр со своими данными
    MyClass mylib2 = new MyClass(456);
    
    // ...
    
    // в первом потоке
    mylib1.Kernel();
    
    // ...
    
    // во втором потоке
    mylib2.Kernel();
    Так потоки будут работать независимо
    7 ноября 2013 г. 19:15
  • Спасибо, кажется начинаем понимать друг друга.

    Дело в том, что я не случайно создал библиотеку на C++/CLI.

    C++/CLI - позволяет использовать код написанный на языке C,C++.

    У меня большой проект написанный на C, и данный подход позволил не переписывать всё под C#.

    Понятно, что при этом все внешние переменные и функции "C", становятся глобальными в C#(в управляемом коде) и параллельная работа в потоках становится не возможной.

    В этой ситуации я вижу пока только два варианта:

    1) Научится создавать динамическую копию всей библиотеки. (если это возможно)

    2) Переписать код с "C" на C#;

    Вы можете ответить, есть возможность создания копии всей библиотеки или нет?


    7 ноября 2013 г. 19:30
  • У меня большой проект написанный на C, и данный подход позволил не переписывать всё под C#.

    Понятно, что при этом все внешние переменные и функции "C", становятся глобальными в C#(в управляемом коде) и параллельная работа в потоках становится не возможной.

    В этой ситуации я вижу пока только два варианта:

    1) Научится создавать динамическую копию всей библиотеки. (если это возможно)

    2) Переписать код с "C" на C#;

    Вы можете ответить, есть возможность создания копии всей библиотеки или нет?


    В этой ситуации правильно набор функций и глобальных переменных на С распихать по классам на "машинном" С++, избегать под страхом расстрела использования глобальных переменных, для этого возможно придется переписывать сишные функции.  Затем, делаете класс на С++.Net и либо наследуетесь от машинного класса (не помню, возможно не сработает), либо его инкапсулируете.

    О возможности создания копии библиотеки думать даже не стоит.

    8 ноября 2013 г. 2:15
  • Глобальные переменные в рамках одного приложения (процесса) существуют только в единственном экземпляре. Поэтому, как Вам уже заметили, в будущем нужно стараться организовывать обмен данными между функциями не с помощью глобальных переменных, а лишь путем использованием параметров и возвращаемых значений функций.

    Мне в голову пришли 2 варианта решения Вашей задачи.

    Первый: организовать параллельную работу не путем запуска нескольких потоков в рамках одного процесса, а путем запуска нескольких экземпляров приложения (нескольких процессов). У каждого из таких процессов гарантировано будет своя копия всех глобальных переменных.

    Второй (требует незначительной модификации кода C-функций): объединить все глобальные переменные в одну структуру. Функция, использующая ту или иную глобальную переменную, должна получить в параметре ссылку или указатель на такую структуру. Например, вместо такого кода:

    int a;
    int b;
    int c;
    
    void sum()
    {
    	c = a + b;
    }
    
    void sub()
    {
    	c = a - b;
    }
    

    Написать вот такой:

    struct Globals
    {
    	int a;
    	int b;
    	int c;
    };
    
    void sum(Globals *data)
    {
    	data->c = data->a + data->b;
    }
    
    void sub(Globals *data)
    {
    	data->c = data->a - data->b;
    }
    

    Тогда экземпляр такой структуры можно было бы сделать полем класса-обертки на C++/CLI

    public ref class MyClass
    {
    	// уникальные данные экземпляра
    	Globals *data;
    
    public:
    
    	MyClass()
    	{
    		data = new Globals;
    	}
    
    	void Kernel()
    	{
    		// метод работает со своим экземпляром данных
    		sum(data);
    		sub(data);
    	}
    };

    Необходимо, конечно, будет позаботиться об освобождении неуправляемой памяти, выделенной в конструкторе (реализовать деструктор и/или финализатор)

    8 ноября 2013 г. 4:18
  • Спасибо, второй вариант понятен.

    Давайте рассмотрим, как реализовать первый вариант: запуск нескольких экземпляров приложения.

    Как это можно сделать?
    8 ноября 2013 г. 5:12
  • Да, собственно, и делать то ничего не надо. Интерфейсная часть приложения задает данные для обработки одним Вашим "ядром" (либо они читаются из файла) и организует один поток, обрабатывающий эти данные (запускающий "ядро"). При необходимости рабочий поток можно прервать командой из интерфейса. Затем запускаете вручную или с помощью скрипта нужное количество приложений и они работают параллельно (работают несколько "ядер").
    8 ноября 2013 г. 7:14
  • То есть, интерфейсная часть при помощи скрипта может запустить любое кол-во приложений?

    Вы можете дать пример запуска нового экземпляра приложения с помощью скрипта?

    8 ноября 2013 г. 7:28
  • Я имел в виду простейший bat-файл, запускающий N экземпляров приложения:

    my_kernel.exe <параметры 1>
    my_kernel.exe <параметры 2>
    my_kernel.exe <параметры 3>

    8 ноября 2013 г. 7:34
  • Хотя нет, так не правильно :) Так экземпляры будут запускаться последовательно. Сейчас подумаю и напишу правильный вариант
    8 ноября 2013 г. 7:36
  • Вот так вернее:

    start my_kernel.exe <параметры 1>
    start my_kernel.exe <параметры 2>
    start my_kernel.exe <параметры 3>

    8 ноября 2013 г. 7:41
  • Понятно.

    А что если программно создать пул объектов или пул приложений (для IIS).

    Как программно создать пул объектов или пул приложений?


    8 ноября 2013 г. 8:04
  • IIS-ом не пользуюсь и, поэтому не владею информацией :) Извините.
    8 ноября 2013 г. 8:07
  • Как программно создать пул объектов на C#  в .NET?
    Поможет это решить вопрос программно?
    8 ноября 2013 г. 8:16
  • Я не понимаю, о чем Вы? Какой "пул объектов", объектов чего?
    8 ноября 2013 г. 9:10

  • using System;
    using System.Collections.Concurrent;
    using System.Threading;
    using System.Threading.Tasks;


    namespace ObjectPoolExample
    {
        public class ObjectPool<T>
        {
            private ConcurrentBag<T> _objects;
            private Func<T> _objectGenerator;

            public ObjectPool(Func<T> objectGenerator)
            {
                if (objectGenerator == null) throw new ArgumentNullException("objectGenerator");
                _objects = new ConcurrentBag<T>();
                _objectGenerator = objectGenerator;
            }

            public T GetObject()
            {
                T item;
                if (_objects.TryTake(out item)) return item;
                return _objectGenerator();
            }

            public void PutObject(T item)
            {
                _objects.Add(item);
            }
        }

        class Program
        {
           static void Main(string[] args)
            {
                CancellationTokenSource cts = new CancellationTokenSource();

                // Create an opportunity for the user to cancel.
                Task.Run(() =>
                    {
                        if (Console.ReadKey().KeyChar == 'c' || Console.ReadKey().KeyChar == 'C')
                            cts.Cancel();
                    });

                ObjectPool<MyClass> pool = new ObjectPool<MyClass> (() => new MyClass());           

                // Create a high demand for MyClass objects.
                Parallel.For(0, 1000000, (i, loopState) =>
                    {
                        MyClass mc = pool.GetObject();
                        Console.CursorLeft = 0;
                        // This is the bottleneck in our application. All threads in this loop
                        // must serialize their access to the static Console class.
                        Console.WriteLine("{0:####.####}", mc.GetValue(i));                

                        pool.PutObject(mc);
                        if (cts.Token.IsCancellationRequested)
                            loopState.Stop();                

                    });
                Console.WriteLine("Press the Enter key to exit.");
                Console.ReadLine();
            }

        }

        // A toy class that requires some resources to create.
        // You can experiment here to measure the performance of the
        // object pool vs. ordinary instantiation.
        class MyClass
        {
            public int[] Nums {get; set;}
            public double GetValue(long i)
            {
                return Math.Sqrt(Nums[i]);
            }
            public MyClass()
            {
                Nums = new int[1000000];
                Random rand = new Random();
                for (int i = 0; i < Nums.Length; i++)
                    Nums[i] = rand.Next();
            }
        }  
    }

    8 ноября 2013 г. 9:47
  • Мы идем по кругу :) Вроде бы, уже пришли к выводу, что многопоточность в одном процессе не решит проблему без изменения исходного кода. То, что Вы написали - это вариант многопоточности, только реализованный более современными средствами .net. Неужели, я до сих пор не ответил на Ваш первоначальный вопрос? :)
    8 ноября 2013 г. 9:59
  • Хорошо, спасибо, видимо действительно придётся менять исходники на C.

    И последний вопрос, действительно нет возможности, запустить новый процесс из текущего программно?

    8 ноября 2013 г. 10:07
  • Ну, почему же, есть. Посмотрите, например, вот этот класс.
    8 ноября 2013 г. 10:10
  • Хорошо, спасибо.

    А что, если в библиотеки C++/CLI, для каждого случая создавать новый поток?

    8 ноября 2013 г. 10:16
  • Потоки все равно будут "крутиться" в рамках одного процесса и обращаться к одним и тем же глобальным переменным.
    8 ноября 2013 г. 10:20