简体   繁体   中英

How to make sure two actions are never executed by different threads at the same time?

Let's assume I have two functions, doAction1() and doAction2() .

Both of those functions can be executed by multiple threads.

Multiple, concurrent activations of doAction1() are allowed, and multiple concurrent activations of doAction2() are allowed, but I do not want to allow any activation of doAction1() to overlap an activation of doAction2() . In other words, if any thread want to execute doAction1() , it should wait until all current executions of doAction2() are finished, and other way around.

If I just wanted doAction2() always to be executed after doAction1() , I could use something like Phaser. Is there something like Phaser with dependencies in both directions? Getting stuck in executing one action all the time is not a concern.

I believe I could have something like thread counters on both actions and have a logic to wait until no threads with other action are active, but I do not like this solution.

I do not have much experience with multithreading, can someone here help?

Well, doAction2 needs to set a lockdown condition which doAction2 itself doesn't need, but which freezes any doAction1 calls.

This is considerably more complicated than it initially sounds because you're set up for a race condition.

Here is one strategy, using primitive operations:

private final Object locker = new Object();
private int a1 = 0, a2 = 0;

public void doAction1() {
    synchronized (locker) {
       //one central lock to avoid race conditions.
       while (a1 > 0) locker.wait();
       a2++;
    }
    
    try {
        doAction1ForReal();
    } finally {
        synchronized (locker) {
            a2--;
            locker.notifyAll();
        }
    }
}

and the same for dA2. You can use Lock objects from juc of course, but not sure how it would end up any cleaner - you want guarantees that you can't deadlock (where a1 and a2 are both non-0, that'd be a deadlock here). Pretty sure this take cannot possibly result in deadlock: a2 cannot possibly be incremented until a1 is 0.

You can build a solution with these patterns:

Bundle the operations as commands that are put into a Queue . A single consumer, running in its own thread, processes each in turn via an ExecutorService . However, it keeps track of the type of the previous item processed (the semaphore); if the next item is not of the same type, it blocks waiting for all processing to complete via a CountDownLatch that the consumer increments before processing and that the processor decrements on completion.

This approach is fair: One type cannot swamp the other.

This is an asynchronous approach, but can be made synchronous by incorporating a callback to the command.

I agree with the comment @Bohemian made about the count downs being unfair. I produced two different solutions using the Ada language because I know Ada better than I know Java. The first example below uses a protected object to provide a form of acquire and release semantics for calls on each action. That example is completely unfair, running only one action until all the tasks calling that action complete.

The protected object in this example is encapsulated in an Ada package. The first file shows the package specification, which contains the interface to the protected object.

package protected_control is
   protected Controller is
      entry Action_1_Get;
      procedure Action_1_Release;
      entry Action_2_Get;
      procedure Action_2_Release;
   private
      A1_Count : Natural := 0;
      A2_Count : Natural := 0;
   end Controller;
   
end protected_control;

The implementation of the protected object Controller is contained in the package body file.

package body protected_control is

   ----------------
   -- Controller --
   ----------------

   protected body Controller is

      ------------------
      -- Action_1_Get --
      ------------------

      entry Action_1_Get when A2_Count = 0 is
      begin
         A1_Count := A1_Count + 1;
      end Action_1_Get;

      ----------------------
      -- Action_1_Release --
      ----------------------

      procedure Action_1_Release is
      begin
         if A1_Count > 0 then
            A1_Count := A1_Count - 1;
         end if;
      end Action_1_Release;

      ----------------------
      -- Action_2_Control --
      ----------------------

      entry Action_2_Get when A1_Count = 0 is
      begin
         A2_Count := A2_Count + 1;
      end Action_2_Get;

      ----------------------
      -- Action_2_Release --
      ----------------------

      procedure Action_2_Release is
      begin
         if A2_Count > 0 then
            A2_Count := A2_Count - 1;
         end if;
      end Action_2_Release;

   end Controller;

end protected_control;

The "main" procedure used to test this package is:

with Ada.Text_IO; use Ada.Text_Io;
with Protected_Control; use Protected_Control;

procedure Protected_test is
   procedure A1 is
   begin
      Put_Line("Starting procedure A1");
      delay 0.6;
      Put_Line("Ending procedure A1");
   end A1;
   
   procedure A2 is
   begin
      Put_Line("Starting procedure A2");
      delay 0.5;
      Put_Line("Ending procedure A2");
   end A2;
   
   task type run_a1;
   task body run_a1 is
   begin
      for I in 1..10 loop
         Controller.Action_1_Get;
         A1;
         Controller.Action_1_Release;
      end loop;
      delay 0.01;
      for I in 1..5 loop
         Controller.Action_1_Get;
         A1;
         Controller.Action_1_Release;
      end loop;
   end run_A1;
   
   task type run_A2;
   task body run_A2 is
      begin
      for I in 1..10 loop
         Controller.Action_2_Get;
         A2;
         Controller.Action_2_Release;
      end loop;
   end run_A2;
   
   A1_runners : array (1..5) of run_A1;
   A2_Runners : array (1..5) of run_A2;
begin
   null;
end Protected_Test;

The results show all action_1 activities in a bunch followed by all action_2 activities, followed by action_1 activities.

A more "fair" implementation uses Ada task entry calls. Ada task entries are a mechanism providing direct synchronous communication between tasks.

The package specification for the coordinating task is:

package Rendezvous_Control is task Do_Actions is entry Action_1; entry Action_2; end Do_Actions;

end Rendezvous_Control;

The package body implementing the controlling task is:

with Ada.Text_IO; use Ada.Text_IO;

package body Rendezvous_Control is

   ----------------
   -- Do_Actions --
   ----------------

   task body Do_Actions is
      Action_1_Count : Natural := 0;
      Action_2_Count : Natural := 0;
      procedure A1 is
      begin
         Action_1_Count := Action_1_Count + 1;
         Put_Line("Starting action 1");
         delay 0.5;
         Put_Line("Ending action 1");
         Action_1_Count := Action_1_Count - 1;
      end A1;
      procedure A2 is
      begin
         Action_2_Count := Action_2_Count + 1;
         Put_Line("Starting action 2");
         delay 0.6;
         Put_Line("Ending action 2");
         Action_2_Count := Action_2_Count - 1;
      end A2;
      
   begin
      loop
         if Action_2_Count = 0 then
            select
               accept Action_1;
               A1;
            or
               terminate;
               
            end select;
         end if;
         if Action_1_Count = 0 then
            select
               Accept Action_2;
               A2;
            or
               terminate;
            end select;
         end if;
      end loop;

   end Do_Actions;

end Rendezvous_Control;

The "main" procedure used to test this approach is:

with Rendezvous_Control; use Rendezvous_Control;

procedure Main is
   task type t1;
   task body T1 is
   begin
      for I in 1..10 loop
         Do_Actions.Action_1;
      end loop;
   end T1;
   
   task type t2;
   task body T2 is
   begin
      for I in 1..10 loop
         Do_Actions.Action_2;
      end loop;
   end T2;
   T1_Arr : array (1..5) of T1;
   T2_Arr : array (1..5) of T2;
begin
   null;
end Main;

The a section of output of this version (truncated for brevity) is shown below:

Starting action 1
Ending action 1
Starting action 2
Ending action 2
Starting action 1
Ending action 1
Starting action 2
Ending action 2
Starting action 1
Ending action 1
Starting action 2
Ending action 2
Starting action 1
Ending action 1
Starting action 2
Ending action 2

The full version simply follows the pattern above, alternating between calling action 1 and action 2.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM