📜 ⬆️ ⬇️

Mt: C language for heavily loaded servers

Greetings, habrovchane!

I want to propose to discuss the idea of ​​how to simplify the writing of server programs in C by introducing additional language tools. I believe that this topic may be of interest to all developers who have had to deal with writing multithreaded or asynchronous code.

At the moment I have almost completed writing the tools - parser generator, C parser and partially C ++ parser - which allows you to start writing a translator that supports language extensions, which I will talk about here. But before continuing to work, I would like to consult with colleagues and find like-minded people.

It's about multi-threaded asynchronous servers. Asynchronous - that is, using customer-based event-based service logic (epoll / kqueue), in which multiple clients are served simultaneously by a single thread. Multi-threaded means ready to take advantage of modern multi-core processors, performing parallel servicing of connections by several threads in a single process.
')
Asynchronous service model allows you to create servers that are most prepared for work with a really high load. But asynchronous (non-blocking) code is harder to write than synchronous, blocking. In addition, it is much more difficult to write asynchronous multithreaded code: it is too easy to make a mistake when synchronizing access to shared data, and it is too difficult to detect such errors. This leads to the fact that in practice it is often wiser to completely abandon support for multithreading.

Having accumulated some experience in writing such programs, I began to notice that the solution of the main problems of synchronization of streams is solved using typical methods, the use of which could be automated at the language level, greatly simplifying the task for the developer. Then I saw that using a similar approach, it is possible to simplify the writing of asynchronous event handling code. It is because of the apparent commonality of solutions that I propose a single set of language additions for these unrelated tasks.

The ideas are quite simple, therefore, in order not to create a sense of complexity, I will list the main points on the additions to C. I will call these additions the “Mt language”.

So Mt is:Let me explain why C is selected as the base, not C ++. The main reason is the significantly higher complexity of C ++ from the point of view of the semantic analysis of source texts. Over the past year I have attempted to write a C ++ parser and a static analysis system for C ++ code. The result is that the C parser and semantics analysis in the C style were fairly easy, and the C ++ parsing is far from complete: the workload is too large. Therefore, I decided that good practical results can be achieved by relying on pure C and adding it to C ++ features that are fairly simple to implement.

Further I will tell about each addition in more detail. I cite some code examples in C ++, because in this way they are easier to relate to the equivalent code on Mt.

1. Automatic Mutex Management


Consider the common query processing scheme. Let we have some object of class MyFrontend, representing the implementation of our server. After receiving and decoding a request from a client, we call the processRequest () server object method. This method performs some operations that cause a change in the internal state of the server. Let us assume, for example, that as a result of processing a request, the count of requests for num_requests is increased:

class MyFrontend
{
private :
Mutex state_mutex ;
unsigned num_requests ;

public :
void processRequest ( )
{
state_mutex. lock ( ) ;
++ num_requests ;
state_mutex. unlock ( ) ;
}
} ;


Mt frees us from the need for manual synchronization:

async object MyFrontend
{
private :
unsigned num_requests ;

public :
async void processRequest ( )
{
++ num_requests ;
}
} ;


async object declares a class of thread safe objects. Such objects will be called asynchronous. The given code example on Mt is translated into code equivalent to the given C ++ example.

Now suppose that in order to process a request, MyFrontend should call the doSomething () method of the MyBackend object, passing the num_back backend access counter as a parameter. In addition, we do not want to think about the possibility of deadlocks and want everything to work with non-recursive mutexes. To do this, we need to free state_mutex before calling MyBackend:

class MyFrontend
{
private :
Mutex state_mutex ;
unsigned num_requests ;
unsigned num_back ;
MyBackend * backend ;

public :
void processRequest ( )
{
state_mutex. lock ( ) ;
++ num_back ;
unsigned tmp_num_back = tmp_num_back ;
state_mutex. unlock ( ) ;

backend - > doSomething ( tmp_num_back ) ;

state_mutex. lock ( ) ;
++ num_requests ;
state_mutex. unlock ( ) ;
}
} ;


The same on Mt:

async object MyFrontend
{
private :
unsigned num_requests ;
unsigned num_back ;
MyBackend * backend ;

public :
async void processRequest ( )
{
call backend - > doSomething ( ++ num_back ) ;
++ num_requests ;
}
} ;


The Mt translator independently inserts operations with state_mutex and enters a temporary variable to pass the parameter. The call operator is introduced in order to emphasize that at the time of calling the asynchronous method of another object, we release the state_mutex lock. In addition, it allows us to explicitly prohibit the call to asynchronous methods in expressions.

In addition to the examples given, a number of other use cases of locks are automated: waiting for events (events are usually used in conjunction with a mutex protecting the wait completion condition), calling an asynchronous method of an object from another method of the same object.

Thus, we minimize the risk of making a mistake while managing a simple lock on the state of an object. According to my observations, if the access to the object is synchronized using not one but several locks, then most likely the design fails: either divide the object into several independent objects, or merge the locks into one. Therefore, automating the simplest case with one mutex, we solve most of the synchronization problems. Ability to perform synchronization in manual mode and using other primitives while
persists.

