Một cuộc gọi lại trên mạng là gì trong C và chúng được thực hiện như thế nào?


153

Từ việc đọc mà tôi đã thực hiện, Core Audio phụ thuộc rất nhiều vào các cuộc gọi lại (và C ++, nhưng đó là một câu chuyện khác).

Tôi hiểu khái niệm (sắp xếp) của việc thiết lập một chức năng được gọi bởi một chức năng khác nhiều lần để hoàn thành một nhiệm vụ. Tôi chỉ không hiểu làm thế nào họ được thiết lập và cách họ thực sự làm việc. Bất kỳ ví dụ sẽ được đánh giá cao.

Câu trả lời:


203

Không có "gọi lại" trong C - không nhiều hơn bất kỳ khái niệm lập trình chung nào khác.

Chúng được thực hiện bằng cách sử dụng các hàm con trỏ. Đây là một ví dụ:

void populate_array(int *array, size_t arraySize, int (*getNextValue)(void))
{
    for (size_t i=0; i<arraySize; i++)
        array[i] = getNextValue();
}

int getNextRandomValue(void)
{
    return rand();
}

int main(void)
{
    int myarray[10];
    populate_array(myarray, 10, getNextRandomValue);
    ...
}

Ở đây, populate_arrayhàm lấy một con trỏ hàm làm tham số thứ ba của nó và gọi nó để lấy các giá trị để điền vào mảng. Chúng tôi đã viết hàm gọi lại getNextRandomValue, trả về giá trị ish ngẫu nhiên và chuyển một con trỏ tới nó populate_array. populate_arraysẽ gọi hàm gọi lại của chúng tôi 10 lần và gán các giá trị được trả về cho các phần tử trong mảng đã cho.


2
Tôi có thể sai ở đây, nhưng không nên dòng trong population_array gọi con trỏ hàm là: mảng [i] = (* getNextValue) (); ?
Nathan Fellman

40
Toán tử dereference là tùy chọn với các con trỏ hàm, cũng như toán tử addressof. myfunc (...) = (* myfunc) (...) và & myfunc = myfunc
aib

1
@NathanFellman Tôi chỉ đọc Lập trình Expert C và nó giải thích con trỏ hàm gọi tốt.
Matt Clarkson

1
@johnny Vì tiêu chuẩn nói vậy. Nhìn vào bình luận nâng cao.
aib

3
@Patrick: popatedArray nằm trong thư viện (và được viết cách đây 12 năm) và bạn đã tự viết getNextRandomValue (ngày hôm qua); vì vậy nó không thể gọi nó trực tiếp Hãy nghĩ về một chức năng sắp xếp thư viện mà bạn tự cung cấp bộ so sánh.
aib

121

Dưới đây là một ví dụ về các cuộc gọi lại trong C.

Giả sử bạn muốn viết một số mã cho phép đăng ký gọi lại khi có sự kiện xảy ra.

Đầu tiên xác định loại hàm được sử dụng cho cuộc gọi lại:

typedef void (*event_cb_t)(const struct event *evt, void *userdata);

Bây giờ, hãy xác định một hàm được sử dụng để đăng ký gọi lại:

int event_cb_register(event_cb_t cb, void *userdata);

Đây là những gì mã sẽ trông giống như đăng ký một cuộc gọi lại:

static void my_event_cb(const struct event *evt, void *data)
{
    /* do stuff and things with the event */
}

...
   event_cb_register(my_event_cb, &my_custom_data);
...

Trong phần bên trong của bộ điều phối sự kiện, cuộc gọi lại có thể được lưu trữ trong một cấu trúc trông giống như thế này:

struct event_cb {
    event_cb_t cb;
    void *data;
};

Đây là những gì mã trông giống như thực hiện một cuộc gọi lại.

struct event_cb *callback;

...

/* Get the event_cb that you want to execute */

callback->cb(event, callback->data);

Đúng thứ tôi cần. Phần userdata rất hữu ích nếu người dùng của bạn muốn truyền dữ liệu tùy chỉnh (ví dụ: tay cầm thiết bị) được yêu cầu trong chức năng gọi lại.
uceumern

