1

私のアプリケーション内には、データベースからの人物のリストがあります。情報を検索するために、1 人につき 5 つの (今のところ) サービスを呼び出す必要があります。サービスが情報を返す場合、その人に追加します (特定の人の注文のリスト)
サービスは独立して動作するため、それらを並行して実行できると考えました。私は自分のコードを次のように作成しました:

using System;
using System.Collections.Generic;
using System.Threading;

namespace Testy
{
    internal class Program
    {
        internal class Person
        {
            public int Id { get; set; }
            public string Name { get; set; }
            public List<string> Orders { get; private set; }

            public Person()
            {
                // thanks for tip @juharr
                Orders = new List<string>();
            }

            public void AddOrder(string order)
            {
                lock (Orders) //access across threads
                {
                    Orders.Add(order);
                }
            }
        }

        internal class Service
        {
            public int Id { get; private set; }

            public Service(int id)
            {
                Id = id;
            }

            //I get error when I use IList instead of List
            public void Search(ref List<Person> list) 
            {
                foreach (Person p in list)
                {
                    lock (p) //should I lock Person here? and like this???
                    {
                        Search(p);
                    }
                }
            }
            private void Search(Person p)
            {
                Thread.Sleep(50);
                p.AddOrder(string.Format("test order from {0,2}",
                                      Thread.CurrentThread.ManagedThreadId));
                Thread.Sleep(100);
            }
        }

        private static void Main()
        {
            //here I load my services from external dll's
            var services = new List<Service>();
            for (int i = 1; i <= 5; i++)
            {
                services.Add(new Service(i));
            }

            //sample data load from db    
            var persons = new List<Person>();

            for (int i = 1; i <= 10; i++)
            {
                persons.Add(
                    new Person {Id = i, 
                    Name = string.Format("Test {0}", i)});
            }

            Console.WriteLine("Number of services: {0}", services.Count);
            Console.WriteLine("Number of persons: {0}", persons.Count);

            ManualResetEvent resetEvent = new ManualResetEvent(false);
            int toProcess = services.Count;

            foreach (Service service in services)
            {
                new Thread(() =>
                    {
                        service.Search(ref persons);
                        if (Interlocked.Decrement(ref toProcess) == 0)
                            resetEvent.Set();
                    }
                    ).Start();
            }

            // Wait for workers.
            resetEvent.WaitOne();

            foreach (Person p in persons)
            {
                Console.WriteLine("{0,2} Person name: {1}",p.Id,p.Name);
                if (null != p.Orders)
                {
                    Console.WriteLine("    Orders:");
                    foreach (string order in p.Orders)
                    {
                        Console.WriteLine("    Order: {0}", order);
                    }
                }
                else
                {
                    Console.WriteLine("    No orders!");
                }
            }
            Console.ReadLine();
        }
    }
}

私のコードには2つの問題があります:

  1. アプリを実行すると、10 人のリストを取得し、各人に対して 5 つの注文を取得する必要がありますが、時々 (3 ~ 5 回の実行の場合) 最初の人の注文は 4 つしか取得できません。どうすればそのような行動を防ぐことができますか?
    解決しました!@juharrに感謝
  2. スレッドから進捗状況を報告するにはどうすればよいですか? 取得したいのは、サービスから注文が追加されるたびに呼び出される Program クラスの 1 つの関数です。レポートごとに何らかの進行状況を表示する必要があります。https://stackoverflow.com/a/3874184/965722
    で説明されている解決策を試していましたが、もっと簡単な方法があるかどうか疑問に思っています。理想的には、デリゲートをクラスに追加し、そこにすべてのスレッド コードを配置したいと考えています。クラスにイベントとデリゲートを追加する方法と、Main メソッドでサブスクライブする方法を教えてください。Service
    Service

.NET 3.5 を使用しています

進捗レポートを取得できるように、次のコードを追加しました。

using System;
using System.Collections.Generic;
using System.Threading;

namespace Testy
{
    internal class Program
    {
        public class ServiceEventArgs : EventArgs
        {
            public ServiceEventArgs(int sId, int progress)
            {
                SId = sId;
                Progress = progress;
            }

            public int SId { get; private set; }
            public int Progress { get; private set; }
        }

        internal class Person
        {
            private static readonly object ordersLock = new object();

