BUAA OO Unit 2 Summary

BUAA OO Unit 2 Summary

Part 1 design strategy

These three jobs use the main thread to obtain the request, the multi-level scheduler dispatches step by step, and the elevator simulates the strategy of running . Specifically, the main thread instantiates the ElevatorInputclass, obtains the request by blocking reading Request, and then assigns the request to the scheduler Scheduler. The scheduler is responsible for processing the request (either by itself or by other sub-schedulers), each The elevator is ElevatorSchedulerbound to one for specific scheduling, sleep control, and printout.

The main difficulties of this operation are as follows:

  • How to control the termination of the dispatcher and elevator threads : In the simple producer-consumer model, producers continue to produce and consumers continue to consume, and there is no thread termination; the actual elevator runs 24 hours a day, and there are no abnormal conditions. Will not terminate. But more multi-threading issues need to consider thread termination. The same is true for these three jobs: after the main thread sends all requests to the scheduler, it tells the scheduler that it is ready to end, and the scheduler ends the thread after processing the remaining requests in its queue.

    A relatively easy way to think of is to use a interruptmechanism

    while (true) {
        Request request = elevatorInput.nextRequest();
        if (request == null) {
            scheduler.interrupt();
        } else {
            // pass
        }
    }
    

    But this method cannot achieve precise control: what we hope is that if the scheduler is waiting for the next input (in the wait()function), it will be interrupted; and if it is performing other tasks, such as sleep(100), or other synchronization tasks, Without interruption. Although this situation does not appear in the work of this unit, it is reasonable to think more.

    The other method is to set a setInputEnd()method specifically , which is called by the main thread to inform the scheduler that the input ends.

    class Scheduler {
        boolean inputEnd = false;
        Queue<Request> requestQueue;
        public synchronized void setInputEnd() {
            this.inputEnd = true;
            notifyAll();
        }
        public synchronized void addRequest() {}
        public synchronized void getRequest() {
            while (requestQueue.isEmpty()) {
                wait();
            }
            return requestQueue.poll();
        }
    }
    

    I used this method in my homework, but later discovered that it can actually be solved in a more concise way: create a TerminationRequestclass.

    interface Request {}
    class PersonRequest implements Request {}
    class ElevatorRequest implements Request {}
    class TerminationRequest implements Request {}
    

    In this way, through normalization, a queue can be used for unified management. At the same time, this normalization also facilitates the use of thread-safe containers.

  • Distinguish between two acquisition request modes : In the simple producer-consumer mode, consumers will always waitwait in functions in the shared area when there is no product , but in many cases of actual life, this kind of The situation is unacceptable-consumers may have other things to complete. Using waitfunctions to wait and release CPU resources is certainly an improvement, but this scheme also restricts consumers' freedom to carry out other activities. Returning to the operation of this unit, every elevator needs to realize its behavior: get a new request and determine the behavior of the elevator (opening, closing, ascending, descending, etc.). However, these two actions are not always necessary: ​​if the local queue of the elevator is empty and there are no passengers in the elevator, the elevator is idle. At this time, it is not necessary to frequently decide the behavior of the elevator, just wait for the next request. arrival. Therefore, when the elevator is idle, the request should be read with congestion, that is, waiting for the next request at the request; and when the elevator is busy, you only need to view and update the request, and there is no new request and it is not blocked .

    These two different modes can be solved by yourself, such as:

    class ElevatorScheduler {
        private Queue<Request> queue;
        private Queue<Request> localQueue;
        private synchronized Request getRequest(boolean quickRetreat) {
            if (quickRetreat) {
                return queue.poll(); // if the queue is empty, return null
            } else {
                while (queue.isEmpty()) {
                    wait();
                }
                return queue.poll();
            }
        }
    }
    

    You can also use Java built-in BlockingQueueto solve:

    private BlockingQueue<Request> queue;
    private void update() {
        if (idle) {
            localQueue.add(queue.take());
        }
        queue.drainTo(localQueue);
    }
    
  • Flexible distributor : the first job has only one elevator; the second job has multiple elevators, but only one model; the third job has different elevators, and the number of elevators under each elevator model is different . It can be considered that the elevator for the first operation only needs a first-level dispatcher (the dispatcher that directly commands the elevator), and the elevator for the second operation is a two-level dispatcher (the first level is responsible for load balancing seen by the elevator, and the other level is directly responsible Command the elevator), the third operation of the elevator is three-level scheduling (the first-level master dispatcher is responsible for transfer-related management, the first level is responsible for the load balancing of the same type of elevator, and the first level is responsible for directing the elevator). As shown below:

    In order to make the distribution more flexible, it is Schedulerenough to design a unified interface for these RequestReceiver. As for the internal processing, or to allocate or command the elevator by itself, the request provider does not have to care.

    interface RequestReceiver {
        void addRequest (Request r);
    }
    class CentralScheduler implements RequestReceiver {}
    class Scheduler implements RequestReceiver {}
    class ElevatorScheduler implements RequestReceiver {}
    
  • Feedback and closed-loop control : In actual multithreaded programming, feedback and closed-loop control are also very common. The operation of this unit is no exception: the transfer requires the feedback of the request, that is, after the elevator runs a part of the request, another elevator continues to complete the other part of the request. Since the elevator is controlled step by step, after the elevator processes the part of the request that it should handle, it needs to feed back the request to the superior dispatcher, and the superior dispatcher makes the secondary allocation. On the other hand, the scheduling algorithm also needs to consider the load balancing problem of each elevator when scheduling, so the elevator must also report its own load situation.

    In these few jobs, you can provide a feedback interface through the corresponding thread class to perform a step-by-step feedback state:

    interface FeedbackReceiver {
        void offerFeedback (Feedback fb);
        void offerRequestFeedback (Collection<PersonRequest> requests);
    }
    class CentralScheduler implements RequestReceiver, FeedbackReceiver {}
    class Scheduler implements RequestReceiver, FeedbackReceiver {}
    

    When the feedback is backpropagated, each stage Schedulercan also process the feedback, such as the load in job 3, and the load of each type of elevator can be the load of the elevator with the smallest load among all elevators of this type.

  • Floor mapping : In fact, this problem does not have any object-oriented difficulties, mainly a little trick. Each elevator dispatcher (a dispatcher that directly directs elevators to move, which implements a dispatch algorithm) has a mapping that enables rapid conversion from floor to floor subscript.

    Instead of using mathematical methods (piecewise function):

    int flr_to_ind (int flr) {
        if (/* some conditions */) {
            // do something
        } else if (/* ... */) {
            // do something
        } else {
            // pass
        }
    }
    

    It is better to use Java's own method:

    List<Integer> flrs = Arrays.asList(-3, -2, -1, 1, 2, 3);
    index = flrs.indexOf(flr);
    flr = flrs.get(index);
    

