Tôi nên đặt bao nhiêu công việc bên trong một tuyên bố khóa?


27

Tôi là một nhà phát triển cơ sở đang viết một bản cập nhật cho phần mềm nhận dữ liệu từ giải pháp của bên thứ ba, lưu trữ nó trong cơ sở dữ liệu và sau đó điều kiện dữ liệu được sử dụng bởi một giải pháp bên thứ ba khác. Phần mềm của chúng tôi chạy như một dịch vụ Windows.

Nhìn vào mã từ một phiên bản trước, tôi thấy điều này:

        static Object _workerLocker = new object();
        static int _runningWorkers = 0;
        int MaxSimultaneousThreads = 5;

        foreach(int SomeObject in ListOfObjects)
        {
            lock (_workerLocker)
            {
                while (_runningWorkers >= MaxSimultaneousThreads)
                {
                    Monitor.Wait(_workerLocker);
                }
            }

            // check to see if the service has been stopped. If yes, then exit
            if (this.IsRunning() == false)
            {
                break;
            }

            lock (_workerLocker)
            {
                _runningWorkers++;
            }

            ThreadPool.QueueUserWorkItem(SomeMethod, SomeObject);

        }

Logic có vẻ rõ ràng: Đợi phòng trong nhóm luồng, đảm bảo dịch vụ chưa bị dừng, sau đó tăng bộ đếm luồng và xếp hàng công việc. _runningWorkersđược giảm xuống bên SomeMethod()trong một locktuyên bố mà sau đó gọi Monitor.Pulse(_workerLocker).

Câu hỏi của tôi là: Có bất kỳ lợi ích nào trong việc nhóm tất cả các mã trong một lock, như thế này:

        static Object _workerLocker = new object();
        static int _runningWorkers = 0;
        int MaxSimultaneousThreads = 5;

        foreach (int SomeObject in ListOfObjects)
        {
            // Is doing all the work inside a single lock better?
            lock (_workerLocker)
            {
                // wait for room in ThreadPool
                while (_runningWorkers >= MaxSimultaneousThreads) 
                {
                    Monitor.Wait(_workerLocker);
                }
                // check to see if the service has been stopped.
                if (this.IsRunning())
                {
                    ThreadPool.QueueUserWorkItem(SomeMethod, SomeObject);
                    _runningWorkers++;                  
                }
                else
                {
                    break;
                }
            }
        }

Có vẻ như, nó có thể gây ra thêm một chút chờ đợi cho các luồng khác, nhưng sau đó có vẻ như việc khóa liên tục trong một khối logic duy nhất cũng sẽ hơi tốn thời gian. Tuy nhiên, tôi chưa quen với đa luồng, vì vậy tôi cho rằng có những mối quan tâm khác ở đây mà tôi không biết.

Chỉ có những nơi khác _workerLockerbị khóa SomeMethod()và chỉ nhằm mục đích giảm dần _runningWorkers, sau đó ở ngoài foreachđể chờ số lượng _runningWorkerschuyển về 0 trước khi đăng nhập và quay trở lại.

Cảm ơn vì bất kì sự giúp đỡ.

EDIT 4/8/15

Cảm ơn @delnan về khuyến nghị sử dụng semaphore. Mã trở thành:

        static int MaxSimultaneousThreads = 5;
        static Semaphore WorkerSem = new Semaphore(MaxSimultaneousThreads, MaxSimultaneousThreads);

        foreach (int SomeObject in ListOfObjects)
        {
            // wait for an available thread
            WorkerSem.WaitOne();

            // check if the service has stopped
            if (this.IsRunning())
            {
                ThreadPool.QueueUserWorkItem(SomeMethod, SomeObject);
            }
            else
            {
                break;
            }
        }

WorkerSem.Release()được gọi là bên trong SomeMethod().


1
Nếu toàn bộ khối bị khóa, làm thế nào một sốMethod sẽ có được khóa để giảm _rastyWorkers?
Russell tại ISC

@RussellatISC: ThreadPool.QueueUserWorkItem gọi SomeMethodkhông đồng bộ, phần "khóa" ở trên sẽ được để lại trước hoặc ít nhất là ngay sau khi luồng mới SomeMethodbắt đầu chạy.
Doc Brown

Điểm tốt. Theo hiểu biết của tôi, mục đích của Monitor.Wait()việc phát hành và mua lại khóa để một tài nguyên khác ( SomeMethodtrong trường hợp này) có thể sử dụng nó. Ở đầu bên kia, SomeMethodlấy khóa, giảm bộ đếm và sau đó gọi Monitor.Pulse()các khóa trả về khóa cho phương thức đang đề cập. Một lần nữa, đây là sự hiểu biết của riêng tôi.
Joseph

@Doc, đã bỏ lỡ điều đó, nhưng vẫn ... có vẻ như một sốMethod sẽ cần phải bắt đầu trước khi foreach bị khóa trong lần lặp tiếp theo hoặc nó vẫn sẽ được treo trên khóa được giữ "while (_ruckyWorkers> = MaxSimallelThreads)".
Russell tại ISC

@RussellatISC: như Joseph đã nêu: Monitor.Waitphát hành khóa. Tôi khuyên bạn nên có một cái nhìn vào các tài liệu.
Doc Brown

Câu trả lời:


33

Đây không phải là một câu hỏi về hiệu suất. Đây là câu hỏi đầu tiên và quan trọng nhất. Nếu bạn có hai câu lệnh khóa, bạn không thể đảm bảo tính nguyên tử cho các hoạt động được trải đều giữa chúng hoặc một phần bên ngoài câu lệnh khóa. Phù hợp với phiên bản cũ của mã của bạn, điều này có nghĩa là:

Giữa cuối while (_runningWorkers >= MaxSimultaneousThreads)_runningWorkers++, mọi thứ đều có thể xảy ra , bởi vì mã đầu hàng và lấy lại khóa ở giữa. Ví dụ, luồng A có thể có được khóa lần đầu tiên, đợi cho đến khi có một số luồng khác thoát ra, sau đó thoát ra khỏi vòng lặp và lock. Sau đó, nó được ưu tiên và chủ đề B đi vào hình ảnh, cũng đang chờ phòng trong nhóm chủ đề. Bởi vì nói thread khác bỏ thuốc lá, có phòng vì vậy nó không chờ đợi rất lâu ở tất cả. Cả luồng A và luồng B bây giờ tiếp tục theo thứ tự, mỗi lần tăng _runningWorkersvà bắt đầu công việc của chúng.

Bây giờ, không có cuộc đua dữ liệu nào như tôi có thể thấy, nhưng về mặt logic thì điều đó là sai, vì hiện tại có nhiều hơn các MaxSimultaneousThreadscông nhân đang chạy. Việc kiểm tra (đôi khi) không hiệu quả vì nhiệm vụ lấy một vị trí trong nhóm luồng không phải là nguyên tử. Điều này sẽ liên quan đến bạn nhiều hơn là tối ưu hóa nhỏ xung quanh độ chi tiết khóa! (Lưu ý rằng ngược lại, khóa quá sớm hoặc quá lâu có thể dễ dàng dẫn đến bế tắc.)

Đoạn mã thứ hai khắc phục vấn đề này, theo như tôi có thể thấy. Một thay đổi ít xâm lấn hơn để khắc phục sự cố có thể được đặt ++_runningWorkersngay sau whilegiao diện, bên trong câu lệnh khóa đầu tiên.

Bây giờ, tính chính xác sang một bên, những gì về hiệu suất? Điều này thật khó để nói. Nói chung, khóa trong một thời gian dài hơn ("thô") sẽ ức chế sự tương tranh, nhưng như bạn nói, điều này cần phải được cân bằng với chi phí từ việc đồng bộ hóa thêm của khóa hạt mịn. Nói chung, giải pháp duy nhất là đo điểm chuẩn và nhận thức được rằng có nhiều lựa chọn hơn là "khóa mọi thứ ở mọi nơi" và "chỉ khóa tối thiểu trần". Có rất nhiều mẫu và nguyên thủy đồng thời và cấu trúc dữ liệu an toàn luồng có sẵn. Ví dụ, điều này có vẻ giống như các semaphores ứng dụng đã được phát minh ra, vì vậy hãy xem xét sử dụng một trong số đó thay vì bộ đếm khóa tay cuộn này.


11

IMHO bạn đang hỏi sai câu hỏi - bạn không nên quan tâm quá nhiều đến sự đánh đổi hiệu quả, mà hơn thế nữa là sự đúng đắn.

Biến thể đầu tiên đảm bảo _runningWorkerschỉ được truy cập trong khi khóa, nhưng nó bỏ lỡ trường hợp _runningWorkerscó thể bị thay đổi bởi một luồng khác trong khoảng cách giữa khóa đầu tiên và khóa thứ hai. Thành thật mà nói, mã tìm đến tôi nếu ai đó đã khóa một cách mù quáng xung quanh tất cả các điểm truy cập _runningWorkersmà không nghĩ về ý nghĩa và các lỗi tiềm ẩn. Có lẽ tác giả đã có một số nỗi sợ hãi mê tín về việc thực hiện breaktuyên bố bên trong lockkhối, nhưng ai biết được?