            public int Id { get; set; }
            public string Name { get; set; }
            public List<string> Orders { get; private set; }

            public Person()
            {
                Orders = new List<string>();
            }

            public void AddOrder(string order)
            {
                lock (ordersLock) //access across threads
                {
                    Orders.Add(order);
                }
            }
        }

        internal class Service
        {
            public event EventHandler<ServiceEventArgs> ReportProgress;

            public int Id { get; private set; }
            public string Name { get; private set; }

            private int counter;

            public Service(int id, string name)
            {
                Id = id;
                Name = name;
            }

            public void Search(List<Person> list) //I get error when I use IList instead of List
            {
                counter = 0;
                foreach (Person p in list)
                {
                    counter++;
                    Search(p);
                    Thread.Sleep(3000);
                }
            }

            private void Search(Person p)
            {
                p.AddOrder(string.Format("Order from {0,2}", Thread.CurrentThread.ManagedThreadId));

                EventHandler<ServiceEventArgs> handler = ReportProgress;
                if (handler != null)
                {
                    var e = new ServiceEventArgs(Id, counter);
                    handler(this, e);
                }
            }
        }

        private static void Main()
        {
            const int count = 5;
            var services = new List<Service>();
            for (int i = 1; i <= count; i++)
            {
                services.Add(new Service(i, "Service " + i));
            }

            var persons = new List<Person>();

            for (int i = 1; i <= 10; i++)
            {
                persons.Add(new Person {Id = i, Name = string.Format("Test {0}", i)});
            }

            Console.WriteLine("Number of services: {0}", services.Count);
            Console.WriteLine("Number of persons: {0}", persons.Count);
            Console.WriteLine("Press ENTER to start...");
            Console.ReadLine();

            ManualResetEvent resetEvent = new ManualResetEvent(false);
            int toProcess = services.Count;

            foreach (Service service in services)
            {
                new Thread(() =>
                    {
                        service.ReportProgress += service_ReportProgress;
                        service.Search(persons);
                        if (Interlocked.Decrement(ref toProcess) == 0)
                            resetEvent.Set();
                    }
                    ).Start();
            }

            // Wait for workers.
            resetEvent.WaitOne();

            foreach (Person p in persons)
            {
                if (p.Orders.Count != count)
                    Console.WriteLine("{0,2} Person name: {1}, orders: {2}", p.Id, p.Name, p.Orders.Count);
            }
            Console.WriteLine("END :)");
            Console.ReadLine();
        }

        private static void service_ReportProgress(object sender, ServiceEventArgs e)
        {
            Console.CursorLeft = 0;
            Console.CursorTop = e.SId;
            Console.WriteLine("Id: {0,2}, Name: {1,2} - Progress: {2,2}", e.SId, ((Service) sender).Name, e.Progress);
        }
    }
}

Service クラスのイベントであるカスタム EventArgs を追加しました。この構成では、5 つのサービスを実行する必要がありますが、進行状況を報告するのはそのうちの 3 つだけです。
5 つのサービスがある場合、5 つのイベント (進行状況を示す 5 行) が必要であると想像しました。
これはおそらくスレッドが原因ですが、これを解決する方法がわかりません。

サンプル出力は次のようになります。

Number of services: 5
Number of persons: 10
Press ENTER to start...
Id:  3, Name: Service 3 - Progress: 10
Id:  4, Name: Service 4 - Progress: 10
Id:  5, Name: Service 5 - Progress: 19
END :)

次のようになります。

Number of services: 5
Number of persons: 10
Press ENTER to start...
Id:  1, Name: Service 1 - Progress: 10
Id:  2, Name: Service 2 - Progress: 10
Id:  3, Name: Service 3 - Progress: 10
Id:  4, Name: Service 4 - Progress: 10
Id:  5, Name: Service 5 - Progress: 10
END :)

最後の編集
で、すべてのスレッド作成を別のクラスに移動しましたServiceManager。コードは次のようになります。

using System;
using System.Collections.Generic;
using System.Threading;

namespace Testy
{
    internal class Program
    {
        public class ServiceEventArgs : EventArgs
        {
            public ServiceEventArgs(int sId, int progress)
            {
                SId = sId;
                Progress = progress;
            }

            public int SId { get; private set; } // service id
            public int Progress { get; private set; }
        }