Part 2 The scalability of the third job

If my third assignment really realized the ideas and methods described in the first part, then it would not be too complicated to expand. But in fact, my third assignment did not fully implement these methods and techniques-the subject part of the program was built during the fifth assignment, and only minor modifications were made afterwards. But after all, the structure is similar, you can also do some analysis:

  • Implement emergency braking : Requestadd an implementation of an emergency braking request from the interface, and the dispatcher dispatches this request to the corresponding elevator. When the elevator arrives at the next stop, all outstanding requests are fed back through the feedback channel, which is allocated again by the upper-level dispatcher and the elevator thread ends.
  • More complex elevator types : construct an elevator factory, use factory mode , and produce corresponding elevators according to the elevator models provided. In terms of the dispatcher, several secondary dispatchers are added, so that each elevator type corresponds to a dispatcher. (Of course, if the type increment is not large, it is also feasible to merge this scheduler with the main scheduler)
  • Larger scale : increase the number of scheduling levels to achieve more fine-grained scheduling.

From the perspective of SOLID :

  • Single Responsibility Principle : The scheduler generally conforms to this principle, and the elevator class has a lower degree of compliance. In my design, the elevator class acts as a container to manage the passengers in the elevator, and is also responsible for output and sleep, and the requested management ElevatorSchedulerpartially overlaps with the responsibilities of the class, and the coupling is too high. In the initial design, the functions of the elevator were defined as responsible for output and sleep (because these two aspects are relatively fixed and can be ElevatorSchedulerseparated from volatile , but in the later iterative development, the functions are gradually expanded.
  • Open Close Principle : These three tasks are more in line with this principle at the function level (that is, the method level of each class). Separate the variable classes from the variable classes. Iterative development mainly replaces some functions without having to modify the functions on a large scale. . But at the class level, the degree of compliance with this principle is low. In the original design, I originally planned to write each major class as an abstract class first, and then implement it by inheriting the abstract class, but in the end it felt unrealistic, and it was not really implemented, but directly changed the abstract class into a concrete class … [Cover face] (Maybe small projects are not easy to achieve OCP, after all, there are only a few categories)
  • Liskov Substitution Principle : These three assignment relationships are relatively few, but basically all existing inheritance relationships satisfy the LSP principle.
  • Segregation Principle Interface : In fact, the third time job and did not use the interface, but if you follow the first part of the analysis, each dispatcher implements RequestReceiver, FeedBackReceiver, Runnablemethod, ISP can be considered a bit mean.
  • Dependency Inversion Principle : After all, there is no interface, and the inheritance relationship is relatively small. The specific implementation of the third job does not actually reflect this principle, but if according to the analysis of the first part, the mainfunction thread only depends on the RequestReceiverinterface, which also means a little DIP Not realized).

