(C #) implement fast response for. Net-based applications through multithreading

Source: Internet
Author: User

If the applicationProgramExecuting non-UI processing on the thread that controls the user interface makes the application running slow and slow, making the user unbearable. However, for a long time, writing multi-threaded Windows applications is limited to C ++ developers. Now with the. NET Framework, You can take full advantage of the multithreading in C # To control the instruction stream in the program, and make the UI thread independent so that the user interface can respond quickly. This article describes how to achieve this goal. In addition, this article will discuss the defects of multithreading and provide a framework to protect the security of concurrent thread execution.

Users do not like slow response programs. The slower the program response, the less users will like it. It is wise to use multithreading when executing a time-consuming operation. It can improve the response speed of the program UI and make everything run faster. Multi-threaded programming in Windows was once the exclusive privilege of C ++ developers, but now, you can use all languages compatible with Microsoft. NET, including Visual Basic. net. However, Windows Forms impose some important restrictions on the use of threads. This article will explain these restrictions and explain how to use them to provide a fast and high-quality UI experience, even if the task to be executed by the program itself is slow.

Why multithreading?

Multi-threaded programs are more difficult to write than single-threaded programs, and the use of threads without selection is also an important cause of difficulty in identifying small errors. This naturally raises two questions: why not insist on writing a single thread?Code? If multithreading is required, how can we avoid defects? Most of this article is about answering the second question, but first I want to explain why multithreading is required.

Multi-threaded processing allows you to maintain a quick UI response by ensuring that the program never sleeps. When most programs do not respond to users: they are busy performing some operations for you to respond to further requests. Perhaps the most widely known example is the pop-up box at the top of the "open file" dialog box. If there is exactly one disc in the CD-ROM drive when you expand the combo box, the computer typically reads the disc before displaying the list. This may take several seconds. During this period, the program neither responds to any input nor allows you to cancel the operation, especially when it does not intend to use the optical drive, this situation is intolerable.

The reason for the UI freeze during this operation is that the UI is a single-threaded program and it is impossible for a single thread to process user input while waiting for the CD-ROM drive to read the operation, as shown in 1. The open file dialog box calls some blocking APIs to determine the title of the CD-ROM. The blocking API does not return before it finishes its work, so it will prevent the thread from doing other things during this period.

In multi-thread mode, tasks that take a long time can run in their own threads. These threads are usually called auxiliary threads. Because only the auxiliary thread is blocked, blocking does not cause the user interface to be frozen, as shown in figure 2. The main thread of the application can continue to process user mouse and keyboard input while the other thread being blocked will wait for the CD-ROM to read, or execute any operations that the auxiliary thread may do.

The basic principle is that the thread responsible for responding to user input and keeping the user interface up-to-date (usually called the UI thread) should not be used to execute any time-consuming operations. The usual practice is to consider removing any operation that takes more than 30 ms from the UI thread. This seems exaggerated, because 30 ms is only the shortest instant pause that most people can feel. In fact, the pause is slightly shorter than the interval between consecutive frames displayed on the screen of a movie.

If the latency between mouse clicks and corresponding UI prompts (for example, re-drawing button) exceeds 30 ms, the operation and display are slightly inconsistent, as a result, it is as disturbing as a broken video frame. In order to achieve a completely high-quality response, the upper limit must be 30 ms. On the other hand, if you do not mind feeling a little incoherent, but do not want to irritate users because of a long pause, you can set the interval to 100 MS as tolerable by the average user.

This means that if you want your interface to remain responsive, any blocking operation should be performed in the auxiliary thread-either mechanically waiting for something to happen (for example, waiting for the CD-ROM to start or the hard disk to locate the data ), wait for the response from the network.

Asynchronous delegate call

The simplest way to run code in the auxiliary thread is to use Asynchronous delegate call (all delegates provide this function ). A delegate is usually called in synchronous mode. That is, when a delegate is called, the call is returned only after the packaging method is returned. To call the delegate asynchronously, call the begininvoke method to queue the method for running in the thread of the system thread pool. The call thread will return immediately without waiting for the method to complete. This is suitable for UI programs, because it can be used to start a time-consuming job without slowing down the user interface response.

For example, in the following code, the system. Windows. Forms. methodinvoker type is a system-defined delegate used to call methods without parameters.

Program code private void startsomeworkfromuithread (){
// The work we want to do is too slow for the UI
// Thread, so let's farm it out to a worker thread.

Methodinvoker MI = new methodinvoker (
Runsonworkerthread );
Mi. begininvoke (null, null); // This will not block.
}

// The slow work is done here, on a thread
// From the system thread pool.
Private void runsonworkerthread (){
Dosomethingslow ();
}

If you want to pass parameters, you can select an appropriate system-defined delegate type or define the delegate by yourself. There is nothing magical about methodinvoker delegation. Like other delegates, calling begininvoke will make the method run in the thread of the system thread pool, without blocking the UI thread so that it can execute other operations. In the above cases, this method does not return data, so you don't have to worry about it after you start it. If you need the results returned by this method, the begininvoke return value is very important and you may not pass a null parameter. However, for most UI applications, this "no matter after startup" style is the most effective and will be briefly discussed later. You should note that begininvoke returns an iasyncresult. This can be used with the entrusted endinvoke method to retrieve the call result after the method is called.

There are other technologies that can be used to run methods on other threads, such as directly using the thread pool API or creating your own threads. However, for most user interface applications, asynchronous delegate calls are enough. Using this technology is not only easy to code, but also can avoid creating unnecessary threads, because the sharing thread in the thread pool can be used to improve the overall performance of the application.

Threads and controls

The Windows form architecture has strict rules for thread usage. If you only write a single-threaded application, you do not need to know these rules because the single-threaded Code cannot violate these rules. However, once multithreading is adopted, you need to understand the most important thread rule in Windows Forms: except for a few exceptions, otherwise, do not use any member of the control in a thread other than its creation thread.

The exceptions of this rule are described in the document, but there are very few such exceptions. This applies to any objects whose classes are derived from system. Windows. Forms. Control, including almost all elements in the UI. All the UI elements (including the form itself) are derived from the control class. In addition, the result of this rule is that a contained control (such as a button contained in a form) must be in the same thread as the control bit that contains it. That is to say, all controls in a window belong to the same UI thread. In reality, most Windows Forms applications eventually have only one thread, and all UI activities occur on this thread. This thread is usually called a UI thread. This means that you cannot call any methods on any controls on the user interface, unless it is indicated in the description of this method. There are few exceptions for this rule (there is always a document record) and there is little relationship between them. Note that the following code is invalid:

Program code // created on UI thread
Private Label lblstatus;
...
// Doesn' t run on UI thread
Private void runsonworkerthread (){
Dosomethingslow ();
Lblstatus. Text = "finished! "; // Bad !!
}

If you try to run this code in. NET Framework 1.0, you may be lucky to run it, or it seems like this at first. This is the main problem in multithreading errors, that is, they are not immediately displayed. Even when some errors occur, everything looks normal before the first demo. But do not make a mistake-the code I just showed clearly violates the rules and can foresee any hope that "the trial run is good, there should be no problem. "people will pay a heavy price in the coming debugging period.

Note that this problem occurs before you explicitly create a thread. Any code that uses the delegate asynchronous call utility (call its begininvoke method) may have the same problem. The delegate provides a very attractive solution to handle slow and congested operations in the UI application, these Delegates allow you to easily run this operation outside the UI thread without creating a new thread. However, because the code that runs in asynchronous delegate call mode runs in a thread from the thread pool, it cannot access any UI element. The preceding restrictions apply to threads in the thread pool and auxiliary threads manually created.

Call controls in the correct thread

Restrictions on controls seem to be very negative for multithreaded programming. If a slow operation running in the auxiliary thread does not affect the UI, how does the user know its progress? At least, how do users know when the work is completed or whether an error occurs? Fortunately, although this restriction may cause inconvenience, it is not insurmountable. There are multiple ways to get a message from the auxiliary thread and pass the message to the UI thread. Theoretically, you can use low-level synchronization principles and pooling technology to generate your own mechanism. Fortunately, there is a solution in the form of control-class invoke methods, therefore, you do not need to work in such a low-level way.

The invoke method is one of the few exceptions to the control class with documented thread rules: It can always invoke the control from any thread. The invoke method simply carries the delegate and the optional parameter list, and calls the delegate for you in the UI thread, regardless of which thread the invoke call was issued. In fact, it is very easy to get any method for the control to run on the correct thread. However, it should be noted that this mechanism is valid only when the UI thread is not currently blocked-the call can only be passed when the UI thread is preparing to process user input. There is another good reason to never block the UI thread. The invoke method is tested to check whether the call thread is a UI thread. If yes, it directly calls the delegate. Otherwise, it schedules thread switching and calls the delegate on the UI thread. In either case, the method encapsulated by the delegate will run in the UI thread, and the invoke will return only when the method is completed.

The control class also supports the asynchronous version of invoke, which will immediately return and arrange this method to run on the UI thread at a certain time in the future. This is called begininvoke. It is similar to asynchronous delegate call. It differs significantly from delegate in that the call runs asynchronously on a thread in the thread pool. However, it runs on the UI thread asynchronously. In fact, the invoke, begininvoke, endinvoke methods, and invokerequired attributes of control are all members of the isynchronizeinvoke interface. This interface can be implemented by any class that needs to control its event transfer method.

Because begininvoke is not easy to cause deadlocks, this method should be used as much as possible; instead, the invoke method should be used less. Because invoke is synchronous, it will block the auxiliary thread until the UI thread is available. But what if the UI thread is waiting for the auxiliary thread to execute an operation? The application will be deadlocked. Begininvoke never waits for the UI thread, so this situation can be avoided.

Now, I want to review the valid version of the code snippet shown above. First, a delegate must be passed to the begininvoke method of control to run thread-sensitive code in the UI thread. This means that the Code should be put in its own method, as shown in 3. Once the auxiliary thread completes the slow work, it will call begininvoke in the label to run some code on its UI thread. In this way, the user interface can be updated.

Packaging control. Invoke

Although the Code in Figure 3 solves this problem, it is quite tedious. If the auxiliary thread wants to provide more feedback at the end, rather than simply giving "finished !" The begininvoke is too complex to use. To send other messages, such as "processing" and "everything goes smoothly", you need to try to pass a parameter to the updateui function. You may also need to add a progress bar to improve the feedback capability. So many calls to begininvoke may cause the auxiliary thread to be dominated by the Code. This will not only cause inconvenience, but also take into account the coordination between the auxiliary thread and the UI, this design is not good. After the analysis, we think that the packaging function can solve these two problems, as shown in figure 4.

The showprogress method encapsulates the work that directs the call to the correct thread. This means that the Helper thread Code does not need to pay too much attention to the UI details, but only needs to call showprogress regularly. Note that I have defined my own method, which violates the rule "must be called on the UI thread", because it then only calls other methods not restricted by this rule. This technology leads to a common topic: Why not write public methods on controls (these methods are recorded as exceptions of UI thread rules )?

The control class provides a useful tool for such a method. If I provide a public method designed to be called from any thread, it is entirely possible that someone will call this method from the UI thread. In this case, there is no need to call begininvoke because I am already in the correct thread. Calling invoke is a waste of time and resources. It is better to call appropriate methods directly. To avoid this, the control class exposes an attribute called invokerequired. This is another exception to the "UI thread only" rule. It can be read from any thread. If the calling thread is a UI thread, false is returned, and other threads return true. This means that you can modify the packaging as follows:

Program code public void showprogress (string MSG, int percentdone ){
If (invokerequired ){
// As before
...
} Else {
// We're already on the UI thread just
// Call straight through.
Updateui (this, new myprogressevents (MSG,
Percentdone ));
}
}

Showprogress can now record public methods that can be called from any thread. This does not eliminate complexity-the code for executing begininvoke still exists, and it has a place. Unfortunately, there is no simple way to get rid of it completely.

Lock

Any concurrent system must face the fact that two threads may simultaneously try to use the same piece of data. Sometimes this is not a problem-if multiple threads try to read a field from an object at the same time, there will be no problem. However, if a thread wants to modify the data, the problem may occur. If another thread is writing data while reading data, the reading thread may see false values. If the two threads perform the write operation at the same time and in the same location, after the synchronous write operation occurs, all the threads reading data from this location may see a pile of junk data. Although this behavior only happens under certain circumstances, the read operation does not even conflict with the write operation, but the data can be mixed with the two write results, and keep the error results until the next write value. To avoid this problem, you must take measures to ensure that only one thread can read or write the status of an object at a time.

To prevent these problems, use the lock function at runtime. C # allows you to use these functions to protect code by locking keywords (Visual Basic has a similar structure called synclock ). The rule is that any object that wants to call its method in multiple threads should use a locked structure each time when accessing its fields (whether read or written. For example, see Figure 5.

The locking structure works in the following way: each object in the Common Language Runtime Library (CLR) has a related lock, which can be obtained by any thread, but only one thread can own it at a time. If a thread tries to obtain a lock that another thread already has, it must wait until the thread that owns the lock releases the lock. C # The lock structure will get the object lock (if needed, wait for another thread to use it to complete the operation), and keep it until the code in braces exits. If the execution statement runs to the end of the block, the lock will be released and returned from the middle of the block, or an exception not captured in the block will be thrown.

Note that the logic in the moveBy method is protected by the same lock statement. When modifications are more complex than simple reads or writes, the entire process must be protected by separate lock statements. This also applies to updating multiple fields-the lock cannot be released before the object is in the consistent state. If the lock is released during the update State, other threads may be able to obtain it and see the inconsistent state. If you already have a lock and call a method to obtain the lock, this will not cause problems because a separate thread allows multiple attempts to obtain the same lock. This is important for code that requires locking to protect low-level access to fields and advanced operations on fields. MoveBy uses the position attribute to obtain the lock at the same time. The lock is released properly only after the outermost lock is blocked.

The code to be locked must be strictly locked. A slight omission will result in a loss. If a method modifies the status without obtaining the object lock, the rest of the code will be futile even if the object is carefully locked before using it. Similarly, if a thread tries to read the status without obtaining the lock in advance, it may read an incorrect value. Check is not allowed during running to ensure that the multi-threaded code runs properly.

Deadlock

Locks are the basic conditions for ensuring the normal operation of multi-threaded code, even if they introduce new risks. The simplest way to run code on another thread is to use Asynchronous delegate call (see figure 6 ).

If you have called the foo callbar method, the code will slowly stop running. The callbar method will obtain the lock on the foo object and release it only after barwork returns. Then, barwork uses an asynchronous delegate call to call the foowork method of the foo object in a thread pool thread. Next, it will execute some other operations before calling the delegate's endinvoke method. Endinvoke will wait for the auxiliary thread to complete, but the auxiliary thread is blocked in foowork. It also tries to get the lock of the foo object, but the lock has been held by the callbar method. Therefore, foowork waits for the callbar to release the lock, but the callbar is also waiting for the barwork to return. Unfortunately, barwork will wait for foowork to complete, so foowork must be completed before it can begin. As a result, no thread can proceed.

This is an example of a deadlock where two or more threads are blocked to wait for the other party to proceed. The situation here is different from the standard deadlock situation. The latter usually includes two locks. This indicates that if a certain cause (process call chain) exceeds the thread limit, a deadlock will occur, even if only one lock is included! Control. Invoke is a method for cross-thread calling, which is an indisputable fact. Begininvoke will not encounter such a problem, because it does not make the cause cross-thread. In fact, it will enable a brand new cause in a thread pool thread to allow the original independent execution. However, if you retain the iasyncresult returned by begininvoke and use it to call endinvoke, the problem will occur again, because endinvoke has actually merged two causal factors into one. The simplest way to avoid this is to hold an object lock and do not wait for the cross-thread call to complete. To ensure this, avoid calling invoke or endinvoke in the lock statement. The result is that when an object lock is held, you do not need to wait for other threads to complete an operation. Sticking to this rule is easy to say.

When checking the barwork of the Code, it is not obvious in the scope of the lock statement, because there is no lock statement in this method. The only reason for this problem is that barwork calls the lock statement from the foo. callbar method. This means that the call of control. invoke or endin-voke is safe only when the called function is not locked. This is not easy for non-private methods, so the best rule is that control. invoke and endinvoke are not called at all. This is why the "No matter after startup" programming style is more desirable, and also why the control. begininvoke solution is generally better than the control. Invoke solution.

Sometimes there is no choice but to break the rules. In this case, you need to carefully and strictly analyze them. However, as long as possible, the lock should be held to avoid blocking, because if not, the deadlock will be difficult to eliminate.

Make it simple

How can I benefit the most from multithreading without encountering the thorny errors that plague concurrent code? If the improved UI response speed only causes frequent program crashes, it is hard to say that it improves the user experience. Most of the common problems in multi-threaded code are caused by the inherent complexity of Running multiple operations at a time, because most people are better at thinking about continuous processes rather than concurrent processes. Generally, the best solution is to make things as simple as possible.

The nature of the UI code is that it receives events from external resources, such as user input. It processes an event, but spends most of its time waiting for it to happen. If the communication between the auxiliary thread and the UI thread can be constructed to suit the model, there may not be so many problems because there will be no new things introduced. I made things simple in this way: treating the auxiliary thread as another asynchronous event source. When a button control transmits events such as click and mouseenter, it can treat the auxiliary thread as something that passes events (such as progressupdate and workcomplete. Simply think of this as a type ratio, or actually encapsulate the helper object in a class, and expose the appropriate events in this way, it all depends on you. The latter option may require more code, but it makes the user interface code look more unified. In either case, Control. begininvoke is required to pass these events on the correct thread.

The simplest way for auxiliary threads is to write code blocks in normal order. But what if I want to use the "using the auxiliary thread as the event source" model just introduced? This model is very suitable, but it imposes restrictions on the interaction between the Code and the user interface: This thread can only send messages to the UI and cannot send requests to it.

For example, it is very difficult for the auxiliary thread to initiate a conversation in the middle to request the information required to complete the result. If you do need to do this, it is best to initiate such a dialog in the auxiliary thread rather than in the main UI thread. This constraint is advantageous because it will ensure that there is a very simple and suitable model for communication between two threads-Here Simplicity is the key to success. The advantage of this development style is that there will be no thread blocking while waiting for another thread. This is an effective strategy to avoid deadlocks.

Figure 7 shows how to use an asynchronous delegate call to perform slow operations (read the content of a directory) in the guides and then display the results on the UI. It does not use the advanced event syntax, but the call does process the complete auxiliary code in a very similar way to processing events (such as clicking.

Cancel

The problem with the preceding example is that the cancellation operation can only be performed by exiting the entire application. Although the UI still responds quickly when reading a directory, the user cannot view another directory because the program will disable related buttons before the current operation is complete. If the directory to be read is on a remote machine that does not respond, this operation takes a long time to time out.

It is also difficult to cancel an operation, although it depends on how it is canceled. One possibility is to stop waiting for this operation to complete and continue with another operation ." This is actually to discard ongoing operations and ignore the possible consequences of the final completion. This is the best option for the current example, because the operation being processed (reading directory content) is executed by calling a blocking API, and it does not matter to cancel it. However, even a loose "fake cancellation" requires a lot of work. If you decide to start a new read operation without waiting for the previous operation to complete, you cannot know which of the two unprocessed requests will receive the next notification.

The only way to cancel a request that can be run in the auxiliary thread is to provide a call object related to each request. The simplest way is to use it as a cookie, which is passed by the auxiliary thread every time a notification is sent, allowing the UI thread to associate the event with the request. Through simple identity comparison (see figure 8), the UI code can know whether the event is from the current request or a request that has already been deprecated.

It would be nice to simply discard it, but you may want to do better. If the secondary thread executes a series of complex operations that are congested, you may want the secondary thread to stop at the earliest time. Otherwise, it may continue useless operations for several minutes. In this case, the call object is not only used as a passive cookie. It also requires at least one flag to indicate whether the request is canceled. The UI can set this flag at any time, and the auxiliary thread regularly tests this flag during execution to determine whether to discard the current job.

For this solution, you still need to make several decisions: If the UI cancels the operation, will it wait until the auxiliary thread notices the cancellation? If you do not wait, you need to consider a race condition: it is possible that the UI thread will cancel this operation, but the auxiliary thread has decided to pass the notification before setting the control mark. Because the UI thread decides not to wait until the processing of the secondary thread is canceled, the UI thread may continue to receive notifications from the secondary thread. If the auxiliary thread uses begininvoke to send notifications asynchronously, the UI may even receive multiple notifications. The UI thread can always process the notification in the same way as the "discard" method-check the identity of the calling object and ignore the operation notifications that it no longer cares about. Or, lock the called object and never call begininvoke from the auxiliary thread to solve the problem. However, it is easier for the UI thread to simply check for an event before processing it to determine whether it is useful. Therefore, this method may encounter fewer problems.

See asyncutils in "code download" (link at the top of this article). It is a useful base class that can cancel operations based on auxiliary threads. Figure 9 shows a derived class that supports canceling recursive Directory Search. These classes clarify some interesting technologies. Both use the C # event syntax to provide notifications. This base class exposes events that occur when the operation is completed, canceled, and thrown an exception. The derived classes are expanded to publicly notify the client to search for matching events, progress events, and events in which directories are currently being searched. These events are always passed in the UI thread. In fact, these classes are not limited to control classes-they can pass events to any class implementing the isynchronizeinvoke interface. Figure 10 is an example Windows Forms application that provides a user interface for the Search Class. It allows you to cancel the search and display the progress and result.

Program closed

In some cases, you can use asynchronous operations that do not matter after startup, without other complex requirements to cancel the operation. However, even if the user interface does not require cancellation, it is possible to implement this function so that the program can be completely closed.

When the application exits, if the auxiliary threads created by the thread pool are still running, these threads will be terminated. Terminating is a simple and crude operation, because disabling or even bypassing any Finally block that still works. If the asynchronous operation does not interrupt some work in this way, make sure that the operation is completed before it is disabled. Such operations may include write operations on files, but files may be damaged due to sudden interruptions.

One solution is to create your own thread instead of the thread from the auxiliary thread pool. This will naturally avoid using asynchronous delegate calls. In this way, even if the main thread is closed, the application will not stop until your thread exits. The system. Threading. Thread class has an isbackground attribute to control this behavior. The default value is false. In this case, CLR terminates the application normally after all non-Background threads exit. However, this may cause another problem because the application suspension time may be longer than you expected. The windows are closed, but the process is still running. This may not be a problem. If the application is suspended for a longer time only because it requires some cleanup operations, then no problem. On the other hand, if the application is suspended for several minutes or even hours after the user interface is closed, it is unacceptable. For example, if it still keeps some files open, it may prevent the user from restarting the application later.

The best way is to write your own asynchronous operations if possible so that you can quickly cancel them and wait until all the unfinished operations are completed before closing the application. This means that you can continue to use Asynchronous delegation, while ensuring that the close operation is complete and timely.

Error Handling

Errors that occur in the auxiliary thread can generally be handled by triggering events in the UI thread. In this way, the error handling method is exactly the same as the method for completing and updating the process. Because it is difficult to recover errors on the auxiliary thread, the simplest policy is to make all errors fatal. The best policy for error recovery is to make the operation completely fail and execute the retry logic on the UI thread. If you need user intervention to fix the problem that causes the error, a simple method is to give an appropriate prompt.

The asyncutils class handles errors and cancels them. If the operation throws an exception, the basic class will capture the exception and pass the exception to the UI through the failed event.

Summary

The careful use of multi-threaded code can prevent the UI from stopping the response when executing a time-consuming task, significantly improving the response speed of the application. Asynchronous delegated call is the simplest way to migrate code with slow execution speed from the UI thread, so as to avoid such intermittent and unresponsive calls.

The Windows Forms control architecture is basically a single thread, but it provides a utility to mail calls from the auxiliary thread back to the UI thread. The simplest policy for Processing notifications from the secondary thread (whether it is success, failure, or ongoing instructions) is to treat events from regular controls (such as mouse clicks or keyboard input). In this way, new problems can be introduced in the UI code, and the unidirectional communication is not easy to cause deadlocks.

Sometimes the UI needs to send messages to an ongoing operation. The most common one is to cancel an operation. This goal can be achieved by creating an object that indicates an ongoing call and maintaining the unmark for regular checks by the auxiliary thread. If the user interface thread needs to wait for cancellation to be recognized (because the user needs to know that the work has been terminated, or needs to completely exit the Program), the implementation will be somewhat complicated, however, the provided sample code contains a base class that encapsulates all complexity. The derived class only needs to perform some necessary work, periodic test cancellation, and notify the base class of the result if the work is stopped because of the cancellation request.

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.