        internal class Person
        {
            private static readonly object ordersLock = new object();

            public int Id { get; set; }
            public string Name { get; set; }
            public List<string> Orders { get; private set; }

            public Person()
            {
                Orders = new List<string>();
            }

            public void AddOrder(string order)
            {
                lock (ordersLock) //access across threads
                {
                    Orders.Add(order);
                }
            }
        }

        internal class Service
        {
            public event EventHandler<ServiceEventArgs> ReportProgress;

            public int Id { get; private set; }
            public string Name { get; private set; }

            public Service(int id, string name)
            {
                Id = id;
                Name = name;
            }

            public void Search(List<Person> list)
            {
                int counter = 0;
                foreach (Person p in list)
                {
                    counter++;
                    Search(p);
                    var e = new ServiceEventArgs(Id, counter);
                    OnReportProgress(e);
                }
            }

            private void Search(Person p)
            {
                p.AddOrder(string.Format("Order from {0,2}", Thread.CurrentThread.ManagedThreadId));
                Thread.Sleep(50*Id);
            }

            protected virtual void OnReportProgress(ServiceEventArgs e)
            {
                var handler = ReportProgress;
                if (handler != null)
                {
                    handler(this, e);
                }
            }
        }

        internal static class ServiceManager
        {
            private static IList<Service> _services;

            public static IList<Service> Services
            {
                get
                {
                    if (null == _services)
                        Reload();
                    return _services;
                }
            }

            public static void RunAll(List<Person> persons)
            {
                ManualResetEvent resetEvent = new ManualResetEvent(false);
                int toProcess = _services.Count;

                foreach (Service service in _services)
                {
                    var local = service;
                    local.ReportProgress += ServiceReportProgress;
                    new Thread(() =>
                        {
                            local.Search(persons);
                            if (Interlocked.Decrement(ref toProcess) == 0)
                                resetEvent.Set();
                        }
                        ).Start();
                }
                // Wait for workers.
                resetEvent.WaitOne();
            }

            private static readonly object consoleLock = new object();

            private static void ServiceReportProgress(object sender, ServiceEventArgs e)
            {
                lock (consoleLock)
                {
                    Console.CursorTop = 1 + (e.SId - 1)*2;
                    int progress = (100*e.Progress)/100;
                    RenderConsoleProgress(progress, '■', ConsoleColor.Cyan, String.Format("{0} - {1,3}%", ((Service) sender).Name, progress));
                }
            }

            private static void ConsoleMessage(string message)
            {
                Console.CursorLeft = 0;
                int maxCharacterWidth = Console.WindowWidth - 1;
                if (message.Length > maxCharacterWidth)
                {
                    message = message.Substring(0, maxCharacterWidth - 3) + "...";
                }
                message = message + new string(' ', maxCharacterWidth - message.Length);
                Console.Write(message);
            }

            private static void RenderConsoleProgress(int percentage, char progressBarCharacter,
                                                      ConsoleColor color, string message)
            {
                ConsoleColor originalColor = Console.ForegroundColor;
                Console.ForegroundColor = color;
                Console.CursorLeft = 0;
                int width = Console.WindowWidth - 1;
                var newWidth = (int) ((width*percentage)/100d);
                string progBar = new string(progressBarCharacter, newWidth) + new string(' ', width - newWidth);
                Console.Write(progBar);
                if (!String.IsNullOrEmpty(message))
                {
                    Console.CursorTop++;
                    ConsoleMessage(message);
                    Console.CursorTop--;
                }
                Console.ForegroundColor = originalColor;
            }

            private static void Reload()
            {
                if (null == _services)
                    _services = new List<Service>();
                else
                    _services.Clear();

                for (int i = 1; i <= 5; i++)
                {
                    _services.Add(new Service(i, "Service " + i));
                }
            }
        }