câu hỏi xác minh: Là cuộc gọi lại typedef với dấu hoa thị bởi vì nó là một con trỏ đến địa chỉ hàm? Nếu dấu hoa thị bị thiếu, điều đó có sai không? Nếu điều đó là không chính xác sau đó có hai ngôi sao thiếu trong thư viện libsrtp của cisco trên github: github.com/cisco/libsrtp/blob/... github.com/cisco/libsrtp/blob/...
twildeman

@twildeman Việc trả lời câu hỏi của bạn có vẻ tầm thường bằng cách biên dịch ở chế độ Tiêu chuẩn C với các cảnh báo trên. Bạn cũng có thể viết một chương trình thử nghiệm tối thiểu hóa. Mã như những người trong libsrtpkhông đưa ra cảnh báo. Sau đó, tôi cho rằng khi một kiểu như vậy xuất hiện dưới dạng đối số hàm, thì bắt buộc phải 'phân rã' thành con trỏ thành hàm, giống như mảng phân rã thành con trỏ tới các phần tử đầu tiên của chúng, vì vậy cuối cùng điều đó cũng xảy ra một trong hai cách Nó thú vị, tuy nhiên, các cuộc thảo luận của typedefs như tôi đã tìm thấy làm cái nhìn không ngay cả ở khía cạnh này, thay vì tập trung vào việc khai báo nguyên mẫu hoặc gợi ý với nó
underscore_d

Tôi không biết cái này làm gì, và nó không thể được biên dịch thành công. Bất cứ ai cũng có thể giải thích nó một cách chi tiết hoặc điền vào phần còn lại của mã để biên dịch thành công?
Andy Lin

20

Một chương trình gọi lại đơn giản. Hy vọng nó trả lời câu hỏi của bạn.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include "../../common_typedef.h"

typedef void (*call_back) (S32, S32);

void test_call_back(S32 a, S32 b)
{
    printf("In call back function, a:%d \t b:%d \n", a, b);
}

void call_callback_func(call_back back)
{
    S32 a = 5;
    S32 b = 7;

    back(a, b);
}

S32 main(S32 argc, S8 *argv[])
{
    S32 ret = SUCCESS;

    call_back back;

    back = test_call_back;

    call_callback_func(back);

    return ret;
}

9

Hàm gọi lại trong C tương đương với tham số / biến số được gán sẽ được sử dụng trong một hàm khác. Ví dụ Wiki

Trong mã dưới đây,

#include <stdio.h>
#include <stdlib.h>

/* The calling function takes a single callback as a parameter. */
void PrintTwoNumbers(int (*numberSource)(void)) {
    printf("%d and %d\n", numberSource(), numberSource());
}

/* A possible callback */
int overNineThousand(void) {
    return (rand() % 1000) + 9001;
}

/* Another possible callback. */
int meaningOfLife(void) {
    return 42;
}

/* Here we call PrintTwoNumbers() with three different callbacks. */
int main(void) {
    PrintTwoNumbers(&rand);
    PrintTwoNumbers(&overNineThousand);
    PrintTwoNumbers(&meaningOfLife);
    return 0;
}

Hàm (* numberSource) bên trong hàm gọi PrintTwoNumbers là một hàm để "gọi lại" / thực thi từ bên trong PrintTwoNumbers như được mã hóa bởi mã khi nó chạy.

Vì vậy, nếu bạn có một cái gì đó giống như một hàm pthread, bạn có thể gán một hàm khác để chạy bên trong vòng lặp từ khởi tạo của nó.


6

Một cuộc gọi lại trong C là một chức năng được cung cấp cho một chức năng khác để "gọi lại" tại một thời điểm nào đó khi chức năng kia đang thực hiện nhiệm vụ của mình.

hai cách mà một cuộc gọi lại được sử dụng : gọi lại đồng bộ và gọi lại không đồng bộ. Một cuộc gọi lại đồng bộ được cung cấp cho một chức năng khác sẽ thực hiện một số nhiệm vụ và sau đó trả lại cho người gọi với nhiệm vụ đã hoàn thành. Một cuộc gọi lại không đồng bộ được cung cấp cho một chức năng khác sẽ bắt đầu một tác vụ và sau đó trả về cho người gọi với nhiệm vụ có thể không hoàn thành.

