跳到主要内容 Linux 多线程编程:线程栈、TLS、互斥锁与条件变量详解 | 极客日志
C++
Linux 多线程编程:线程栈、TLS、互斥锁与条件变量详解 Linux 多线程编程涉及线程栈隔离、线程局部存储(TLS)、互斥锁及条件变量机制。线程栈为私有资源,全局数据共享但需保护。__thread 关键字实现线程私有全局变量。互斥锁解决临界区竞争问题,防止数据不一致。条件变量用于线程同步,避免忙等待,提高并发效率。通过抢票示例演示了竞态条件及加锁、同步解决方案。
线程栈
在 Linux 系统中,没有很明确的线程概念,只有轻量级进程的概念。操作系统提供的系统调用不能直接创建线程,只能创建轻量级进程,该接口为 clone。
fn :指定子任务的执行入口函数。新创建的任务从该函数开始运行,函数返回时子任务结束。
child_stack :指向子任务使用的栈空间。由于 Linux 栈向低地址增长,该参数通常指向栈空间的高地址。系统调用要求手动为子任务分配栈空间,风险较高。pthread 库底层封装了此接口,提供默认方法避免手动分配。
flags :用于指定子任务与父任务共享的资源类型,是 clone 的核心参数。通过不同标志位组合,可以创建进程或线程。
arg :传递给子任务入口函数的参数,用于初始化子任务的运行环境。
pthread 是用户态的第三方库,通过动态链接方式加载到进程虚拟地址空间的共享区。除了主线程外,其他线程都需要独立的栈空间,用于保存函数调用过程中的局部变量、返回值地址及临时数据。若所有线程共享同一栈空间,易造成数据覆盖。因此线程栈是线程私有资源,而代码区、数据区及堆空间是共享的。pthread 维护的线程栈通过 clone 的 child_stack 参数传递给内核,保证线程栈独立性。
示例验证
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string>
#include <vector>
struct threadData {
std::string threadname;
};
void *threadRoutine (void *args) {
threadData *td = (threadData *)args;
int count = 5 ;
while (count--) {
printf ("pid:%d, tid:%lx, threadname:%s\n" , getpid (), pthread_self (), td->threadname.c_str ());
sleep (1 );
}
return nullptr ;
}
{
std::vector< > tids;
( i = ; i < ; i++) {
tid;
threadData *td = threadData;
td->threadname = + std:: (i);
(&tid, , threadRoutine, td);
tids. (tid);
}
( i = ; i < ; i++) {
(tids[i], );
}
;
}
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online
int
main
()
pthread_t
for
int
0
5
pthread_t
new
"thread-"
to_string
pthread_create
nullptr
push_back
for
int
0
5
pthread_join
nullptr
return
0
通过上述代码创建多线程后,可验证每个线程是否有独立栈结构。在线程函数中定义局部变量 test,观察其地址和值。
void *threadRoutine (void *args) {
int test = 1 ;
threadData *td = (threadData *)args;
int count = 5 ;
while (count--) {
printf ("pid:%d, tid:%lx, threadname:%s, test:%d , &test:%p\n" , getpid (), pthread_self (), td->threadname.c_str (), test, &test);
test++;
sleep (1 );
}
return nullptr ;
}
结果显示,每个线程中的局部变量 test 都是从 1 开始,且地址不同,证明每个线程拥有独立的线程栈,互不干扰。
int value = 10 ;
void *threadRoutine (void *args) {
threadData *td = (threadData *)args;
int count = 5 ;
while (count--) {
printf ("pid:%d, tid:%lx, threadname:%s, value:%d , &value:%p\n" , getpid (), pthread_self (), td->threadname.c_str (), value, &value);
sleep (1 );
}
return nullptr ;
}
线程局部存储 (TLS) 若需定义既是全局的又独属于线程的变量,可使用 __thread 关键字(GCC 提供的线程局部存储机制)。每个线程拥有独立的变量副本。
加 __thread 后,每个线程拥有属于自己的全局变量副本,实现全局数据区的线程隔离。
线程分离 创建新线程后,主线程调用 pthread_join 会阻塞等待线程结束。若不关心退出结果,仅需线程完成任务后自动释放资源,可使用线程分离。
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string>
#include <vector>
struct threadData {
std::string threadname;
};
__thread int value = 10 ;
void *threadRoutine (void *args) {
pthread_detach (pthread_self ());
threadData *td = (threadData *)args;
int count = 5 ;
while (count--) {
printf ("pid:%d, tid:%lx, threadname:%s, value:%d , &value:%p\n" , getpid (), pthread_self (), td->threadname.c_str (), value, &value);
sleep (1 );
}
return nullptr ;
}
int main () {
std::vector<pthread_t > tids;
for (int i = 0 ; i < 3 ; i++) {
pthread_t tid;
threadData *td = new threadData;
td->threadname = "thread-" + std::to_string (i);
pthread_create (&tid, nullptr , threadRoutine, td);
tids.push_back (tid);
}
return 0 ;
}
线程分离后,主线程无需等待新线程。需注意主线程应在分离线程之后退出,否则程序终止会导致未完成的分离线程被强制回收。
线程的互斥 大部分情况下线程使用局部变量,但共享变量在多并发操作时可能引发问题。例如多线程抢票场景。
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string>
#include <vector>
int tickets = 100 ;
class threadData {
public :
threadData (int num) { threadname = "thread-" + std::to_string (num); }
public :
std::string threadname;
};
void *getTicket (void *args) {
threadData *td = (threadData *)args;
while (1 ) {
if (tickets > 0 ) {
usleep (1000 );
printf ("%s get a tickets , tickets : %d\n" , td->threadname.c_str (), tickets);
tickets--;
} else {
break ;
}
}
return nullptr ;
}
int main () {
std::vector<pthread_t > tids;
std::vector<threadData *> thread_datas;
for (int i = 1 ; i <= 3 ; i++) {
pthread_t tid;
threadData *td = new threadData (i);
thread_datas.push_back (td);
pthread_create (&tid, nullptr , getTicket, td);
tids.push_back (tid);
}
for (int i = 0 ; i < 3 ; i++) {
pthread_join (tids[i], nullptr );
}
for (auto thread : thread_datas) {
delete thread;
}
return 0 ;
}
可能出现票数变为负数或重复抢票的情况。这是因为 tickets-- 操作非原子性,包含读取、修改、写回三步。线程切换可能导致数据不一致。
互斥锁接口
pthread_mutex_init :初始化互斥锁。
pthread_mutex_lock :加锁,竞争失败则阻塞。
pthread_mutex_unlock :解锁,唤醒等待线程。
pthread_mutex_destroy :销毁互斥锁。
加锁示例 #include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string>
#include <vector>
int tickets = 100 ;
class threadData {
public :
threadData (int num, pthread_mutex_t *mutex) : mutex_ (mutex) {
threadname = "thread-" + std::to_string (num);
}
public :
std::string threadname;
pthread_mutex_t *mutex_;
};
void *getTicket (void *args) {
threadData *td = (threadData *)args;
while (1 ) {
pthread_mutex_lock (td->mutex_);
if (tickets > 0 ) {
usleep (1000 );
printf ("%s get a tickets , tickets : %d\n" , td->threadname.c_str (), tickets);
tickets--;
} else {
pthread_mutex_unlock (td->mutex_);
break ;
}
pthread_mutex_unlock (td->mutex_);
}
return nullptr ;
}
int main () {
pthread_mutex_t mutex;
pthread_mutex_init (&mutex, nullptr );
std::vector<pthread_t > tids;
std::vector<threadData *> thread_datas;
for (int i = 1 ; i <= 3 ; i++) {
pthread_t tid;
threadData *td = new threadData (i, &mutex);
thread_datas.push_back (td);
pthread_create (&tid, nullptr , getTicket, td);
tids.push_back (tid);
}
for (int i = 0 ; i < 3 ; i++) {
pthread_join (tids[i], nullptr );
}
for (auto thread : thread_datas) {
delete thread;
}
pthread_mutex_destroy (&mutex);
return 0 ;
}
加锁后解决了数据一致性问题,但可能导致饥饿现象(某线程持续获取锁)。这类似于多人争用单一电脑的场景,持有者释放锁后若立即再次获取,其他等待者将无法获得资源。
锁的原理 CPU 指令执行具有原子性。锁的实现常利用 CPU 提供的 swap 或 exchange 指令保证原子交换。
寄存器初始化为 0。
寄存器值与内存锁值原子交换。
判断寄存器值:大于 0 表示获取锁成功;否则挂起等待。
无论线程如何切换,原子交换确保同一时刻仅一个线程能获取锁。
线程的同步 线程同步要求在数据安全基础上保证访问顺序。条件变量可维护等待队列,避免忙等待。
当线程申请已被占用的锁时,调用条件变量等待接口进入挂起状态。锁释放后通知条件变量,唤醒一个等待线程重新获取锁。
条件变量接口
pthread_cond_init :初始化条件变量。
pthread_cond_wait :进入等待队列,挂起并释放互斥锁;唤醒后重新获取锁。
pthread_cond_signal :唤醒一个等待线程。
pthread_cond_broadcast :唤醒所有等待线程。
pthread_cond_destroy :销毁条件变量。
同步示例 #include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string>
#include <vector>
int tickets = 100 ;
class threadData {
public :
threadData (int num, pthread_mutex_t *mutex, pthread_cond_t *cond) : mutex_ (mutex), cond_ (cond) {
threadname = "thread-" + std::to_string (num);
}
public :
std::string threadname;
pthread_mutex_t *mutex_;
pthread_cond_t *cond_;
};
void *getTicket (void *args) {
pthread_detach (pthread_self ());
threadData *td = (threadData *)args;
while (1 ) {
pthread_mutex_lock (td->mutex_);
pthread_cond_wait (td->cond_, td->mutex_);
if (tickets > 0 ) {
usleep (1000 );
printf ("%s get a tickets , tickets : %d\n" , td->threadname.c_str (), tickets);
tickets--;
} else {
pthread_mutex_unlock (td->mutex_);
break ;
}
pthread_mutex_unlock (td->mutex_);
}
return nullptr ;
}
int main () {
pthread_mutex_t mutex;
pthread_cond_t cond;
pthread_mutex_init (&mutex, nullptr );
pthread_cond_init (&cond, nullptr );
std::vector<pthread_t > tids;
std::vector<threadData *> thread_datas;
for (int i = 1 ; i <= 3 ; i++) {
pthread_t tid;
threadData *td = new threadData (i, &mutex, &cond);
thread_datas.push_back (td);
pthread_create (&tid, nullptr , getTicket, td);
tids.push_back (tid);
}
while (tickets > 0 ) {
pthread_cond_signal (&cond);
sleep (1 );
}
for (auto thread : thread_datas) {
delete thread;
}
pthread_mutex_destroy (&mutex);
pthread_cond_destroy (&cond);
return 0 ;
}
通过条件变量,所有线程均有机会抢到票,避免了单线程独占资源。注意条件变量的正确用法需结合生产者 - 消费者模型进一步探讨。