        private static void Main()
        {
            var services = ServiceManager.Services;
            int count = services.Count;

            var persons = new List<Person>();

            for (int i = 1; i <= 100; i++)
            {
                persons.Add(new Person {Id = i, Name = string.Format("Test {0}", i)});
            }

            Console.WriteLine("Services: {0}, Persons: {1}", services.Count, persons.Count);
            Console.WriteLine("Press ENTER to start...");
            Console.ReadLine();
            Console.Clear();
            Console.CursorVisible = false;

            ServiceManager.RunAll(persons);

            foreach (Person p in persons)
            {
                if (p.Orders.Count != count)
                    Console.WriteLine("{0,2} Person name: {1}, orders: {2}", p.Id, p.Name, p.Orders.Count);
            }
            Console.CursorTop = 12;
            Console.CursorLeft = 0;
            Console.WriteLine("END :)");
            Console.CursorVisible = true;
            Console.ReadLine();
        }
    }
}
4

2 に答える 2

1

基本的に、注文の作成で競合状態が発生します。次の 2 つのスレッドの実行を想像してください。

スレッド 1 は、Orders が null であるかどうかを確認します。
スレッド 2 は、Orders が null であるかどうかを確認します。
スレッド 1 は Orders を新しいリストに設定します。
スレッド 1 がロックを取得します。
スレッド 1 が Orders リストに追加されます。
スレッド 2 は Order を新しいリストに設定します。(スレッド1が追加したものを失っただけです)

注文の作成をロック内に含める必要があります。

public void AddOrder(string order)
{
    lock (Orders) //access across threads
    {
        if (null == Orders)
            Orders = new List<string>();
        Orders.Add(order);
    }
}

または、 Person コンストラクターで Order リストを作成する必要があります

public Person()
{
    Orders = new List<Order>();
}

また、ロック用に別のオブジェクトを実際に作成する必要があります。

private object ordersLock = new object();


public void AddOrder(string order)
{
    lock (ordersLock) //access across threads
    {
        Orders.Add(order);
    }
}

編集:

スレッドを作成する foreach では、ラムダ式内で使用するサービスのローカル コピーを作成する必要があります。これは、foreach がサービス変数を更新し、スレッドが間違った変数をキャプチャしてしまう可能性があるためです。だから、このようなもの。

foreach (Service service in services)
{
    Service local = service;
    local.ReportProgress += service_ReportProgress;
    new Thread(() =>
        {
            local.Search(persons);
            if (Interlocked.Decrement(ref toProcess) == 0)
                resetEvent.Set();
        }
    ).Start();
}

サブスクリプションはスレッド内にある必要はないことに注意してください。

または、スレッドの作成をクラス のSearchメソッド内に移動することもできます。Service

さらに、次のようOnReportProgressにクラスにメソッドを作成することもできます。Service

protected virtual void OnReportProgress(ServiceEventArgs e)
{
    EventHandler<ServiceEventArgs> handler = ReportProgress;
    if (handler != null)
    {
        handler(this, e);
    }
}

次に、メソッド内でそれを呼び出しますSearch。個人的には、パブリックSearchメソッドで呼び出し、カウンターをローカル変数にして、Service別のリストでオブジェクトを再利用できるようにします。

最後に、別のスレッドが出力を書き込む前に、あるスレッドがカーソル位置を変更しないように、コンソールに書き込むときにイベント ハンドラーに追加のロックが必要になります。

private static object consoleLock = new object();

private static void service_ReportProgress(object sender, ServiceEventArgs e)
{
    lock (consoleLock)
    {
        Console.CursorLeft = 0;
        Console.CursorTop = e.SId;
        Console.WriteLine("Id: {0}, Name: {1} - Progress: {2}", e.SId, ((Service)sender).Name, e.Progress);
    }
}

またConsole.Clear()、次の場所で使用することもできます。

...
Console.WriteLine("Number of services: {0}", services.Count);
Console.WriteLine("Number of persons: {0}", persons.Count);
Console.WriteLine("Press ENTER to start...");
Console.Clear();
Console.ReadLine();
...

また、end ステートメントを書き出す前に、カーソル位置を更新する必要があります。

Console.CursorTop = 6;
Console.WriteLine("END :)");
于 2013-01-28T13:26:44.797 に答える
0

これはあなたの質問に完全には答えないかもしれません (ただし、競合状態があると思います)。スレッドの処理を開始するときは、異なるスレッドからオブジェクトを更新するときに適切な同期を実装する必要があります。常に 1 つのスレッドだけが person クラスのインスタンスを更新できるようにする必要があります。p.AddOrder( には、1 つのスレッドのみが Person オブジェクトを更新することを保証するミューテックスが必要です。

于 2013-01-28T13:27:17.307 に答える