Vì vậy, bạn thực sự nên sử dụng biến thể thứ hai, không phải vì nó hiệu quả hơn hay kém hơn, mà vì nó (hy vọng) đúng hơn so với biến thể thứ nhất.


Mặt khác, giữ một khóa trong khi thực hiện một nhiệm vụ có thể yêu cầu mua một khóa khác có thể gây ra bế tắc mà khó có thể được gọi là hành vi "chính xác". Người ta phải đảm bảo rằng tất cả các mã cần được thực hiện như một đơn vị được bao quanh bởi một khóa chung, nhưng người ta phải di chuyển ra ngoài khóa những thứ không cần phải là một phần của đơn vị đó, đặc biệt là những thứ có thể yêu cầu mua lại các khóa khác .
supercat

@supercat: đây không phải là trường hợp ở đây, vui lòng đọc các bình luận bên dưới câu hỏi ban đầu.
Doc Brown

9

Các câu trả lời khác là khá tốt và giải quyết rõ ràng mối quan tâm chính xác. Hãy để tôi giải quyết câu hỏi chung hơn của bạn:

Tôi nên đặt bao nhiêu công việc bên trong một tuyên bố khóa?

Hãy bắt đầu với lời khuyên tiêu chuẩn, rằng bạn ám chỉ và phân định ám chỉ trong đoạn cuối của câu trả lời được chấp nhận:

  • Làm càng ít việc càng tốt trong khi khóa một đối tượng cụ thể. Các khóa được giữ trong một thời gian dài có thể gây tranh cãi và tranh chấp là chậm. Lưu ý rằng điều này ngụ ý rằng tổng số lượng mã trong một khóa cụ thểtổng số lượng mã trong tất cả các câu lệnh khóa trên cùng một đối tượng đều có liên quan.

  • Có càng ít ổ khóa càng tốt, để làm cho khả năng bế tắc (hoặc livelocks) thấp hơn.

Người đọc thông minh sẽ lưu ý rằng đây là những mặt đối lập. Điểm đầu tiên cho thấy việc chia các ổ khóa lớn thành nhiều ổ khóa nhỏ hơn, mịn hơn để tránh sự tranh chấp. Thứ hai đề nghị hợp nhất các khóa riêng biệt vào cùng một đối tượng khóa để tránh bế tắc.

Những gì chúng ta có thể kết luận từ thực tế rằng lời khuyên tiêu chuẩn tốt nhất là hoàn toàn mâu thuẫn? Chúng tôi nhận được lời khuyên thực sự tốt:

  • Đừng đến đó ngay từ đầu. Nếu bạn đang chia sẻ bộ nhớ giữa các chủ đề, bạn đang mở ra cho mình một thế giới đau khổ.

Lời khuyên của tôi là, nếu bạn muốn đồng thời, hãy sử dụng các quy trình làm đơn vị đồng thời. Nếu bạn không thể sử dụng các quy trình thì hãy sử dụng các miền ứng dụng. Nếu bạn không thể sử dụng các miền ứng dụng, thì hãy quản lý các luồng của bạn bởi Thư viện song song tác vụ và viết mã của bạn theo các nhiệm vụ cấp cao (công việc) thay vì các luồng cấp thấp (công nhân).

Nếu bạn hoàn toàn tích cực phải sử dụng các nguyên hàm đồng thời ở mức độ thấp như các luồng hoặc semaphores, thì hãy sử dụng chúng để xây dựng một bản tóm tắt cấp cao hơn để nắm bắt những gì bạn thực sự cần. Bạn có thể sẽ thấy rằng sự trừu tượng hóa ở cấp độ cao hơn giống như "thực hiện một nhiệm vụ không đồng bộ có thể bị hủy bởi người dùng", và này, TPL đã hỗ trợ điều đó, vì vậy bạn không cần phải tự thực hiện. Bạn có thể sẽ thấy rằng bạn cần một cái gì đó như khởi tạo lười biếng an toàn; không tự lăn, sử dụng Lazy<T>, được viết bởi các chuyên gia. Sử dụng các bộ sưu tập chủ đề (không thay đổi hoặc bằng cách khác) được viết bởi các chuyên gia. Di chuyển mức độ trừu tượng lên càng cao càng tốt.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.