Part 3 Classic metrics

Considering that the structure of the three jobs is in the same line, and there is no major change in each iteration, only the last job is analyzed.

UML diagram :

Only the second-level dispatch structure is implemented here, which PersonTransferis a PersonRequestsubclass of the class provided by the course group and represents the passenger request for transfer. The two-level dispatch structure can solve the problem of these three jobs. The main function obtains the request, and then the high-level dispatcher dispatches it to the low-level dispatcher. The low-level dispatcher cooperates with the elevator class to implement the look elevator scheduling algorithm. In the implementation of the algorithm, it is necessary to manage floor information and user request information. These managements are handled by the building class and floor class, and the FloorNumberManagerclass is set at the same time , providing some static methods to manage services such as floor mapping and reachability query.

The main problem with this structure is that the relationship between elevators Elevatorand elevator dispatchers is not handled properly ElevatorScheduler. The elevator dispatcher class only has one elevator, which is responsible for more detailed scheduling management of this elevator. For example, each time node determines the actions of elevator up, down, door opening, and door closing, and mainly implements the algorithm. But at the same time, the elevator class is not only responsible for output and sleep, but also responsible for managing the internal personnel of the elevator, checking the passengers arriving at the destination, and feeding back passenger information inside the elevator. In a specific implementation, the elevator class exposes its own container to the elevator management class, making the coupling between the two classes high.

In addition, the elevator class Elevatordoes not become an independent thread, so when the elevator sleeps, it is actually ElevatorSchedulersleeping in the thread, which causes the elevator sleep and the elevator scheduling algorithm to run in parallel, reducing efficiency.

Complexity :

It can be seen that the scheduler is a more complex class, and the method responsible for the algorithm in the scheduler is the more complicated method in the scheduler. But in addition to the dispatcher, the elevator class is also more complicated, which is inconsistent with the original intention of the design. The reason was also mentioned above, mainly because with the advancement of the code, the functions of the elevator class continue to expand, overlapping with the scheduling class, and this problem is not well dealt with.

Collaboration diagram :

Part 4 bug analysis

The main bug-prone areas of this unit include:

  • Program deadlocks, especially when the output is terminated, when the thread is ready to end.
  • Pseudo-multithreading mainly refers to the fact that the program is polling in essence, and the resources are not well realized.
  • Demand understanding deviation.

