Linux C语言多线程创建的高级玩法
1. 多线程基础回顾
在深入探讨 Linux C 语言多线程创建的高级玩法之前,我们先来回顾一下多线程编程的基础知识。
线程是进程中的一个执行流,一个进程可以包含多个线程,这些线程共享进程的资源,如内存空间、文件描述符等。在 Linux 环境下,我们主要使用 POSIX 线程库(通常称为 pthread 库)来进行多线程编程。
要使用 pthread 库,我们需要包含头文件 <pthread.h>
。在编译时,需要链接 pthread 库,一般使用 -lpthread
选项。
1.1 简单线程创建示例
下面是一个简单的创建单个线程的示例代码:
#include <stdio.h>
#include <pthread.h>
// 线程执行函数
void* thread_function(void* arg) {
printf("This is a thread.\n");
return NULL;
}
int main() {
pthread_t thread;
// 创建线程
int ret = pthread_create(&thread, NULL, thread_function, NULL);
if (ret != 0) {
printf("Error creating thread: %d\n", ret);
return 1;
}
// 等待线程结束
ret = pthread_join(thread, NULL);
if (ret != 0) {
printf("Error joining thread: %d\n", ret);
return 1;
}
printf("Thread has finished.\n");
return 0;
}
在这个示例中,我们首先定义了一个 thread_function
函数,它将在线程中执行。然后在 main
函数中,使用 pthread_create
函数创建一个新线程。pthread_create
的第一个参数是指向 pthread_t
类型变量的指针,用于存储新线程的标识符;第二个参数通常为 NULL
,用于设置线程属性;第三个参数是线程执行函数的指针;第四个参数是传递给线程执行函数的参数。
创建线程后,我们使用 pthread_join
函数等待线程结束。pthread_join
的第一个参数是要等待的线程标识符,第二个参数用于获取线程执行函数的返回值(这里我们不需要,所以设为 NULL
)。
2. 线程属性的深入探讨
2.1 线程属性结构体
在 pthread_create
函数的第二个参数中,可以传入一个指向 pthread_attr_t
类型结构体的指针,用于设置线程的属性。这个结构体定义在 <pthread.h>
中,包含了许多成员来控制线程的各种特性。
要使用 pthread_attr_t
结构体,首先需要对其进行初始化,使用 pthread_attr_init
函数。在使用完毕后,需要使用 pthread_attr_destroy
函数来释放相关资源。
pthread_attr_t attr;
pthread_attr_init(&attr);
// 设置线程属性
//...
pthread_create(&thread, &attr, thread_function, NULL);
pthread_attr_destroy(&attr);
2.2 常见线程属性设置
2.2.1 线程栈大小
线程栈用于存储线程函数的局部变量、函数调用栈等。可以通过 pthread_attr_setstacksize
函数来设置线程栈的大小。
size_t stack_size = 8192 * 1024; // 8MB 栈大小
pthread_attr_setstacksize(&attr, stack_size);
如果栈大小设置过小,可能会导致线程在运行过程中栈溢出;而设置过大则会浪费内存资源。
2.2.2 线程调度策略与优先级
Linux 支持多种线程调度策略,如 SCHED_OTHER(普通调度策略)、SCHED_FIFO(先来先服务调度策略)和 SCHED_RR(时间片轮转调度策略)。可以使用 pthread_attr_setschedpolicy
函数来设置调度策略,并使用 pthread_attr_setschedparam
函数来设置调度参数,包括优先级。
struct sched_param param;
param.sched_priority = 50; // 优先级,值越大优先级越高
pthread_attr_setschedpolicy(&attr, SCHED_RR);
pthread_attr_setschedparam(&attr, ¶m);
需要注意的是,设置调度策略和优先级通常需要较高的权限,普通用户可能无法成功设置某些策略和优先级。
3. 线程同步机制的高级应用
3.1 互斥锁(Mutex)的深入使用
互斥锁是一种最基本的线程同步机制,用于保护共享资源,确保同一时间只有一个线程可以访问共享资源。
3.1.1 死锁问题及避免 死锁是多线程编程中常见的问题,当两个或多个线程相互等待对方释放资源时,就会发生死锁。例如:
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;
void* thread1_function(void* arg) {
pthread_mutex_lock(&mutex1);
// 模拟一些操作
sleep(1);
pthread_mutex_lock(&mutex2);
// 访问共享资源
pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);
return NULL;
}
void* thread2_function(void* arg) {
pthread_mutex_lock(&mutex2);
// 模拟一些操作
sleep(1);
pthread_mutex_lock(&mutex1);
// 访问共享资源
pthread_mutex_unlock(&mutex1);
pthread_mutex_unlock(&mutex2);
return NULL;
}
在这个例子中,如果 thread1
先获取了 mutex1
,thread2
先获取了 mutex2
,然后它们都试图获取对方持有的锁,就会发生死锁。
为了避免死锁,可以采用以下方法:
- 按照固定顺序获取锁:例如,总是先获取
mutex1
,再获取mutex2
,这样就不会出现相互等待的情况。 - 使用超时机制:在获取锁时设置一个超时时间,如果在超时时间内未能获取锁,则放弃并进行其他处理。可以使用
pthread_mutex_timedlock
函数来实现。
3.1.2 递归互斥锁 递归互斥锁允许同一个线程多次获取锁,而不会导致死锁。在某些情况下,一个线程可能需要多次访问被互斥锁保护的资源,例如一个递归函数访问共享资源。
pthread_mutexattr_t mutex_attr;
pthread_mutexattr_init(&mutex_attr);
pthread_mutexattr_settype(&mutex_attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_t recursive_mutex;
pthread_mutex_init(&recursive_mutex, &mutex_attr);
void recursive_function() {
pthread_mutex_lock(&recursive_mutex);
// 访问共享资源
// 递归调用
recursive_function();
pthread_mutex_unlock(&recursive_mutex);
}
3.2 条件变量(Condition Variable)的高级应用
条件变量用于线程间的同步,它允许线程等待某个条件满足后再继续执行。
3.2.1 生产者 - 消费者模型 生产者 - 消费者模型是条件变量的一个经典应用场景。假设有一个共享缓冲区,生产者线程向缓冲区中写入数据,消费者线程从缓冲区中读取数据。
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#define BUFFER_SIZE 5
int buffer[BUFFER_SIZE];
int in = 0;
int out = 0;
int count = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t not_full = PTHREAD_COND_INITIALIZER;
pthread_cond_t not_empty = PTHREAD_COND_INITIALIZER;
void* producer(void* arg) {
int i;
for (i = 0; i < 10; i++) {
pthread_mutex_lock(&mutex);
while (count == BUFFER_SIZE) {
pthread_cond_wait(¬_full, &mutex);
}
buffer[in] = i;
printf("Produced: %d\n", buffer[in]);
in = (in + 1) % BUFFER_SIZE;
count++;
pthread_cond_signal(¬_empty);
pthread_mutex_unlock(&mutex);
}
return NULL;
}
void* consumer(void* arg) {
int i;
for (i = 0; i < 10; i++) {
pthread_mutex_lock(&mutex);
while (count == 0) {
pthread_cond_wait(¬_empty, &mutex);
}
int data = buffer[out];
printf("Consumed: %d\n", data);
out = (out + 1) % BUFFER_SIZE;
count--;
pthread_cond_signal(¬_full);
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main() {
pthread_t producer_thread, consumer_thread;
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(¬_full);
pthread_cond_destroy(¬_empty);
return 0;
}
在这个例子中,生产者线程在缓冲区满时等待 not_full
条件变量,消费者线程在缓冲区空时等待 not_empty
条件变量。当条件满足时,通过 pthread_cond_signal
函数唤醒等待的线程。
4. 线程池技术
4.1 线程池概念
线程池是一种多线程处理模式,它预先创建一定数量的线程,并将这些线程放入线程池中。当有任务到来时,从线程池中取出一个线程来执行任务,任务完成后,线程并不销毁,而是返回线程池等待下一个任务。
线程池的优点包括:
- 减少线程创建和销毁的开销,提高系统性能。
- 控制并发线程的数量,避免过多线程导致系统资源耗尽。
4.2 线程池实现示例
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#define THREADS 4
#define QUEUE_SIZE 10
// 任务结构体
typedef struct {
void (*function)(void*);
void* arg;
} task;
// 线程池结构体
typedef struct {
task queue[QUEUE_SIZE];
int front;
int rear;
int count;
pthread_mutex_t mutex;
pthread_cond_t not_empty;
pthread_cond_t not_full;
int shutdown;
} thread_pool;
// 线程执行函数
void* worker(void* arg) {
thread_pool* pool = (thread_pool*)arg;
while (1) {
pthread_mutex_lock(&pool->mutex);
while (pool->count == 0 &&!pool->shutdown) {
pthread_cond_wait(&pool->not_empty, &pool->mutex);
}
if (pool->shutdown && pool->count == 0) {
pthread_mutex_unlock(&pool->mutex);
pthread_exit(NULL);
}
task t = pool->queue[pool->front];
pool->front = (pool->front + 1) % QUEUE_SIZE;
pool->count--;
pthread_cond_signal(&pool->not_full);
pthread_mutex_unlock(&pool->mutex);
t.function(t.arg);
}
return NULL;
}
// 初始化线程池
void init_thread_pool(thread_pool* pool) {
pool->front = 0;
pool->rear = 0;
pool->count = 0;
pool->shutdown = 0;
pthread_mutex_init(&pool->mutex, NULL);
pthread_cond_init(&pool->not_empty, NULL);
pthread_cond_init(&pool->not_full, NULL);
}
// 添加任务到线程池
void add_task(thread_pool* pool, void (*function)(void*), void* arg) {
pthread_mutex_lock(&pool->mutex);
while (pool->count == QUEUE_SIZE &&!pool->shutdown) {
pthread_cond_wait(&pool->not_full, &pool->mutex);
}
if (pool->shutdown) {
pthread_mutex_unlock(&pool->mutex);
return;
}
pool->queue[pool->rear].function = function;
pool->queue[pool->rear].arg = arg;
pool->rear = (pool->rear + 1) % QUEUE_SIZE;
pool->count++;
pthread_cond_signal(&pool->not_empty);
pthread_mutex_unlock(&pool->mutex);
}
// 销毁线程池
void destroy_thread_pool(thread_pool* pool) {
pthread_mutex_lock(&pool->mutex);
pool->shutdown = 1;
pthread_cond_broadcast(&pool->not_empty);
pthread_cond_broadcast(&pool->not_full);
pthread_mutex_unlock(&pool->mutex);
int i;
for (i = 0; i < THREADS; i++) {
pthread_join(pool->threads[i], NULL);
}
pthread_mutex_destroy(&pool->mutex);
pthread_cond_destroy(&pool->not_empty);
pthread_cond_destroy(&pool->not_full);
}
// 示例任务函数
void task_function(void* arg) {
int num = *((int*)arg);
printf("Task %d is running.\n", num);
sleep(1);
}
int main() {
thread_pool pool;
init_thread_pool(&pool);
pthread_t threads[THREADS];
int i;
for (i = 0; i < THREADS; i++) {
pthread_create(&threads[i], NULL, worker, &pool);
}
int tasks[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
for (i = 0; i < 10; i++) {
add_task(&pool, task_function, &tasks[i]);
}
sleep(3);
destroy_thread_pool(&pool);
return 0;
}
在这个线程池实现中,我们定义了 task
结构体来表示任务,thread_pool
结构体来管理线程池。线程池包含任务队列、互斥锁、条件变量等成员。worker
函数是线程执行的函数,它从任务队列中取出任务并执行。init_thread_pool
函数用于初始化线程池,add_task
函数用于向线程池添加任务,destroy_thread_pool
函数用于销毁线程池。
5. 多线程与信号处理
5.1 信号与多线程的关系
在多线程程序中,信号处理会带来一些特殊的问题。默认情况下,信号会发送到进程中的任意一个线程。这可能导致一些意外的行为,因为不同线程可能处于不同的执行状态。
为了更好地控制信号处理,我们可以使用 pthread_sigmask
函数来设置线程的信号掩码,从而决定哪些信号该线程可以接收。
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
pthread_sigmask(SIG_BLOCK, &set, NULL);
在这个例子中,我们将 SIGINT
信号(通常由用户按 Ctrl+C 产生)阻塞,使得当前线程不会接收到该信号。
5.2 线程特定的信号处理
我们可以设置某个线程专门处理特定的信号。首先,我们需要创建一个新线程,并在该线程中设置信号处理函数。
#include <stdio.h>
#include <pthread.h>
#include <signal.h>
#include <unistd.h>
void signal_handler(int signum) {
printf("Caught signal %d in signal handling thread.\n", signum);
}
void* signal_thread(void* arg) {
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
pthread_sigmask(SIG_UNBLOCK, &set, NULL);
struct sigaction sa;
sa.sa_handler = signal_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, NULL);
while (1) {
sleep(1);
}
return NULL;
}
int main() {
pthread_t signal_thread_id;
pthread_create(&signal_thread_id, NULL, signal_thread, NULL);
while (1) {
printf("Main thread is running.\n");
sleep(1);
}
return 0;
}
在这个例子中,我们创建了一个专门处理 SIGINT
信号的线程。在该线程中,我们先解除对 SIGINT
信号的阻塞,然后设置信号处理函数 signal_handler
。主线程则继续执行自己的任务。
6. 多线程调试技巧
6.1 使用 GDB 调试多线程程序
GDB 是一个强大的调试工具,它支持多线程调试。在调试多线程程序时,可以使用以下 GDB 命令:
info threads
:显示当前所有线程的信息,包括线程 ID、线程状态等。thread <thread_id>
:切换到指定线程进行调试。break <function_name>
:在指定函数处设置断点,该断点会作用于所有线程。break <function_name> thread <thread_id>
:在指定线程的指定函数处设置断点。
例如,我们有一个简单的多线程程序:
#include <stdio.h>
#include <pthread.h>
void* thread_function(void* arg) {
int i;
for (i = 0; i < 5; i++) {
printf("Thread is running: %d\n", i);
sleep(1);
}
return NULL;
}
int main() {
pthread_t thread;
pthread_create(&thread, NULL, thread_function, NULL);
int i;
for (i = 0; i < 5; i++) {
printf("Main thread is running: %d\n", i);
sleep(1);
}
pthread_join(thread, NULL);
return 0;
}
使用 GDB 调试时,可以这样操作:
gdb./a.out
(gdb) break thread_function
(gdb) run
(gdb) info threads
(gdb) thread <thread_id>
(gdb) next
6.2 日志记录调试
在多线程程序中,日志记录是一种非常有效的调试方法。通过在关键代码位置添加日志输出,可以了解线程的执行流程和状态。
#include <stdio.h>
#include <pthread.h>
#include <time.h>
#include <sys/time.h>
void log_message(const char* message) {
struct timeval tv;
gettimeofday(&tv, NULL);
struct tm* tm_info;
tm_info = localtime(&tv.tv_sec);
char time_str[26];
strftime(time_str, 26, "%Y-%m-%d %H:%M:%S", tm_info);
printf("%s.%06ld %s\n", time_str, tv.tv_usec, message);
}
void* thread_function(void* arg) {
log_message("Thread started");
int i;
for (i = 0; i < 5; i++) {
log_message("Thread is running");
sleep(1);
}
log_message("Thread ended");
return NULL;
}
int main() {
pthread_t thread;
pthread_create(&thread, NULL, thread_function, NULL);
int i;
for (i = 0; i < 5; i++) {
log_message("Main thread is running");
sleep(1);
}
pthread_join(thread, NULL);
return 0;
}
在这个例子中,log_message
函数用于记录日志,包括时间戳和具体消息。通过查看日志,可以清楚地了解线程的运行情况。
通过上述对 Linux C 语言多线程创建的高级玩法的探讨,我们深入了解了线程属性设置、同步机制、线程池技术、信号处理以及调试技巧等方面的知识。这些技术和方法在实际的多线程编程中非常实用,能够帮助我们编写出高效、稳定的多线程程序。在实际应用中,需要根据具体的需求和场景,合理选择和运用这些技术。