2. Asynchronous processing chains


In my opinion, this is the most interesting and unusual idea. I will explain with an example illustrating the operation of a real service.

Suppose our server is a frontend photohost. We want to realize the return to customers of HTML-pages with photos of users. The page displays information from the photo owner’s profile and the photo itself. Photos can be watched only by friends of photo owners. Thus, in order to respond to an HTTP request, you need to collect the necessary information by polling a number of internal services.

Request processing algorithm:
  1. Send the user’s cookie authorization check server to identify it (check_auth);
  2. After checking the authorization, send a request to the server serving the social graph to check whether users are friends (check_friends);
  3. Send a request to the owner of the photo (get_userinfo);
  4. After receiving responses from check_friends and get_userinfo, generate an HTML page and send it to the client.
Operation 2 can be performed only after receiving a response to request 1. Request 3 does not depend on requests 1 and 2 and we want it to run in parallel with them.

Let's make a sketch of the implementation in C. This example can be disregarded. His task is to show how cumbersome the manual implementation of even such a simple asynchronous processing chain is:

// .
typedef struct {
// "" , .
MReferenced parent_referenced ;
// .
MMutex state_mutex ;

uint64_t owner_id ;
uint32_t photo_id ;

// 'true', .
bool cancel ;

// get_userinfo?
bool got_userinfo ;
// ( ).
uint32_t owner_carma ;

// check_friends?
bool got_friends ;
} HttpPhotoData ;

static void http_photo_data_init ( HttpPhotoData * data )
{
m_referenced_init ( ( MReferenced * ) data ) ;
m_mutex_init ( & data - > state_mutex ) ;

data - > cancel = false ;
data - > got_userinfo = false ;
data - > got_friends = false ;
}

static void http_photo__get_userinfo_ret ( uin32_t owner_carma,
void * _data ) ;

static void http_photo__check_auth_ret ( uint64_t client_id,
void * _data ) ;

static void http_photo__end ( HttpPhotoData * data ) ;

// .
void http_photo ( char * cookie_str,
uint64_t owner_id,
uint32_t photo_id )
{
HttpPhotoData * data = m_malloc ( sizeof ( HttpPhotoData ) ) ;
http_photo_data_init ( data ) ;

data - > owner_id = owner_id ;
data - > photo_id = photo_id ;

{
m_referenced_ref ( ( MReferenced * ) data ) ;

MCallbackDesc cb ;
m_callback_desc_init_refdata ( & cb, http_photod__get_userinfo_ret, data ) ;

get_userinfo ( & cb, owner_id ) ;
}

{
m_referenced_ref ( ( MReferenced * ) data ) ;

MCallbackDesc cb ;
m_callback_desc_init_refdata ( & cb, http_photod__check_auth_ret, data ) ;

check_auth ( & cb, cookie_str ) ;
}

m_referenced_unref ( ( MReferenced * ) data ) ;
}

// get_userinfo.
static void http_photo__get_userinfo_ret ( uint32_t owner_carma,
void * _data )
{
HttpPhotoData * data = ( HttpPhotoData * ) _data ;

m_mutex_lock ( & data - > state_mutex ) ;

if ( data - > cancel ) {
m_mutex_unlock ( & data - > state_mutex ) ;
return ;
}

data - > owner_carma = owner_carma ;
data - > got_userinfo = true ;

if ( data - > got_friends ) {
// , .
http_photo_end ( data ) ;
}

m_mutex_unlock ( & data - > state_mutex ) ;
}

// check_auth.
static void http_photo__check_auth_ret ( uint64_t client_id,
void * _data )
{
HttpPhotoData * data = ( HttpPhotoData * ) _data ;

m_referenced_ref ( ( MReferenced * ) data ) ;

MCallbackDesc cb ;
m_callback_desc_init_refdata ( & cb, http_photo__check_friends_ret, data ) ;

check_friends ( & cb, client_id, data - > owner_id ) ;
}

// check_friends.
static void http_photo__check_friends_ret ( bool friends,
void * _data )
{
HttpPhotoData * data = ( HttpPhotoData * ) _data ;

m_mutex_lock ( & data - > state_mutex ) ;
if ( data - > cancel ) {
m_mutex_unlock ( & data - > state_mutex ) ;
return ;
}

data - > got_friends = true ;

if ( ! friends ) {
// . .
data - > cancel = true ;
m_mutex_unlock ( & data - > state_mutex ) ;
return ;
}

if ( data - > got_userinfo ) {
// , .
http_photo_end ( data ) ;
}

m_mutex_unlock ( data - > state_mutex ) ;
}

// .
static void http_photo_end ( HttpPhotoData * data )
{
photo_send_page ( data - > owner_id, data - > photo_id, data - > owner_carma ) ;
}


The resulting code is complex, it is difficult to read. Most of the time while writing it was spent on routinely providing the necessary order of operations.