I did n’t find any bugs in the strong test and mutual test of the three assignments, but my first assignment (the fifth assignment of the entire course) had a very serious error (strong test and mutual test, due to the fixed test mechanism, None detected): If the input end signal is not provided immediately after all input is completed, the program will enter a deadlock and cannot be terminated. Part of the code is as follows:

// methods of ElevatorScheduler
public synchronized void setInputEnd() {
    this.inputEnd = true;
    notifyAll();
}
private synchronized void update(boolean quickRetreat) {
    if ( (inputEnd || quickRetreat) && buffer.isEmpty() ) {
        return;
    }
    while (buffer.isEmpty()) {
        try {
            wait();
            /* when the thread is notified, it's still in the while loop */
        } catch (InterruptedException e) {
            return;
        }
    }
    // some updates here
}

It can be seen that although the program exits the wait()function, it will still enter the wait()function again , causing a deadlock. A simple fix is whileto add a condition to the loop; the design fix I mentioned in the first part, TerminationRequestafter the thread is detected update, the function is no longer called , and the problem is solved inside the thread .

Another problem is that the yield of CPU resources is not handled properly, which is caused by polling CPU_TLE. Generally speaking, you need to wait when the thread is idle and use the wait()function. Once the thread needs to be woken up, the corresponding lock notifyAll()function must be called. In the first mutual test of elevator operation, I found two solid bugs of house A with data points similar to the following data:

[5.0]1-FROM-2-TO-3
[150.0]2-FROM-14-TO-5

Part 5 test program analysis

This unit test program mainly considers its own use, including three parts:

  1. The time mapper of the input file maps the input of the file with time to the time axis to realize the timing input. See the specific code: https://github.com/YushengZhao/BUAA_OO_elevator_input_mapper

  2. The elevator simulator simulates the real operation of the elevator, and checks related problems during the operation. The main idea is to convert the elevator request and the output of the tested program into several elevator commands, sort them according to time, simulate the operation on the simulator, record the parameters and check the behavior during the operation, and finally give a performance report.

  3. The request instruction generator can customize several request sequences, each of which can set parameters, please refer to the following code:

    def generator(size=10, timescale=10, id_start=1, time_shift=1):
        # generate one segment of requests
        pass
    
    def generate():
        periods	 = 	[12,19,8,4,13]
        sizes	 = 	[8,4,10,13,16]
        s = []
        id = 1
        time_shift = 0.3
        for i, period in enumerate(periods):
            s += generator(size=sizes[i],timescale=period,
                           id_start=id,time_shift=time_shift)
            id += (sizes[i]+1)
            time_shift += (period+0.1)
    

The evaluation machine can be generated by connecting these components , but considering its own actual needs, there is no specific implementation.

One noteworthy point: many students use python scripts for time mapping. After referring to the blogs of some seniors, I found that this method is prone to time deviation and time control is not very accurate, and the time mapper is embedded in Java. More precise control can be achieved within the language. At the same time, this is also convenient for debugging.

Regarding the request generation strategy: actual applications may have different loads in different periods of time. I generate requests by segments in the evaluation machine, which can simulate this situation. After conducting several to dozens of tests (total request 1e2magnitude), there are generally no significant problems; performing a large number of tests ( 1e3,1e4magnitude tests) may reveal some problems, but considering the time cost of each test , There will not be too many tests. In real practical applications, a lot of testing is definitely necessary.

Compared with the previous unit, the debugging and testing of this unit is mainly due to the time factor . When testing, it is necessary to consider the different characteristics of the input distribution over time, such as a large number of inputs at a time point, no input for a period of time, etc Wait. When debugging, since the breakpoint debugging method cannot be used, I generally use the logging method and add a pluggable logger:

private static final boolean LOGGER = true;
private static final boolean ASSERTION = true;
public static void log(String msg) {
    if (LOGGER) {
        System.out.println(msg);
    }
}
public static void ast(boolean condition, String msg) { // ast == assert
    if (ASSERTION && !condition) {
        System.out.println("Assertion failed: "+msg);
    }
}

Or implement a levellog output with different importance:

