MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C语言结构体与多线程编程的结合

2022-09-295.8k 阅读

C语言结构体基础

结构体定义与声明

在C语言中,结构体(struct)是一种用户自定义的数据类型,它允许将不同类型的数据组合在一起。结构体的定义形式如下:

struct 结构体名 {
    数据类型1 成员1;
    数据类型2 成员2;
    // 更多成员...
};

例如,定义一个表示学生信息的结构体:

struct Student {
    char name[50];
    int age;
    float grade;
};

这里struct Student就是我们定义的结构体类型,它包含了一个字符数组name用于存储学生姓名,一个整数age表示年龄,以及一个浮点数grade记录成绩。

声明结构体变量有以下几种方式:

  1. 先定义结构体类型,再声明变量
struct Student {
    char name[50];
    int age;
    float grade;
};
struct Student stu1, stu2;
  1. 定义结构体类型的同时声明变量
struct Student {
    char name[50];
    int age;
    float grade;
} stu1, stu2;
  1. 匿名结构体声明变量
struct {
    char name[50];
    int age;
    float grade;
} stu1, stu2;

不过匿名结构体由于没有名字,后续无法再声明该类型的其他变量,使用场景相对较少。

结构体成员访问

结构体变量的成员通过点运算符(.)来访问。例如:

#include <stdio.h>
struct Student {
    char name[50];
    int age;
    float grade;
};
int main() {
    struct Student stu;
    // 给结构体成员赋值
    strcpy(stu.name, "Alice");
    stu.age = 20;
    stu.grade = 3.5;
    // 输出结构体成员的值
    printf("Name: %s\n", stu.name);
    printf("Age: %d\n", stu.age);
    printf("Grade: %.2f\n", stu.grade);
    return 0;
}

上述代码首先定义了Student结构体,然后在main函数中声明了stu变量,并为其成员赋值,最后输出成员的值。

结构体指针

结构体指针可以指向结构体变量。定义结构体指针的方式如下:

struct Student *ptr;

通过结构体指针访问成员需要使用箭头运算符(->)。例如:

#include <stdio.h>
struct Student {
    char name[50];
    int age;
    float grade;
};
int main() {
    struct Student stu;
    struct Student *ptr = &stu;
    // 通过指针给结构体成员赋值
    strcpy(ptr->name, "Bob");
    ptr->age = 21;
    ptr->grade = 3.8;
    // 通过指针输出结构体成员的值
    printf("Name: %s\n", ptr->name);
    printf("Age: %d\n", ptr->age);
    printf("Grade: %.2f\n", ptr->grade);
    return 0;
}

这里先声明了Student结构体指针ptr并使其指向stu变量,然后通过指针访问并操作结构体成员。

多线程编程基础

什么是多线程

在操作系统中,线程是进程中的一个执行单元,一个进程可以包含多个线程。与进程相比,线程共享进程的资源(如内存空间、文件描述符等),但有自己独立的栈空间和寄存器状态。多线程编程允许程序同时执行多个任务,从而提高程序的性能和响应性。

例如,在一个图形界面应用程序中,一个线程可以负责处理用户输入,另一个线程可以进行复杂的计算,还有一个线程可以处理网络通信,这样各个任务之间不会相互阻塞,提高了用户体验。

C语言中的多线程库 - POSIX线程(pthread)

在POSIX兼容的系统(如Linux、macOS等)中,C语言可以使用POSIX线程库(pthread)进行多线程编程。要使用pthread库,需要包含头文件<pthread.h>

  1. 线程创建 pthread_create函数用于创建一个新线程,其原型如下:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine) (void *), void *arg);
  • thread:指向新线程标识符的指针。
  • attr:线程属性,通常可以设置为NULL,表示使用默认属性。
  • start_routine:线程函数的指针,新线程从该函数开始执行。
  • arg:传递给线程函数的参数。

例如,创建一个简单的线程:

#include <stdio.h>
#include <pthread.h>
void *print_message(void *ptr) {
    char *message = (char *)ptr;
    printf("%s\n", message);
    return NULL;
}
int main() {
    pthread_t thread;
    char *message = "Hello from thread!";
    int ret = pthread_create(&thread, NULL, print_message, (void *)message);
    if (ret != 0) {
        printf("Error creating thread: %d\n", ret);
        return 1;
    }
    printf("Main thread continues execution\n");
    pthread_join(thread, NULL);
    return 0;
}

