Trong lập trình, cụ thể trong embedded C domain, các thuộc tính của một function ngoài các arguments, giá trị return còn có 2 khái niệm nữa khá phức tạp là Synchonous/Asynchronous, reentrancy và threadsafe. Trong bài này mình tập trung nói về khái niệm reentrancy.
Nói một cách hàn lâm, một hàm được gọi là "thread safe" nếu nó bị interrupt giữa function và khi trở lại, nó vẫn safely hoạt động như mong đợi.
Ví dụ một vài hàm threadsafe:
int my_adder( int a , int b)
{
return (a + b);
}
Hàm trên được gọi là threadsafe bởi vì dù bạn có interrupt nó ở đâu thì kết quả của my_adder(10,11) luôn luôn là 21.
Vậy hàm như thế nào được gọi là non safe. Mình sẽ ví dụ một use case cụ thể trong embedded về vấn đề threadsafe.
Giả sử mình cần viết ứng dụng hiển thị đồng hồ phút và giây. Mình có một timer đếm giây, mỗi giây timer sẽ được trigger và đếm tăng counter lên 1.
/* Start of File */
#include <somefile.h>
/* Global variable */
uint32_t count_second = 0;
/* define ISR for timer */
ISR void timer( void )
{
if(count_second == 3600)
{
count_second = 0;
}
else
{
count_second = count_second+1;
}
}
void show_clock( void );
void main( void )
{
init_timer( some_configuration );
init_uart( for_displaying_clock );
while( 1 )
{
show_clock();
}
}
void show_clock( void );
{
uint32_t minute;
uint32_t second;
minute = count_second/60;
/* Timer ISR may happen here */
second = count_second%60;
printf(" It is %d:%d", minute, second");
}
/* End of File */
Bạn nghĩ chương trình trên chạy ổn chứ? Absolutely not,
Giả sử như biến count_second đang có giá trị là 59, theo như logic, thì kết quả đồng hồ phải chuyển từ ... 00:58 >> 00:59 >> 1:00 >>..., đúng không?
Đúng, theo như expectation thì đồng hồ phải hiển thị như vậy, nhưng vẫn có trường hợp, với code kiểu tiên, tôi dám chắc là các bạn từng viết chương trình này đã gặp những giá trị lạ như
00:58 >> 00:59 >> 00:00 > 1:00 tại sao lại có điều đó xảy ra.
Bạn hãy để ý dòng "/* Timer ISR may happen here */"
Lúc counter_second đang có giá trị là 59 thì minute=59/60 = 0;
lúc interrupt xảy ra, giá trị counter_second sẽ là 60 và 60%60 sẽ là 0 nên second sẽ là "00", khi chạy lệnh print, 2 giá trị minute và second sẽ là 0:00. Và hàm này (hàm show_clock) không được gọi là threadsafe, bởi vì khi nó bị ngắt và trở lại, nó đã không hoạt động đúng như mong đợi nữa. Vậy có cách nào để hàm trên trở thành threadsafe, có nhiều cách:
uint32_t minute;
uint32_t second;
uint32_t t_second_count;
t_second_count = count_second;
minute = count_second/60;
/* Timer ISR may happen here */
second = count_second%60;
printf(" It is %d:%d", minute, second");
}
uint32_t minute;
uint32_t second;
díable_interrupt( void ); minute = count_second/60;
/* Timer ISR may NOT happen here */
second = count_second%60;
enable_interrupt( void );
printf(" It is %d:%d", minute, second");
}
Nói một cách hàn lâm, một hàm được gọi là "thread safe" nếu nó bị interrupt giữa function và khi trở lại, nó vẫn safely hoạt động như mong đợi.
Ví dụ một vài hàm threadsafe:
int my_adder( int a , int b)
{
return (a + b);
}
Hàm trên được gọi là threadsafe bởi vì dù bạn có interrupt nó ở đâu thì kết quả của my_adder(10,11) luôn luôn là 21.
Vậy hàm như thế nào được gọi là non safe. Mình sẽ ví dụ một use case cụ thể trong embedded về vấn đề threadsafe.
Giả sử mình cần viết ứng dụng hiển thị đồng hồ phút và giây. Mình có một timer đếm giây, mỗi giây timer sẽ được trigger và đếm tăng counter lên 1.
/* Start of File */
#include <somefile.h>
/* Global variable */
uint32_t count_second = 0;
/* define ISR for timer */
ISR void timer( void )
{
if(count_second == 3600)
{
count_second = 0;
}
else
{
count_second = count_second+1;
}
}
void show_clock( void );
void main( void )
{
init_timer( some_configuration );
init_uart( for_displaying_clock );
while( 1 )
{
show_clock();
}
}
void show_clock( void );
{
uint32_t minute;
uint32_t second;
minute = count_second/60;
/* Timer ISR may happen here */
second = count_second%60;
printf(" It is %d:%d", minute, second");
}
/* End of File */
Bạn nghĩ chương trình trên chạy ổn chứ? Absolutely not,
Giả sử như biến count_second đang có giá trị là 59, theo như logic, thì kết quả đồng hồ phải chuyển từ ... 00:58 >> 00:59 >> 1:00 >>..., đúng không?
Đúng, theo như expectation thì đồng hồ phải hiển thị như vậy, nhưng vẫn có trường hợp, với code kiểu tiên, tôi dám chắc là các bạn từng viết chương trình này đã gặp những giá trị lạ như
00:58 >> 00:59 >> 00:00 > 1:00 tại sao lại có điều đó xảy ra.
Bạn hãy để ý dòng "/* Timer ISR may happen here */"
Lúc counter_second đang có giá trị là 59 thì minute=59/60 = 0;
lúc interrupt xảy ra, giá trị counter_second sẽ là 60 và 60%60 sẽ là 0 nên second sẽ là "00", khi chạy lệnh print, 2 giá trị minute và second sẽ là 0:00. Và hàm này (hàm show_clock) không được gọi là threadsafe, bởi vì khi nó bị ngắt và trở lại, nó đã không hoạt động đúng như mong đợi nữa. Vậy có cách nào để hàm trên trở thành threadsafe, có nhiều cách:
- Cách #1: lưu giá trị biến toàn cục vào local variable, với cách này (tạm coi như lệnh gán là một lệnh trông thể bị interrupt), hàm này luôn đảm bảo 2 biến minute và second luôn đồng nhất với count_second và vấn đề trên sẽ được giải quyết.
uint32_t minute;
uint32_t second;
uint32_t t_second_count;
t_second_count = count_second;
minute = count_second/60;
/* Timer ISR may happen here */
second = count_second%60;
printf(" It is %d:%d", minute, second");
}
- Cách #2: ngắt interrupt tại nơi mà interrupt có thể làm sai lệch hoạt động của hàm, với cách này, interrupt sẽ không thể chen giữa 2 lệnh gán và sẽ không có cơ hội cho giá trị lạ xuất hiên.
uint32_t minute;
uint32_t second;
díable_interrupt( void ); minute = count_second/60;
/* Timer ISR may NOT happen here */
second = count_second%60;
enable_interrupt( void );
printf(" It is %d:%d", minute, second");
}
Hi vọng các bạn có thể hiểu được phần nào về khái niệm threadsafe. Còn về reentrancy, khơi khó tưởng tượng hơn một chút.
Một function được gọi là reentrancy khi nó có thể gọi lại lần nữa trước khi kết thúc lần chạy đầu tiên.
Ví dụ mình có một hàm làm nhiệm vụ là double giá trị của một biến
#include "somefile.h"
uint32_t my_var=2;
void my_double( uint32_t *arg)
{
uint32_t temp;
temp = *arg;
/*Interrupt may happen here*/
temp = temp + *arg;
*arg = temp;
}
ISR someInturrupt( void )
{
my_double( &my_vả );
}
Hàm này sẽ được gọi trong hàm main mà trong một interrupt:
Giả sử giá trị của my var hiện tại đang là 2, trong trường hợp bình thường, hàm main gọi my_double( &my_var ) thì my_var sau đó phải có giá trị là 4.
Nhưng không may, trong lúc đay chạy hàm my_double, một interrupt xảy ra và trong interrup, hàm my_double được gọi ngay giữa hàm ( như chú thích), lúc này, hàm inturrupt chạy xong và biến var có giá trị là 4, và temp của lần gọi đầu tiên có giá trị là 2 + với *my_var là 4, và cuối cùng, kết của của biến var là 6, đã mất đi ý nghĩa của hàm my_double.
cách giải quyết vấn đề trên cũng giống như thread safe là disable interuppt những chỗ có thể gây hư hại cho hàm my_double.
Và nhìn chung thì, hàm thread safe luôn luôn reentrant, nhưng hàm reentrant chưa chắc đã threadsafe. Vấn đề này các bạn có thể google thêm, mình chỉ cố gắng đưa ra khái niệm để các bạn hiểu được concept.
Trong thực tế, theo mình thấy thì người ta luôn cố gắng để function trở thành threadsafe hoặc reentrant nếu cần thiết, đương nhiên là chỉ nếu thôi, vì có những trường hợp reentrant là quá xa xỉ với một function, ví dụ như những function luôn chỉ được gọi một lần trong chương trình (initialization) chẳng hạn thì không cần reentrant, vì đâu có hàm nào gọi lại function ấy lần 2. Còn threadsafe, rõ ràng là để đảm bảo tính đồng nhất (data consistency như trong ví dụ 1), thì phải cố gắng bảo vệ nhũng vùng data dùng chung để cho hàm trở thành threadsafe.
Một function được gọi là reentrancy khi nó có thể gọi lại lần nữa trước khi kết thúc lần chạy đầu tiên.
Ví dụ mình có một hàm làm nhiệm vụ là double giá trị của một biến
#include "somefile.h"
uint32_t my_var=2;
void my_double( uint32_t *arg)
{
uint32_t temp;
temp = *arg;
/*Interrupt may happen here*/
temp = temp + *arg;
*arg = temp;
}
ISR someInturrupt( void )
{
my_double( &my_vả );
}
Hàm này sẽ được gọi trong hàm main mà trong một interrupt:
Giả sử giá trị của my var hiện tại đang là 2, trong trường hợp bình thường, hàm main gọi my_double( &my_var ) thì my_var sau đó phải có giá trị là 4.
Nhưng không may, trong lúc đay chạy hàm my_double, một interrupt xảy ra và trong interrup, hàm my_double được gọi ngay giữa hàm ( như chú thích), lúc này, hàm inturrupt chạy xong và biến var có giá trị là 4, và temp của lần gọi đầu tiên có giá trị là 2 + với *my_var là 4, và cuối cùng, kết của của biến var là 6, đã mất đi ý nghĩa của hàm my_double.
cách giải quyết vấn đề trên cũng giống như thread safe là disable interuppt những chỗ có thể gây hư hại cho hàm my_double.
Và nhìn chung thì, hàm thread safe luôn luôn reentrant, nhưng hàm reentrant chưa chắc đã threadsafe. Vấn đề này các bạn có thể google thêm, mình chỉ cố gắng đưa ra khái niệm để các bạn hiểu được concept.
Trong thực tế, theo mình thấy thì người ta luôn cố gắng để function trở thành threadsafe hoặc reentrant nếu cần thiết, đương nhiên là chỉ nếu thôi, vì có những trường hợp reentrant là quá xa xỉ với một function, ví dụ như những function luôn chỉ được gọi một lần trong chương trình (initialization) chẳng hạn thì không cần reentrant, vì đâu có hàm nào gọi lại function ấy lần 2. Còn threadsafe, rõ ràng là để đảm bảo tính đồng nhất (data consistency như trong ví dụ 1), thì phải cố gắng bảo vệ nhũng vùng data dùng chung để cho hàm trở thành threadsafe.
No comments:
Post a Comment