文章目录
多线程
多线程概念
在Linux中,线程是进程内的执行单元。换句话说,线程是进程内部的子任务,它们共享相同的进程资源,如内存空间、文件描述符等。线程在进程内部运行,本质就是在进程地址空间内运行。并且每个线程都有自己的执行路径和栈,但它们可以访问相同的全局变量和数据结构。
多线程优点
多线程允许程序同时执行多个任务,从而提高了系统的响应速度和吞吐量,对于处理大量并发请求或任务非常有用。并且线程之间可以共享相同的内存地址空间,因此它们可以更容易地共享数据和通信,而不需要复杂的进程间通信机制。与多个独立进程相比,线程之间的切换成本较低,因为它们共享相同的上下文,使得创建和销毁线程相对较快。相对于进程间通信,线程之间的通信和同步通常更容易实现,因为它们共享相同的地址空间,简化了编程工作。多线程在多核处理器上还可以更有效地利用多个处理核心,从而提高性能。多线程也可以用于创建具有更快响应时间的应用程序,例如图形用户界面(GUI)应用程序,以便用户可以同时执行多个操作。
优点:
- 并发性
- 资源共享
- 资源节省
- 简化编程
- 更好的利用多核处理器
- 响应性
- 多任务处理
多线程在计算机编程中还有许多优点,这些优点使其成为处理并发任务的强大工具。
多线程缺点
凡事都有两面性,说完优点再来盘一下多线程的缺点。由于多线程共享相同的内存空间,因此可能会导致竞态条件,即多个线程同时尝试访问和修改共享数据,导致不可预测的结果和错误。还有当多个线程互相等待彼此释放资源时,可能会发生死锁,导致所有线程被阻塞,无法继续执行。每个线程都有自己的栈和上下文切换开销,创建大量线程可能会消耗大量系统资源,导致性能下降。并且多线程编程通常比单线程编程更复杂,需要处理同步、互斥、线程安全等问题,容易出现错误。这些多线程应用程序中的错误可能会更难以调试和重现,因为问题可能是由于特定的线程交互导致的,而这些交互可能是不确定的。其中多线程应用程序中的线程执行顺序和时间是不确定的,这也使得难以预测和控制应用程序的行为。虽然多线程可以在多核处理器上提高性能,但过多的线程可能会导致线程切换开销增加,从而降低性能。多线程应用程序可能容易受到一些安全漏洞,如数据泄漏、竞态条件漏洞等的影响。
缺点:
- 竞态条件
- 死锁
- 资源消耗
- 复杂性
- 调试困难
- 不确定性
- 性能下降
- 安全性问题
所以说要充分利用多线程的优势并避免其缺点,需要谨慎设计和测试多线程应用程序,并使用适当的同步机制来管理线程间的资源访问。此外,使用并发编程框架和工具可以帮助减轻一些多线程编程的挑战。
线程和进程
我们都知道进程是独立的执行单元,拥有独立的内存空间、文件描述符、系统资源等。每个进程都是一个独立的程序实例。而线程是进程内的执行单元,共享相同的内存空间、文件描述符和其他进程资源。可以说线程是进程的子任务,也被称为轻量级进程。因此进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位。虽说线程共享进程数据,但是他们也拥有自己的一部分数据,例如线程ID,一组寄存器,栈,errno,信号屏蔽字,调度优先级。
Linux线程控制
在Linux中要进行线程控制就需要用到pthread库来实现线程控制。pthread库提供了创建、销毁、等待和管理线程的函数。而pthread是POSIX线程库的一部分,它为多线程编程提供了一组标准化的接口和工具。要使用这些函数库,要通过引入头文<pthread.h>,并且链接这些线程函数库时要使用编译器命令的"-lpthread"选项。
POSIX线程库
POSIX线程库,通常称为pthread库,是一种用于多线程编程的标准库。POSIX是Portable Operating System Interface的缩写,它定义了一套API(应用程序编程接口),用于提高在不同操作系统上移植应用程序的可移植性。一些关键的pthread库功能包括:
-
线程创建和管理:pthread库允许程序员创建、销毁和管理线程。使用pthread库,你可以在应用程序中创建多个线程,以实现并发执行。
-
线程同步:提供了互斥锁、条件变量等同步机制,用于控制线程对共享资源的访问,防止竞态条件和确保数据一致性。
-
线程间通信:支持线程间的通信机制,如信号量、消息队列等,以便线程之间能够进行有效的信息交换。
-
线程属性:允许设置和获取线程的属性,如优先级、栈大小等。
-
线程取消:提供了线程取消机制,使得一个线程能够取消另一个线程的执行。
-
线程安全函数:POSIX线程库定义了一组线程安全的C标准库函数,这些函数可在多线程环境中安全使用。
在许多类Unix系统和类Unix系统兼容的操作系统上,包括Linux,pthread库是广泛使用的。开发者可以使用这个库在不同的平台上实现可移植的多线程应用程序。在使用pthread库时,通常需要在编译时链接libpthread库。
线程的创建
在Linux中可以用pthread_create函数来创建线程,他是一个 POSIX 线程库中的函数。它的函数原型如下:
其中它的参数 thread 是一个指向 pthread_t 类型的指针,用于存储新线程的标识符。attr 参数是一个指向 pthread_attr_t 类型的指针,用于设置线程的属性,通常可以传入 NULL 以使用默认属性。start_routine 参数是一个指向函数的指针,这个函数是新线程要执行的函数,它的参数和返回值都是 void *。arg 参数是一个传递给 start_routine 函数的参数。
其中pthread_t类型是一个无符号长整型,用于声明线程ID。
pthread_attr_t是一个用于线程属性的struct,它定义了线程的一些属性,例如线程的调度策略、堆栈大小、优先级等。
pthread_create 函数创建一个新线程,新线程会执行 start_routine 函数,并将 arg 作为参数传递给它。线程的标识符会存储在 thread 指向的位置。如果成功创建线程,pthread_create 返回0,否则返回一个非零的错误码,可以使用 pthread_strerror 函数来获取错误信息。
cpp
void *thread_run(void *args)
{
for (int i = 0; i < 5; i++)
{
cout << "run....." << i + 1 << endl;
sleep(1);
}
cout << "线程退出......" << endl;
return nullptr;
}
int main()
{
pthread_t t1;
pthread_create(&t1, nullptr, thread_run, nullptr);
while(1)
{
usleep(1200000);//停止时间岔开,不然打印出来乱了
cout << "我是主线程" << endl;
}
return 0;
}
进程ID获取
在线程中如果想获取线程ID,可以使用pthread_self() 函数,用于获取调用线程的唯一标识符,即线程ID(Thread ID)。函数的原型如下:
它不接受任何参数,直接返回调用该函数的线程的ID,返回值类型为 pthread_t。通常情况下,你可以使用 pthread_self() 函数获取当前线程的ID,并将其用于线程管理、线程间通信或其他需要唯一标识线程的场景。可以将上述代码稍作修改,如下:
cpp
void *thread_run(void *args)
{
for (int i = 0; i < 5; i++)
{
cout << "run....." << i << " self id : " << pthread_self() << endl;
sleep(1);
}
cout << "线程退出......" << endl;
return nullptr;
}
int main()
{
pthread_t t1;
pthread_create(&t1, nullptr, thread_run, nullptr);
while(1)
{
usleep(1200000);
cout << "我是主线程" << endl;
}
return 0;
}
在这里需要注意的是,这里所说的线程ID是 pthread 库给每个线程定义的进程内唯一标识,是 pthread 库来进行维护的。由于线程是进程的一个执行分支,而每个进程有自己独立的内存空间,所以这个线程ID的作用域是进程级(该进程内,内核不认识)。
而pthread 库实际上是建立在操作系统内核提供的基础之上的。当程序中使用 pthread 库创建一个新线程时,实际上是通过调用内核提供的系统调用(如 clone)来完成。这个系统调用会在内核层面创建一个新的线程。每个由内核创建的线程都会被分配一个全局唯一的标识符,用来唯一地标识这个线程。这个标识符由内核维护,对用户空间程序是不可见的,但可以通过 pthread 库提供的接口来获取。这个系统全局唯一的ID叫做线程PID,或叫做TID,也有叫做LWP。可以用命令
ps -aL
来查看,如下图:
线程终止
线程终止有异常终止与正常终止,当我们如果需要只终止某个线程而不终止整个进程,可以使用return来使进程返回终止,但是这种方法对主线程不适用,因为从main函数return相当于调用exit,会导致所有线程都退出。还有就是可以使用pthread_exit函数来终止自己,pthread_exit() 函数也是 POSIX 线程库中的一个函数,用于终止当前线程的执行并返回一个指定的退出状态。这个函数允许线程在结束时提供一个状态码,以便其他线程可以通过 pthread_join() 函数获取这个状态码。该函数原型如下:
retval 参数是一个指针,用于传递线程的退出状态。这个状态可以是任何指针类型的数据,通常用于传递线程的返回值或其他有用的信息。当线程调用 pthread_exit() 时,它将会立即终止,并将 retval 的值传递给等待它的线程(如果有的话)。如果不需要返回状态,可以简单地传递 NULL。
需要注意,pthread_exit 或者 return 返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了,该栈上的临时对象也就销毁了。
如下代码:
cpp
void *thread_run(void *args)
{
for (int i = 0; i < 5; i++)
{
cout << "run....." << i << " self id : " << pthread_self() << endl;
sleep(3);
cout << "pthread_exit" << endl;
pthread_exit(nullptr);
}
cout << "线程退出......" << endl;
return nullptr;
}
int main()
{
pthread_t t1;
pthread_create(&t1, nullptr, thread_run, nullptr);
sleep(5);
cout << "主线程退出" << endl;
return 0;
}
新线程 t1 在运行 thread_run 函数后,进入循环打印一条信息后休眠3秒,然后打印pthread_exit再调用该函数直接退出。主线程在启动 t1 后会休眠5秒,然后打印 "主线程退出" 消息。
除了使用这个函数之外,一个线程还可以调用 pthread_ cancel 函数来终止同一进程中的另一个线程,原型如下:
pthread_cancel() 函数允许一个线程向另一个线程发送取消请求,但不会立即终止目标线程的执行。取消请求会等待目标线程在取消点处停止执行,然后才能生效。取消点是指程序中可以接收取消请求的位置,这些位置通常是系统调用或标准库函数内部的位置。例如sleep()、read()、write() 等函数都包含取消点。当目标线程执行到取消点时,取消请求将生效,目标线程会被终止。pthread_cancel 函数的返回值是0表示成功,非0值表示发生了错误。如果 pthread_cancel 返回非0值,可以使用 strerror 函数来获取错误描述。
如下代码:
cpp
void *thread_function(void *arg)
{
for (int i = 0; i < 5; ++i)
{
cout << "Thread is running... " << i << endl;
sleep(1);
}
cout << "Thread execution completed." << endl;
pthread_exit(NULL);
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, thread_function, NULL);
sleep(2);
pthread_cancel(tid);
cout << "Main thread exiting." << endl;
return 0;
}
主线程在休眠2秒后向tid线程发送取消请求,tid线程退出。
线程等待
与进程等待类似,有时候我们让线程去执行某个任务,我们需要知道执行的结果,因此就需要进行线程等待。除此之外,有时候线程等待还要收集线程结果,线程可能会返回一个结果或执行一些计算,而父线程需要获取这些结果或计算的值。通过线程等待父线程可以获得子线程的退出状态或其他返回值。线程等待还可以进行资源回收,当一个线程结束时,它可能会占用一些系统资源(如内存)。因此可以使用线程等待确保线程结束后,相关资源能够被正确释放,从而避免资源泄漏。进行线程等待可以使用 pthread_join 函数。这个函数的主要作用是阻塞当前线程,直到指定的线程完成执行为止。以下是 pthread_join 函数的原型:
thread 参数是要等待的目标线程的线程ID。retval 参数是一个指向指针的指针(即二级指针),用于存储目标线程的退出状态。如果不需要获取线程的退出状态,可以将 retval 设置为 NULL。pthread_join() 函数的返回值是一个整数,用于表示函数调用的成功或失败。通常,成功时返回0,失败时返回一个非零值,表示出现了错误。
如下代码:
cpp
void *thread_function(void *arg)
{
for (int i = 0; i < 5; ++i)
{
cout << "Thread is running... " << i << endl;
sleep(1);
}
cout << "Thread execution completed." << endl;
pthread_exit(NULL);
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, thread_function, NULL);
pthread_join(tid, nullptr);
cout << "Main thread exiting." << endl;
return 0;
}
线程分离
线程分离是一种线程属性,用于定义线程的生命周期和资源管理方式。在默认情况下,新创建的线程是 joinable 的,也就是需要被 pthread_join 等待的,否则线程退出后,无法释放资源,从而造成系统泄漏。如果不关心线程的返回值,需要手动的进行 pthread_join 反而是一种负担,这个时候可以直接进行线程分离,也就是告诉系统,当线程退出时,自动释放线程资源。
需要注意的是,除了线程组内其他线程对目标线程进行分离外,也可以自己对自己进行线程分离(我抽我自己,没毛病吧)。还有就是线程一旦分离就不能再进行 pthread_join 了,一个线程不能既是 joinable 又是分离的。
进行线程分离可以使用 pthread_detach 函数,该函数是 POSIX 线程库中的一个函数,用于将线程设置为分离状态。函数原型如下:
thread参数为要设置为分离状态的线程的线程标识符(线程ID)。如果分离成功,函数返回0。如果失败,函数返回一个非零错误码,表示设置线程分离状态时发生了错误。如下代码:
cpp
void *thread_function(void *arg)
{
pthread_detach(pthread_self());
for (int i = 0; i < 5; ++i)
{
cout << "Thread is running... " << i << endl;
sleep(1);
}
cout << "Thread execution completed." << endl;
pthread_exit(NULL);
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, thread_function, NULL);
// pthread_detach(tid); 也可以在这里分离
sleep(2);
if (pthread_join(tid, nullptr) == 0)
cout << "wait success" << endl;
else
cout << "wait fail" << endl;
cout << "Main thread exiting." << endl;
return 0;
}
代码中,在主线程创建tid线程后,休眠2秒确保tis线程先进行分离,然后主线程对tid线程进行等待,由于线程在分离后是不能够进行等待的,所以主线程会打印 wait fail 然后结束线程,主线程结束tid线程也会跟着结束,结果如下。
总结
文章介绍了多线程的概念以及优缺点,并对线程和进程进行比较分析,最后介绍了Linux中线程控制的一些函数方法,如线程的创建,线程的终止,线程的分离等等,总的来说多线程是一种强大的编程技术,适用于需要同时处理多个任务或实现高并发性能的应用程序。然而,它需要谨慎的设计和管理,以确保线程安全性和避免常见的多线程问题。
码文不易,如果客官觉得文章对你有所帮助的话,就点一个小小的👍吧!