在上述代码中,print_message是线程函数,pthread_create创建了一个新线程并传递了message字符串作为参数。pthread_join函数用于等待线程结束。

  1. 线程终止 线程可以通过以下几种方式终止:
  • 线程函数返回:线程函数执行完毕并返回,这是最常见的终止方式。
  • pthread_exit函数:线程可以调用pthread_exit函数主动终止自身,其原型为void pthread_exit(void *retval)retval是线程的返回值。
  • 被其他线程取消:其他线程可以调用pthread_cancel函数取消指定的线程。

线程同步

由于多个线程共享进程资源,可能会出现竞态条件(race condition),即多个线程同时访问和修改共享资源,导致结果不可预测。为了避免竞态条件,需要进行线程同步。

  1. 互斥锁(Mutex) 互斥锁是一种简单的线程同步机制,它保证在同一时间只有一个线程可以访问共享资源。pthread库提供了以下函数来操作互斥锁:
  • pthread_mutex_init:初始化互斥锁,原型为int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr)
  • pthread_mutex_lock:锁定互斥锁,如果互斥锁已被其他线程锁定,则调用线程会阻塞等待。
  • pthread_mutex_unlock:解锁互斥锁,允许其他线程锁定它。
  • pthread_mutex_destroy:销毁互斥锁,释放相关资源。

例如,使用互斥锁保护共享变量:

#include <stdio.h>
#include <pthread.h>
int shared_variable = 0;
pthread_mutex_t mutex;
void *increment(void *arg) {
    pthread_mutex_lock(&mutex);
    shared_variable++;
    printf("Incremented shared variable: %d\n", shared_variable);
    pthread_mutex_unlock(&mutex);
    return NULL;
}
int main() {
    pthread_t thread1, thread2;
    pthread_mutex_init(&mutex, NULL);
    pthread_create(&thread1, NULL, increment, NULL);
    pthread_create(&thread2, NULL, increment, NULL);
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
    pthread_mutex_destroy(&mutex);
    return 0;
}

在这个例子中,mutex互斥锁保护了shared_variable,确保每次只有一个线程可以对其进行增量操作,避免了竞态条件。

  1. 条件变量(Condition Variable) 条件变量用于线程间的同步,它允许线程等待某个条件满足。pthread库提供了以下函数来操作条件变量:
  • pthread_cond_init:初始化条件变量,原型为int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr)
  • pthread_cond_wait:使线程等待在条件变量上,同时会自动解锁关联的互斥锁,当条件变量被唤醒时,线程重新锁定互斥锁并继续执行。
  • pthread_cond_signal:唤醒等待在条件变量上的一个线程。
  • pthread_cond_broadcast:唤醒等待在条件变量上的所有线程。
  • pthread_cond_destroy:销毁条件变量,释放相关资源。

例如,生产者 - 消费者模型中使用条件变量:

#include <stdio.h>
#include <pthread.h>
#define BUFFER_SIZE 5
int buffer[BUFFER_SIZE];
int in = 0;
int out = 0;
pthread_mutex_t mutex;
pthread_cond_t not_full;
pthread_cond_t not_empty;
void *producer(void *arg) {
    int item = 1;
    while (1) {
        pthread_mutex_lock(&mutex);
        while ((in + 1) % BUFFER_SIZE == out) {
            pthread_cond_wait(&not_full, &mutex);
        }
        buffer[in] = item;
        printf("Produced: %d at position %d\n", item, in);
        in = (in + 1) % BUFFER_SIZE;
        pthread_cond_signal(&not_empty);
        pthread_mutex_unlock(&mutex);
        item++;
    }
    return NULL;
}
void *consumer(void *arg) {
    while (1) {
        pthread_mutex_lock(&mutex);
        while (in == out) {
            pthread_cond_wait(&not_empty, &mutex);
        }
        int item = buffer[out];
        printf("Consumed: %d from position %d\n", item, out);
        out = (out + 1) % BUFFER_SIZE;
        pthread_cond_signal(&not_full);
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}
int main() {
    pthread_t producer_thread, consumer_thread;
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&not_full, NULL);
    pthread_cond_init(&not_empty, NULL);
    pthread_create(&producer_thread, NULL, producer, NULL);
    pthread_create(&consumer_thread, NULL, consumer, NULL);
    pthread_join(producer_thread, NULL);
    pthread_join(consumer_thread, NULL);
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&not_full);
    pthread_cond_destroy(&not_empty);
    return 0;
}