private static final int LEVEL = 5;
public static void log(int level, String msg) {
    if (level >= LEVEL) {
        System.out.println(msg);
    }
}

Of course, Java also has a corresponding logging mechanism, but considering that this unit job does not require complex logging, it is not used.

Part 6 Experience

  1. In terms of multi-threading, I spent a relatively long time doing the first job of the elevator (I even thought I was going to stop there). At that time, there were many problems that I did not want to know, and there were many logical confusions in the final code. . The reason why multi-threading is prone to various safety problems is ultimately the complexity of the thread's own behavior logic . For example, a simple producer-consumer model, basically no one writes the problem of thread safety, but a more complicated producer-consumer Models (such as consumers do not call waitfunctions when there is no product , but perform other activities), it is prone to thread safety problems. Therefore, it is very important to establish a simple and general thread model according to the requirements -simple logic is often not easy to make mistakes. For example, in the observer mode, a model is built in which one thread publishes messages and several threads receive messages; in turn, sometimes several threads are needed to send messages. If a certain thread receives messages, then the message queue can be used:

    class Scheduler {
        private Queue<Message> messageQueue;
        public synchronized void addMsg() {/*some code here*/}
        public synchronized Message getMsg() {/*some code here*/}
    }
    interface Message {}
    class Feedback implements Message {}
    interface Request extends Message {}
    

    All Schedulermessages that need to be notified are passed in by addMsgmethod, regardless of their specific content, and then processed separately by other functions. Therefore, when Schedulerthe remaining tasks are processed, you can getMsgwait directly in the method.

  2. In terms of design principles, when I completed the first elevator operation, I did not have much awareness of the design principles, and because the structure of the first operation was used in the last two operations, there was not much design in all the last two operations. In principle, this is regrettable.

  3. In terms of code refactoring, I started from this unit with a firm attitude of not refactoring . From the planning group to the present, in various projects that require iterative development, I always feel that the previous writing is not good enough and I want to refactor, but often overlook the risk of refactoring and the possibility of not refactoring. In actual development, refactoring must be avoided as much as possible. It is actually the norm to modify the original code (probably seemingly messy code). In fact, the seemingly messy code may not be as bad as expected. I tried refactoring during the second elevator operation, but I did n’t find out until I actually wrote the refactoring code. If refactoring, many codes are similar, so many design considerations are reasonable .

  4. In terms of algorithms, the operations of this unit leave a lot of room for the algorithms. But to get a high score does not require a very complicated algorithm. The most basic look algorithm can basically achieve more than 95 points in the case of strong testing without errors. After the second two jobs, add some load balancing considerations, if there are no errors, you can basically get more than 95 points. Nonetheless, there is no harm in discussing some algorithms:

    • Decoupling elevator printout / sleep and elevator operation logic . The purpose of this is that the total running time of the elevator can be estimated without running the elevator (operating a virtual elevator), and further, the total running time after adding a request. In theory, this can always achieve local optimal planning. Moreover, this kind of estimation does not depend on a specific algorithm, and the flexibility is relatively strong.
    • Model with Markov decision process . This is done considering that there are many algorithms available for Markov Decision Process.

    Of course, these algorithms are not implemented by me. After all, the main purpose of the course is not algorithms.


other:

  1. Some problems are not actually OOP problems, and some bugs are not because of thread safety. For example, look algorithm, I spent a lot of time to implement and debug.
  2. Many times it is also a good way to imitate real world entities and relationships. For example, arrange one Buildingfor each elevator , Buildingarrange several for each Floor, and provide "up button" and "down button" interfaces, just like the elevator in real life. The elevator does not know how many people there are on a floor, but only knows whether there is a person on a floor who wants to go upstairs and whether someone wants to go downstairs.
  3. The second idea is very helpful for us to write homework, but is it referring to the code structure of the programmer who actually writes the program to the elevator? In fact, I have more or less referred to the previous classmates ’blogs for several homework assignments. If there is no such reference, what structure can I write?

Guess you like

Origin www.cnblogs.com/yszhao/p/12706395.html