目录
1、进程与线程
Linux 线程是在 Linux 操作系统中运行的基本执行单元。线程是进程的一部分,共享同一地址空间和其他资源,但拥有独立的执行流。在 Linux 中,线程被称为轻量级进程(LWP,Lightweight Process),但轻量级进程更多指的是内核线程(kernel thread),而把用户线程(user thread)称为线程。
"进程——资源分配的最小单位,线程——程序执行的最小单位"
进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。进程有自己的堆栈和局部变量,但线程没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
2、线程的优势与使用理由
从上面我们大概了解了进程与线程的区别,使用多线程的理由主要包括资源节俭、方便的通信机制、提高应用程序响应性、有效利用多CPU系统以及改善程序结构。这些优点使得多线程成为处理并发任务的一种强大工具,特别是在需要高效利用系统资源和提高程序性能的场景下。当然,使用多线程也需要注意线程安全性和避免潜在的并发问题。
这部分摘抄于网络:
使用多线程的理由之一是和进程相比,它是一种非常"节俭"的多任务操作方式。我们知道,在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右,当然,在具体的系统上,这个数据可能会有较大的区别。
使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。
除了以上所说的优点外,不和进程比较,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:
- 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。
- 使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
- 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。
3、多线程的使用
多线程开发在Linux开发平台上已经有成熟的pthread库支持,详细请见下表:
3.1 线程的使用
1.线程创建函数
#include <pthread.h> 注意都是调用这个库
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void *), void *arg);
参数说明:
thread
:指向新创建的线程标识符的指针,该标识符用于标识新线程。attr
:指向线程属性的指针,用于指定新线程的属性,通常可以设置为NULL
,表示使用默认属性。start_routine
:是一个函数指针,指向新线程将要执行的函数。该函数应该接受一个void*
类型的参数并返回一个void*
类型的指针。arg
:是传递给start_routine
函数的参数。如果需要向start_routine
函数传递的参数不止一个,那么需要把这些参数放到一个结构中,然后把这个结构的地址作为arg参数传入。
pthread_create
的返回值为 0 表示成功创建线程,非零表示创建线程失败,返回的错误码可以用errno
查看。
2.线程退出函数
单个线程可以通过以下三种方式退出,在不终止整个进程的情况下停止它的控制流:
1)线程只是从启动例程中返回,返回值是线程的退出码。
2)线程可以被同一进程中的其他线程取消。
3)线程调用pthread_exit:
函数原型
void pthread_exit(void *retval);
retval是一个无类型指针,线程可以选择在执行过程中退出,并提供一个退出状态。这个状态可以在其他线程中通过调用pthread_join
来获取,用于了解线程的退出状态。使用这个函数可以确保线程资源的正确清理。
退出的状态retval变量必须是static类型的,不然系统会随便给你一个变量
3.线程的等待
函数原型
int pthread_join(pthread_t thread, void **retval);
pthread_t thread
是要等待的线程的标识符。void **retval
是一个指针,用于存储线程的退出状态。这个参数可以为NULL
,表示不关心线程的退出状态。
pthread_join
函数的返回值为 0 表示成功等待线程结束,非零表示等待失败。如果成功,线程的退出状态将存储在retval
指向的地址中
4.线程脱离
一个线程或者是可汇合(joinable,默认值),或者是脱离的(detached)。当一个可汇合的线程终止时,它的线程ID和退出状态将留存到另一个线程对它调用pthread_join。脱离的线程却像守护进程,当它们终止时,所有相关的资源都被释放,我们不能等待它们终止,使用pthread_detach函数可以把一个线程标记为脱离状态。如果一个线程需要知道另一线程什么时候终止,那就最好保持第二个线程的可汇合状态。
函数原型
int pthread_detach(pthread_t thread);
pthread_t thread
是要设置为可分离状态的线程的标识符。pthread_detach
函数的返回值为 0 表示成功,非零表示失败。
5. 线程ID获取及比较
pthread_t pthread_self(void);
// 返回:调用线程的ID
对于线程ID比较,为了可移植操作,我们不能简单地把线程ID当作整数来处理,因为不同系统对线程ID的定义可能不一样。我们应该要用下边的函数:
int pthread_equal(pthread_t tid1, pthread_t tid2);
//用于比较两个线程的标识符是否相等
// 返回:若相等则返回非0值,否则返回0
6.示例
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define THREAD_EXIT_STATUS "happy"
void *start_rtn(void *arg) {
int data = *((int *)arg);
printf("data = %d\n", data);
printf("pthread_pid = %ld\n", pthread_self());
// 注意:如果线程函数返回的字符串是动态分配的内存,需要在主线程中释放
//strdup:用于创建一个字符串的副本并返回指向这个副本的指针。
//方式一:
//char *exit_status = strdup(THREAD_EXIT_STATUS);
//方式二:
static char *exit_status = "happy";`
pthread_exit((void *)exit_status);
}
int main() {
pthread_t pthread_pid;
int dis = 100;
char *exit_status = NULL;
// 添加错误处理
if (pthread_create(&pthread_pid, NULL, start_rtn, (void *)&dis) != 0) {
perror("Thread creation failed");
return 1;
}
// 添加错误处理
if (pthread_join(pthread_pid, (void **)&exit_status) != 0) {
perror("Thread join failed");
return 1;
}
printf("main - - data = %s\n", exit_status);
// 释放动态分配的内存
free(exit_status);
printf("main pthread_pid = %ld\n", pthread_self());
return 0;
}
3.2 互斥锁的应用
互斥锁(Mutex,全名为 Mutual Exclusion)是一种用于多线程编程的同步机制,用于保护共享资源,防止多个线程同时访问或修改这些资源。互斥锁的基本思想是一次只允许一个线程进入临界区(临界区是指访问共享资源的代码段),其他线程需要等待当前线程释放锁后才能进入。互斥变量用pthread_mutex_t数据类型表示。在使用互斥变量前必须对它进行初始化,可以把它置为常量PTHREAD_MUTEX_INITIALIZER(只对静态分配的互斥量),也可以通过调用pthread_mutex_init函数进行初始化。如果动态地分配互斥量(例如通过调用malloc函数),那么在释放内存前需要调用pthread_mutex_destroy。
1.互斥锁相关API
//创建互斥锁对象,PTHREAD_MUTEX_INITIALIZER是一个宏,用于静态初始化互斥锁。在声明互斥锁变量时,可以使用这个宏进行初始化,而无需调用 pthread_mutex_init pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //销毁互斥锁 int pthread_mutex_destroy(pthread_mutex_t *mutex); //初始化互斥锁,第二个参数是 NULL 的话,互斥锁的属性会设置为默认属性 int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); //阻塞调用,如果这个锁此时正在被其它线程占用, 那么 pthread_mutex_lock() 调用会进入到这个锁的排队队列中,并会进入阻塞状态, 直到拿到锁之后才会返回。 int pthread_mutex_lock(pthread_mutex_t *mutex); //非阻塞调用,当请求的锁正砸被其他线程占用时, 不会进入阻塞状态,而是立刻返回,并返回一个错误代码 EBUSY,意思是说, 有其它线程正在使用这个锁。 int pthread_mutex_trylock(pthread_mutex_t *mutex); //释放锁 int pthread_mutex_unlock(pthread_mutex_t *mutex); //在指定时间内等待互斥锁,超时返回错误 int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abs_timeout);
2.示例
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
int dis = 0;
pthread_mutex_t mutex;
// 线程1的执行函数
void *func1(void *data) {
static int ret = 10;
while (1) {
pthread_mutex_lock(&mutex);
printf("func1: %ld\n", pthread_self());
printf("func1: %s\n", (char *)data);
printf("func1 -> dis = %d\n", dis++);
pthread_mutex_unlock(&mutex);
sleep(1);
}
pthread_exit((void *)&ret);
}
// 线程2的执行函数
void *func2(void *data) {
static int ret = 20;
while (1) {
pthread_mutex_lock(&mutex);
printf("func2: %ld\n", pthread_self());
printf("func2: %s\n", (char *)data);
printf("func2 -> dis = %d\n", dis++);
pthread_mutex_unlock(&mutex);
sleep(1);
}
pthread_exit((void *)&ret);
}
// 线程3的执行函数
void *func3(void *data) {
static int ret = 30;
while (1) {
pthread_mutex_lock(&mutex);
printf("func3: %ld\n", pthread_self());
printf("func3: %s\n", (char *)data);
printf("func3 -> dis = %d\n", dis++);
pthread_mutex_unlock(&mutex);
if (dis == 5) {
//当dis加到5时线程3会退出,其他线程继续循环
pthread_exit((void *)&ret);
}
}
}
int main() {
pthread_t tidp1, tidp2, tidp3;
char *arg = "happy bay!!!";
int *ret;
// 初始化互斥锁
pthread_mutex_init(&mutex, NULL);
// 创建三个线程
pthread_create(&tidp1, NULL, func1, (void *)arg);
pthread_create(&tidp2, NULL, func2, (void *)arg);
pthread_create(&tidp3, NULL, func3, (void *)arg);
while (1) {
printf("main -> dis = %d\n", dis++);
sleep(1);
}
// 等待线程1结束
pthread_join(tidp1, (void **)&ret);
printf("Thread 1 returned: %d\n", *ret);
// 等待线程2结束
pthread_join(tidp2, (void **)&ret);
printf("Thread 2 returned: %d\n", *ret);
// 等待线程3结束
pthread_join(tidp3, (void **)&ret);
printf("Thread 3 returned: %d\n", *ret);
// 销毁互斥锁
pthread_mutex_destroy(&mutex);
return 0;
}
3.3 条件变量的使用
条件变量是线程另一可用的同步机制。条件变量给多个线程提供了一个会合的场所。条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。条件本身是由互斥量保护的。线程在改变条件状态前必须首先锁住互斥量,其他线程在获得互斥量之前不会察觉到这种改变,因为必须锁定互斥量以后才能计算条件。条件变量使用之前必须首先初始化,pthread_cond_t数据类型代表的条件变量可以用两种方式进行初始化,可以把常量PTHREAD_COND_INITIALIZER赋给静态分配的条件变量,但是如果条件变量是动态分配的,可以使用pthread_cond_destroy函数对条件变量进行去除初始化(deinitialize)。
1. 创建及销毁条件变量
#include <pthread.h> //初始化条件变量 int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr); //销毁条件变量 int pthread_cond_destroy(pthread_cond_t cond); // 返回:若成功返回0,否则返回错误编号
除非需要创建一个非默认属性的条件变量,否则pthread_cont_init函数的attr参数可以设置为NULL
2. 等待
#include <pthread.h> //等待条件变量满足,同时释放互斥锁,使得其他线程可以获取互斥锁 int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex); int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, cond struct timespec *restrict timeout); // 返回:若成功返回0,否则返回错误编号
pthread_cond_wait等待条件变为真。如果在给定的时间内条件不能满足,那么会生成一个代表一个出错码的返回变量。传递给pthread_cond_wait的互斥量对条件进行保护,调用者把锁住的互斥量传给函数。函数把调用线程放到等待条件的线程列表上,然后对互斥量解锁,这两个操作都是原子操作。这样就关闭了条件检查和线程进入休眠状态等待条件改变这两个操作之间的时间通道,这样线程就不会错过条件的任何变化。pthread_cond_wait返回时,互斥量再次被锁住。
pthread_cond_timedwait函数的工作方式与pthread_cond_wait函数类似,只是多了一个timeout。timeout指定了等待的时间,它是通过timespec结构指定。
3. 触发
#include <pthread.h> //唤醒等待条件变量的一个线程 int pthread_cond_signal(pthread_cond_t cond); //唤醒等待条件变量的所有线程 int pthread_cond_broadcast(pthread_cond_t cond); // 返回:若成功返回0,否则返回错误编号
这两个函数可以用于通知线程条件已经满足。pthread_cond_signal函数将唤醒等待该条件的某个线程,而pthread_cond_broadcast函数将唤醒等待该条件的所有进程。
注意一定要在改变条件状态以后再给线程发信号。
4.示例
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
//初值为一个包含所有字段为 0 的结构体
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int shared_data = 0;
void *producer(void *arg) {
while (1) {
pthread_mutex_lock(&mutex);
// 生产者线程更新共享资源
shared_data++;
printf("Producer produced item: %d\n", shared_data);
pthread_mutex_unlock(&mutex);
// 在解锁之后,通知等待条件的线程
pthread_cond_signal(&cond);
// 模拟生产过程
sleep(1);
}
}
void *consumer(void *arg) {
while (1) {
pthread_mutex_lock(&mutex);
// 检查条件,如果条件不满足,则等待
while (shared_data == 0) {
pthread_cond_wait(&cond, &mutex);
}
// 消费者线程消耗共享资源
printf("Consumer consumed item: %d\n", shared_data);
shared_data--;
pthread_mutex_unlock(&mutex);
// 模拟消费过程
sleep(1);
}
}
int main() {
pthread_t producer_tid, consumer_tid;
// 创建生产者线程和消费者线程
pthread_create(&producer_tid, NULL, producer, NULL);
pthread_create(&consumer_tid, NULL, consumer, NULL);
// 等待线程结束
pthread_join(producer_tid, NULL);
pthread_join(consumer_tid, NULL);
// 销毁互斥锁和条件变量
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}