Một cuộc gọi lại đồng bộ thường được sử dụng để cung cấp một ủy nhiệm cho một chức năng khác mà chức năng kia ủy nhiệm một số bước của nhiệm vụ. Các ví dụ cổ điển của phái đoàn này là các chức năng bsearch()qsort()từ Thư viện chuẩn C. Cả hai hàm này đều có một cuộc gọi lại được sử dụng trong tác vụ mà hàm đang cung cấp để loại dữ liệu được tìm kiếm, trong trường hợp bsearch()hoặc được sắp xếp, trong trường hợp qsort(), không cần biết bởi hàm đang được đã sử dụng.

Ví dụ ở đây là một chương trình mẫu nhỏ với bsearch()việc sử dụng các hàm so sánh khác nhau, các cuộc gọi lại đồng bộ. Bằng cách cho phép chúng ta ủy quyền so sánh dữ liệu cho hàm gọi lại, bsearch()hàm cho phép chúng ta quyết định trong thời gian chạy loại so sánh nào chúng ta muốn sử dụng. Điều này là đồng bộ bởi vì khi bsearch()hàm trả về nhiệm vụ hoàn thành.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    int iValue;
    int kValue;
    char label[6];
} MyData;

int cmpMyData_iValue (MyData *item1, MyData *item2)
{
    if (item1->iValue < item2->iValue) return -1;
    if (item1->iValue > item2->iValue) return 1;
    return 0;
}

int cmpMyData_kValue (MyData *item1, MyData *item2)
{
    if (item1->kValue < item2->kValue) return -1;
    if (item1->kValue > item2->kValue) return 1;
    return 0;
}

int cmpMyData_label (MyData *item1, MyData *item2)
{
    return strcmp (item1->label, item2->label);
}

void bsearch_results (MyData *srch, MyData *found)
{
        if (found) {
            printf ("found - iValue = %d, kValue = %d, label = %s\n", found->iValue, found->kValue, found->label);
        } else {
            printf ("item not found, iValue = %d, kValue = %d, label = %s\n", srch->iValue, srch->kValue, srch->label);
        }
}

int main ()
{
    MyData dataList[256] = {0};

    {
        int i;
        for (i = 0; i < 20; i++) {
            dataList[i].iValue = i + 100;
            dataList[i].kValue = i + 1000;
            sprintf (dataList[i].label, "%2.2d", i + 10);
        }
    }

//  ... some code then we do a search
    {
        MyData srchItem = { 105, 1018, "13"};
        MyData *foundItem = bsearch (&srchItem, dataList, 20, sizeof(MyData), cmpMyData_iValue );

        bsearch_results (&srchItem, foundItem);

        foundItem = bsearch (&srchItem, dataList, 20, sizeof(MyData), cmpMyData_kValue );
        bsearch_results (&srchItem, foundItem);

        foundItem = bsearch (&srchItem, dataList, 20, sizeof(MyData), cmpMyData_label );
        bsearch_results (&srchItem, foundItem);
    }
}

Một cuộc gọi lại không đồng bộ khác ở chỗ khi hàm được gọi mà chúng ta cung cấp trả về gọi lại, tác vụ có thể không được hoàn thành. Kiểu gọi lại này thường được sử dụng với I / O không đồng bộ trong đó một thao tác I / O được bắt đầu và sau đó khi nó được hoàn thành, cuộc gọi lại được gọi.

Trong chương trình sau, chúng tôi tạo một ổ cắm để lắng nghe các yêu cầu kết nối TCP và khi nhận được yêu cầu, chức năng thực hiện nghe sẽ gọi chức năng gọi lại được cung cấp. Ứng dụng đơn giản này có thể được thực hiện bằng cách chạy nó trong một cửa sổ trong khi sử dụng telnettiện ích hoặc trình duyệt web để cố gắng kết nối trong một cửa sổ khác.