在这个生产者 - 消费者模型中,条件变量not_fullnot_empty分别用于控制缓冲区不满和不空的情况,确保生产者和消费者在合适的时机进行操作。

C语言结构体与多线程编程的结合

结构体在多线程中的应用场景

  1. 传递复杂参数 在多线程编程中,pthread_create函数的arg参数只能传递一个指针。当需要传递多个参数给线程函数时,可以将这些参数封装在一个结构体中。例如,假设有一个线程函数需要处理文件读写,需要文件名和读写模式两个参数,就可以这样做:
#include <stdio.h>
#include <pthread.h>
#include <fcntl.h>
#include <unistd.h>
struct FileArgs {
    char *filename;
    int flags;
};
void *file_operation(void *arg) {
    struct FileArgs *args = (struct FileArgs *)arg;
    int fd = open(args->filename, args->flags);
    if (fd == -1) {
        perror("open");
        return NULL;
    }
    // 这里可以进行文件读写操作
    close(fd);
    return NULL;
}
int main() {
    struct FileArgs args = {"test.txt", O_RDONLY};
    pthread_t thread;
    pthread_create(&thread, NULL, file_operation, (void *)&args);
    pthread_join(thread, NULL);
    return 0;
}

在上述代码中,FileArgs结构体封装了文件名和文件打开标志,通过结构体指针传递给线程函数file_operation

  1. 共享数据结构 在多线程环境中,多个线程可能需要共享一个复杂的数据结构,结构体可以方便地定义这种共享数据。例如,一个多线程的银行账户管理系统,账户信息可以用结构体表示,多个线程可以对账户进行存款、取款等操作。
#include <stdio.h>
#include <pthread.h>
struct Account {
    int balance;
    pthread_mutex_t mutex;
};
void *deposit(void *arg) {
    struct Account *account = (struct Account *)arg;
    pthread_mutex_lock(&account->mutex);
    account->balance += 100;
    printf("Deposited 100. New balance: %d\n", account->balance);
    pthread_mutex_unlock(&account->mutex);
    return NULL;
}
void *withdraw(void *arg) {
    struct Account *account = (struct Account *)arg;
    pthread_mutex_lock(&account->mutex);
    if (account->balance >= 50) {
        account->balance -= 50;
        printf("Withdrew 50. New balance: %d\n", account->balance);
    } else {
        printf("Insufficient funds\n");
    }
    pthread_mutex_unlock(&account->mutex);
    return NULL;
}
int main() {
    struct Account account = {100, PTHREAD_MUTEX_INITIALIZER};
    pthread_t deposit_thread, withdraw_thread;
    pthread_create(&deposit_thread, NULL, deposit, (void *)&account);
    pthread_create(&withdraw_thread, NULL, withdraw, (void *)&account);
    pthread_join(deposit_thread, NULL);
    pthread_join(withdraw_thread, NULL);
    pthread_mutex_destroy(&account.mutex);
    return 0;
}

这里Account结构体不仅包含了账户余额balance,还包含了一个互斥锁mutex来保护余额的操作,确保多线程操作的安全性。

结构体与线程同步机制结合

  1. 结构体中包含互斥锁 如上面银行账户的例子,将互斥锁作为结构体的成员,可以方便地对结构体中的其他成员进行保护。这种方式使得对共享结构体数据的操作更加集中和易于管理。
#include <stdio.h>
#include <pthread.h>
struct SharedData {
    int value;
    pthread_mutex_t mutex;
};
void *update_data(void *arg) {
    struct SharedData *data = (struct SharedData *)arg;
    pthread_mutex_lock(&data->mutex);
    data->value += 1;
    printf("Updated value: %d\n", data->value);
    pthread_mutex_unlock(&data->mutex);
    return NULL;
}
int main() {
    struct SharedData data = {0, PTHREAD_MUTEX_INITIALIZER};
    pthread_t thread1, thread2;
    pthread_create(&thread1, NULL, update_data, (void *)&data);
    pthread_create(&thread2, NULL, update_data, (void *)&data);
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
    pthread_mutex_destroy(&data.mutex);
    return 0;
}