Equivalent implementation on Mt:

async chain void http_photo ( char * cookie_str,
uint64_t owner_id,
uint32_t photo_id )
{
async call GetUserinfoCall get_userinfo ( owner_id ) ;

async call check_auth ( cookie ) ;
gives uint64_t client_id ;

async call check_friends ( client_id, owner_id )
gives bool friends ;

if ( ! friends ) {
//
return ;
}

GetUserinfoCall gives uint32_t owner_carma ;

// HTTP- .
photo_send_page ( owner_id, photo_id, owner_carma ) ;
}


Please note that we have almost completely freed ourselves from the routine work of organizing the necessary order of execution. In addition, the generated C-code can be used in multi-threaded programs.

In the given example, two new operators are used:In addition to the demonstrated operators, the possibility of early processing of the results of asynchronous calls is provided so that you can break the chain if an error occurs before we reach the gives operator that corresponds to the failed call. You can also specify an additional destructor for the data included in the chain state structure.

The main work that Mt performs when translating asynchronous chains to C is as follows:It can be shown that each of these operations lends itself well to automation.

Parallel execution of complex chains of operations that cannot be represented by a single function in Mt can be implemented using several different asynchronous chains. That is, if the branches in the chain are so complex that they cannot be described by a single function, then a description can be constructed as a composition of several functions.

With the help of Mt, we can describe cycles and conditional transitions, broken by waiting for answers from asynchronous calls, in the same simple and familiar form as when writing ordinary code. Suppose we want to look for a value in three independent caches in turn:

async chain void search ( int key )
{
for ( int i = 0 ; i < 3 ; i ++ ) {
async call search_cache ( key, i /* cache id */ )
gives int value ;

if ( value > 0 ) {
printf ( "VALUE: %d \n " , value ) ;
break ;
}
}
}


Thus, for asynchronous processing chains, Mt can provide a qualitative leap in expressiveness and usability compared to C.

3. Annotation


Annotation is the introduction to the code of additional supporting information using annotation words. Annotated words can stand anywhere where const and volatile qualifiers can stand. All annotating words begin with a '$' character. Examples:
int * $nonnull ptr ; // NULL
MyObject * $heap myobj ; //


The Mt translator is also the basis for a static code review system that uses annotation for additional checks.

4. Borrowing C ++ Features


From the point of view of translator implementation, the most difficult feature of C ++ compared to C is the dependence of the semantic meaning of language constructs on the data types of the arguments. This concerns, first of all, the rules for overloading function names (including operator overloading) and the rules for choosing templates with regard to partial specializations. Such dependence means that in order to determine which particular function or template is used in the expression, you need to deduce the data types of all the arguments of the function or template. This, in turn, requires a complete and accurate implementation of the type conversion rules. All this is a fairly significant amount of work.

At the same time, name overloading is a controversial property of the language. For example, the creators of the Go language refused it, as there is a meaningful entry in the FAQ .

In Mt, there will be no overloading of function names and operator overloading, multiple inheritance, exceptions, RTTI. Templates without partial specialization are not so difficult to implement, and this is enough so that you can create generic container classes, so it is advisable to include templates in the language. In the absence of name overloading, templates will become a much simpler and more concise tool: there will not be such opportunities for metaprogramming as in C ++.

5. Links and weak links


The absence of operator overloading does not make it possible to implement convenient analogs of smart pointers in Mt, such as std :: auto_ptr and boost :: shared_ptr. Instead, at the language level, a link counting mechanism is proposed:
int @a ; //
int @@b ; //


The basic version uses a simple reference counting. Link counters embedded
into each synchronous or asynchronous Mt object (object and async object). With this approach to managing links, the programmer should avoid creating circular references that lead to memory leaks. Weak links are used to break cycles. In the future, it is possible to envisage the possibility of using a more sophisticated garbage collector, but now I do not see any need for this.

In the absence of operator overloading, there is no need to support C ++ references in Mt (of the int & a type).

I note that the implementation of links at the language level allows you to get a more complete and convenient mechanism than you can build on C ++ templates.

Conclusion


I have outlined the main features that I am going to include in the Mt. In addition to them, there are other ideas in the study aimed at simplifying development and reducing the expected number of errors in programs.

Mt Translator is an open source project. Currently, it exists as a parser of a subset of the C ++ language and lives in the svn repository mync.svn.sourceforge.net/svnroot/mync/trunk under the working name “scruffy”.

If you have thoughts and ideas on adding or changing certain properties of C / C ++ languages, then I suggest to voice them: perhaps it is time for their practical testing.

I invite you to participate in the project. If you are looking for a topic for independent work that could provide new knowledge and experience, and if successful, turn into a high-quality and successful product, then a new language compiler can be an excellent choice. If the problems targeted by Mt are not very relevant to you, then you may be interested in developing a C ++ parser and a system for static analysis of C / C ++ programs.

Together we can do more!

Thank you for attention.

Source: https://habr.com/ru/post/111514/


All Articles