Tôi đã gỡ bỏ hầu hết mã WinSock từ ví dụ Microsoft cung cấp với accept()chức năng tại https://msdn.microsoft.com/en-us/l Library / windows / desktop / ms737526 (v = vs85) .aspx

Ứng dụng này khởi động listen()máy chủ cục bộ, 127.0.0.1, sử dụng cổng 8282 để bạn có thể sử dụng telnet 127.0.0.1 8282hoặc http://127.0.0.1:8282/.

Ứng dụng mẫu này được tạo ra dưới dạng một ứng dụng bảng điều khiển với Visual Studio 2017 Community Edition và nó đang sử dụng phiên bản ổ cắm Microsoft WinSock. Đối với một ứng dụng Linux, các chức năng WinSock sẽ cần được thay thế bằng các lựa chọn thay thế của Linux và thư viện các luồng của Windows sẽ sử dụng pthreadsthay thế.

#include <stdio.h>
#include <winsock2.h>
#include <stdlib.h>
#include <string.h>

#include <Windows.h>

// Need to link with Ws2_32.lib
#pragma comment(lib, "Ws2_32.lib")

// function for the thread we are going to start up with _beginthreadex().
// this function/thread will create a listen server waiting for a TCP
// connection request to come into the designated port.
// _stdcall modifier required by _beginthreadex().
int _stdcall ioThread(void (*pOutput)())
{
    //----------------------
    // Initialize Winsock.
    WSADATA wsaData;
    int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (iResult != NO_ERROR) {
        printf("WSAStartup failed with error: %ld\n", iResult);
        return 1;
    }
    //----------------------
    // Create a SOCKET for listening for
    // incoming connection requests.
    SOCKET ListenSocket;
    ListenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (ListenSocket == INVALID_SOCKET) {
        wprintf(L"socket failed with error: %ld\n", WSAGetLastError());
        WSACleanup();
        return 1;
    }
    //----------------------
    // The sockaddr_in structure specifies the address family,
    // IP address, and port for the socket that is being bound.
    struct sockaddr_in service;
    service.sin_family = AF_INET;
    service.sin_addr.s_addr = inet_addr("127.0.0.1");
    service.sin_port = htons(8282);

    if (bind(ListenSocket, (SOCKADDR *)& service, sizeof(service)) == SOCKET_ERROR) {
        printf("bind failed with error: %ld\n", WSAGetLastError());
        closesocket(ListenSocket);
        WSACleanup();
        return 1;
    }
    //----------------------
    // Listen for incoming connection requests.
    // on the created socket
    if (listen(ListenSocket, 1) == SOCKET_ERROR) {
        printf("listen failed with error: %ld\n", WSAGetLastError());
        closesocket(ListenSocket);
        WSACleanup();
        return 1;
    }
    //----------------------
    // Create a SOCKET for accepting incoming requests.
    SOCKET AcceptSocket;
    printf("Waiting for client to connect...\n");

    //----------------------
    // Accept the connection.
    AcceptSocket = accept(ListenSocket, NULL, NULL);
    if (AcceptSocket == INVALID_SOCKET) {
        printf("accept failed with error: %ld\n", WSAGetLastError());
        closesocket(ListenSocket);
        WSACleanup();
        return 1;
    }
    else
        pOutput ();   // we have a connection request so do the callback

    // No longer need server socket
    closesocket(ListenSocket);

    WSACleanup();
    return 0;
}

// our callback which is invoked whenever a connection is made.
void printOut(void)
{
    printf("connection received.\n");
}

#include <process.h>

int main()
{
     // start up our listen server and provide a callback
    _beginthreadex(NULL, 0, ioThread, printOut, 0, NULL);
    // do other things while waiting for a connection. In this case
    // just sleep for a while.
    Sleep(30000);
}

Câu trả lời tuyệt vời, hiển thị cả các cuộc gọi lại đồng bộ và không đồng bộ. Một ví dụ cụ thể khác về việc sử dụng các cuộc gọi lại không đồng bộ trong C- * NIX là các tín hiệu không đồng bộ và các trình xử lý tín hiệu của chúng. Dưới đây là một mô tả tuyệt vời về cách xử lý tín hiệu được xử lý trong Linux [link] ( stackoverflow.com/questions/6949025/ ám ).
drlolly