在这个示例中,SharedData结构体包含一个整数值value和一个互斥锁mutex,两个线程通过获取互斥锁来安全地更新value

  1. 结构体与条件变量结合 在更复杂的多线程场景中,结构体可以与条件变量结合使用。例如,在一个任务队列的场景中,任务可以用结构体表示,生产者线程将任务放入队列,消费者线程从队列中取出任务执行。
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#define QUEUE_SIZE 3
struct Task {
    int task_id;
    // 其他任务相关数据
};
struct TaskQueue {
    struct Task tasks[QUEUE_SIZE];
    int front;
    int rear;
    pthread_mutex_t mutex;
    pthread_cond_t not_empty;
    pthread_cond_t not_full;
};
void enqueue(struct TaskQueue *queue, struct Task task) {
    pthread_mutex_lock(&queue->mutex);
    while ((queue->rear + 1) % QUEUE_SIZE == queue->front) {
        pthread_cond_wait(&queue->not_full, &queue->mutex);
    }
    queue->tasks[queue->rear] = task;
    queue->rear = (queue->rear + 1) % QUEUE_SIZE;
    pthread_cond_signal(&queue->not_empty);
    pthread_mutex_unlock(&queue->mutex);
}
struct Task dequeue(struct TaskQueue *queue) {
    pthread_mutex_lock(&queue->mutex);
    while (queue->front == queue->rear) {
        pthread_cond_wait(&queue->not_empty, &queue->mutex);
    }
    struct Task task = queue->tasks[queue->front];
    queue->front = (queue->front + 1) % QUEUE_SIZE;
    pthread_cond_signal(&queue->not_full);
    pthread_mutex_unlock(&queue->mutex);
    return task;
}
void *producer(void *arg) {
    struct TaskQueue *queue = (struct TaskQueue *)arg;
    for (int i = 0; i < 5; i++) {
        struct Task task = {i};
        enqueue(queue, task);
        printf("Produced task %d\n", i);
    }
    return NULL;
}
void *consumer(void *arg) {
    struct TaskQueue *queue = (struct TaskQueue *)arg;
    while (1) {
        struct Task task = dequeue(queue);
        printf("Consumed task %d\n", task.task_id);
    }
    return NULL;
}
int main() {
    struct TaskQueue queue = {0, 0, PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER, PTHREAD_COND_INITIALIZER};
    pthread_t producer_thread, consumer_thread;
    pthread_create(&producer_thread, NULL, producer, (void *)&queue);
    pthread_create(&consumer_thread, NULL, consumer, (void *)&queue);
    pthread_join(producer_thread, NULL);
    pthread_cancel(consumer_thread);
    pthread_mutex_destroy(&queue.mutex);
    pthread_cond_destroy(&queue.not_empty);
    pthread_cond_destroy(&queue.not_full);
    return 0;
}

在这个任务队列的例子中,TaskQueue结构体包含任务数组、队列指针、互斥锁和条件变量。生产者线程通过enqueue函数将任务放入队列,消费者线程通过dequeue函数从队列中取出任务,条件变量用于同步生产者和消费者的操作。

注意事项

  1. 结构体的内存管理 在多线程环境中,要注意结构体的内存管理。如果结构体是在堆上分配的(例如使用malloc),需要确保在所有线程使用完毕后正确释放内存。同时,要避免多个线程同时释放同一块内存,这可能导致内存错误。
  2. 线程安全的结构体设计 当设计用于多线程的结构体时,要充分考虑线程安全性。除了使用互斥锁和条件变量等同步机制外,还要注意结构体成员的访问顺序和原子性。例如,对于一些简单的数值类型,如果对其操作不是原子的,即使加了锁也可能出现问题。在C11标准中,可以使用原子类型(如_Atomic)来确保某些操作的原子性。
  3. 避免死锁 在结构体与多线程结合使用时,由于可能涉及多个同步机制,要特别注意避免死锁。死锁通常发生在多个线程相互等待对方释放资源的情况下。例如,如果两个线程分别持有不同的互斥锁,并且都试图获取对方的互斥锁,就可能导致死锁。为了避免死锁,要确保所有线程以相同的顺序获取锁,或者使用超时机制来防止无限等待。

通过合理地将C语言结构体与多线程编程结合,可以有效地处理复杂的多任务场景,提高程序的性能和可维护性。但在实际应用中,需要仔细考虑各种潜在问题,确保程序的正确性和稳定性。