4

Các cuộc gọi lại trong C thường được thực hiện bằng cách sử dụng các con trỏ hàm và một con trỏ dữ liệu liên quan. Bạn chuyển chức năng on_event()và con trỏ dữ liệu của mình cho một hàm khung watch_events()(ví dụ). Khi một sự kiện xảy ra, chức năng của bạn được gọi với dữ liệu của bạn và một số dữ liệu dành riêng cho sự kiện.

Gọi lại cũng được sử dụng trong lập trình GUI. Các GTK + hướng dẫn có một phần tốt đẹp trên lý thuyết về tín hiệu và callbacks .


2

Bài viết trên wikipedia này có một ví dụ trong C.

Một ví dụ điển hình là các mô-đun mới được viết để tăng đăng ký máy chủ Web Apache với quy trình apache chính bằng cách chuyển cho chúng các con trỏ hàm để các hàm đó được gọi lại để xử lý các yêu cầu trang web.


0

Thông thường điều này có thể được thực hiện bằng cách sử dụng một con trỏ hàm, đó là một biến đặc biệt trỏ đến vị trí bộ nhớ của hàm. Sau đó, bạn có thể sử dụng hàm này để gọi hàm với các đối số cụ thể. Vì vậy, có thể sẽ có một chức năng thiết lập chức năng gọi lại. Điều này sẽ chấp nhận một con trỏ hàm và sau đó lưu địa chỉ đó ở một nơi có thể sử dụng nó. Sau đó khi sự kiện đã chỉ định được kích hoạt, nó sẽ gọi hàm đó.


0

Nó dễ dàng hơn nhiều để hiểu một ý tưởng thông qua ví dụ. Những gì đã được nói về chức năng gọi lại trong C cho đến nay là những câu trả lời tuyệt vời, nhưng có lẽ lợi ích lớn nhất của việc sử dụng tính năng này là giữ cho mã sạch và không bị lộn xộn.

Thí dụ

Mã C sau đây thực hiện sắp xếp nhanh chóng. Dòng thú vị nhất trong đoạn mã dưới đây là dòng này, nơi chúng ta có thể thấy chức năng gọi lại trong thực tế:

qsort(arr,N,sizeof(int),compare_s2b);

So sánh_s2b là tên của hàm mà qsort () đang sử dụng để gọi hàm. Điều này giữ cho qsort () không bị xáo trộn (do đó dễ bảo trì hơn). Bạn chỉ cần gọi một hàm theo tên từ bên trong một hàm khác (tất nhiên, ít nhất là khai báo nguyên mẫu hàm, trước khi nó có thể được gọi từ một hàm khác).

Mã hoàn chỉnh

#include <stdio.h>
#include <stdlib.h>

int arr[]={56,90,45,1234,12,3,7,18};
//function prototype declaration 

int compare_s2b(const void *a,const void *b);

int compare_b2s(const void *a,const void *b);

//arranges the array number from the smallest to the biggest
int compare_s2b(const void* a, const void* b)
{
    const int* p=(const int*)a;
    const int* q=(const int*)b;

    return *p-*q;
}

//arranges the array number from the biggest to the smallest
int compare_b2s(const void* a, const void* b)
{
    const int* p=(const int*)a;
    const int* q=(const int*)b;

    return *q-*p;
}

int main()
{
    printf("Before sorting\n\n");

    int N=sizeof(arr)/sizeof(int);

    for(int i=0;i<N;i++)
    {
        printf("%d\t",arr[i]);
    }

    printf("\n");

    qsort(arr,N,sizeof(int),compare_s2b);

    printf("\nSorted small to big\n\n");

    for(int j=0;j<N;j++)
    {
        printf("%d\t",arr[j]);
    }

    qsort(arr,N,sizeof(int),compare_b2s);

    printf("\nSorted big to small\n\n");

    for(int j=0;j<N;j++)
    {
        printf("%d\t",arr[j]);
    }

    exit(0);
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.