恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Linux——线程的创建与同步
首页
资讯中心
/
Linux——线程的创建与同步
Linux——线程的创建与同步
发布时间:2026/10/12 4:39:00
一、线程的概念与实现方式一线程的概念线程是进程内部的一条执行序列或执行路径是cpu调度的基本单元一个进程可以包含多条线程进程是系统中进行分配的基本单元二线程的特性线程间并发执行具体调度根据操作系统不同决定资源共享线程共享进程的资源全局变量堆内存文件描述符独立的栈空间局部变量存储在各自的栈空间中三线程的实现方式在操作系统中线程的实现有三种方式内核级线程用户及线程组合级线程Linux 中线程的实现Linux 实现线程的机制非常独特。从内核的角度来说它并没有线程这个概念Linux 把所有的线程都当作进程来实现。内核并没有准备特别的调度算法或是定义特别的数据结构来表示线程相反线程仅仅被视为一个与其他进程共享某些资源的进程每个线程都拥有唯一隶属于自己的 task_struct所以在内核中它看起来就像是一个普通的进程只是线程和其他一些进程共享某些资源如地址空间。linux 下的线程的本质就是通过 pcb-task_struct 结构体实现的而这个pcb相较于传统操作系统设计的pcb更加轻量化所以 linux 下的线程也被称作轻量级进程四进程与线程的区别进程详见进程线程进程是系统进程资源分配的基本单元线程是系统中进程CPU调度的基本单元进程有自己的独立地址空间线程共享进程中的地址空间进程的创建消耗资源大线程创建和销毁成本低进程的切换开销大同一进程内线程调度成本更低进程的稳定性更高线程间通信更加方便执行力度更加细致二、线程使用一线程库中的接口介绍1.创建线程int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg); /* thread: 接收创建的线程的ID attr: 指定线程的属性默认NULL start_routinue: 指定线程函数 arg: 给线程函数传递的参数 返回值: 成功返回0失败返回错误码 */2.退出线程终止当前线程并返回结果int pthread_exit(void *retval); /* retval: 指定退出信息 如果不需要返回值填NULL 如果需要返回值则必须指向全局变量或动态分配的内存不能是栈区内存因为线程终止后栈区内存会被释放 */3.等待线程等待某个线程退出并回收线程资源线程未退出时该方法阻塞int pthread_join(pthread_t thread, void **retval); /* thread: 等待的线程ID retval: 接收线程返回值的指针 */4.线程分离设置线程的分离属性从 joinable 设置为 detach 状态线程一旦被分离线程退出后就会自动释放所有资源不需要被等待也不能被等待应用场景当我们不关心一个线程的返回值而且也并不希望阻塞在等待上则使用线程分离int pthread_detach(pthread_t thread); /* thread: 等待的线程ID */二多线程代码#include stdio.h #include stdlib.h #include string.h #include assert.h #include unistd.h #include pthread.h // 子线程入口函数 void *pthread_fun(void *arg) { int i 0; // 循环5次模拟子线程任务 for (; i 5; i) { sleep(1); // 休眠1秒 printf(fun thread running\n); } // 子线程退出返回字符串常量的地址作为返回值 pthread_exit(fun over); } int main() { pthread_t tid; // 定义线程ID变量用于存放创建出的子线程id // 创建子线程 int res pthread_create(tid, NULL, pthread_fun, NULL); assert(res 0); // 断言创建失败程序直接终止 // 主线程循环5次模拟主线程任务 int i 0; for (; i 5; i) { sleep(1); printf(main thread running\n); } char *s NULL; // 阻塞等待子线程tid执行完毕获取子线程的返回值存入s pthread_join(tid, (void **)s); printf(s %s\n, s); // 打印子线程通过pthread_exit返回的内容 exit(0); }三线程并发运行#include stdio.h #include stdlib.h #include assert.h #include unistd.h #include string.h #include pthread.h // 线程入口函数 void *thread_fun(void *arg) { // 把void*类型的参数强转为int*解引用拿到index int index *(int *)arg; int i 0; // 循环5次打印当前线程的index for (; i 5; i) { printf(index%d\n, index); sleep(1); } } int main() { pthread_t id[5]; // 定义线程id数组存放5个线程的ID // 循环创建5个子线程 int i 0; for (; i 5; i) { // 重点这里传入i所有线程拿到的是同一个变量i的地址 // 主线程循环不断修改i线程读取时i的值已经发生变化会出现结果错乱 pthread_create(id[i], NULL, thread_fun, (void *)i); } // 循环等待5个子线程全部执行完毕 for (i 0; i 5; i) { pthread_join(id[i], NULL); } exit(0); }三、线程安全一相关概念线程安全是指一个函数、类或数据结构在多线程环境中能够正确、一致地共工作不会因为多个线程的并发访问而导致数据损坏、程序错误或异常行为临界资源是指那些能够被多个线程同时访问的共享资源临界区是指访问共享资源的那段代码在这段代码中可能会发生竞态条件竞态条件当多个线程/进程并发访问共享资源且最终结果依赖线程执行的相对时序时就发生了竞态条件原子性一次性完成中间不会被打断的操作这种操作具备原子性函数的重入多个线程在并发执行中同时进入同一个函数内部进行数据处理也就是说多线程同时调用同一个函数函数的可重入多线程重入函数后函数的执行结果并不会出现二义性不会出现问题这种函数叫做可重入函数。函数的不可重入多线程重入函数后函数的执行结果并不确定这种函数叫做不可重入函数。函数可重入与不可重入的本质原因函数内部是否依赖于临界资源可重入函数是指执行过程中可以被中断例如信号稍后能安全地重新进入的函数可重入函数必然是线程安全的但线程安全的函数不一定时可重入的关键点在于是否可在中断后重入线程安全可能是加锁实现的但是一旦被信号中断就有可能导致死锁要保证线程安全需要做到1对线程同步保证同一时刻只有一个线程访问临界资源2在多线程中使用线程安全的函数可重入函数二线程同步线程同步指的是当一个线程在对某个临界资源进行操作时其他线程都不可以对这个资源进行操作直到该线程完成操作其他线程才能操作也就是协同步调让线程按预定的先后次序进行运行。线程同步的方法有四种互斥锁、信号量、条件变量、读写锁。1.互斥锁1本质只有0和1的计数器阻塞队列2作用用于保证临界资源的访问过程是安全的通过0/1这个计数器对当前锁的状态进行标记已加锁/未加锁访问资源之前进行加锁未加锁状态直接加锁已加锁状态则阻塞访问资源完毕后进行解锁3使用定义互斥锁// 定义锁变量 pthread_mutex_t mutex;初始化// 方法一定义变量时直接初始化 pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER// 方法二通过函数进行初始化 pthread_mutex_init(pthread_ mutex_t *mutex, pthread_mutexattr_t *attr) /* mutex: 锁变量指针 attr: 属性通常置空 */加锁阻塞加锁直到加锁成功后返回int pthread_mutex_lock(pthread_ mutex_t *mutex); // 阻塞加锁 /* 该加锁的阻塞有可能会被信号打断 如果在等待过程中遇到信号中断则会先去处理中断 处理完毕后返回函数不会自动继续等锁而是直接返回错误码 但此时并未加锁成功 */ int pthread_mutex_trylock(pthread_ mutex_t *mutex); // 非阻塞加锁阻塞调用一个函数内部要完成一个功能但是当下无法立即完成则一直等待直到功能完成后函数返回非阻塞调用一个函数内部要完成一个功能但是当下无法立即完成则函数立即报错返回解锁int pthread_mutex_unlock(pthread_ mutex_t *mutex);释放锁int pthread_mutex_destroy(pthread_ mutex_t *mutex);2.信号量1本质一个计数器阻塞队列信号量 ≠ 信号信号是系统中的软件中断信号的使用详见Linux——信号的使用-CSDN博客https://blog.csdn.net/wy_05136/article/details/160348069?spm1001.2014.3001.55012作用实现同步与互斥通过计数器对资源进行计数用于同步控制线程当前是否可以获取资源信号量实现同步计数器表示的是资源的数量根据当前资源数量初始化信号量计数器初值线程A获取资源之前进行P操作线程B生产资源后进行V操作信号量实现互斥计数器表示的是资源的状态让信号的计数器永远保证只在0/1之间切换将信号量初值设置为1线程访问资源之前加锁进行P操作访问资源完毕后解锁进行V操作3操作P操作获取资源前对计数器 -1若大于等于0则可以访问资源否则就阻塞V操作生产资源后对计数器1并唤醒至少一个阻塞的线程4使用初始化int sem_init(sem_t *sem, int pshared, unsigned int value); /* sem表示指向sem_t类型的指针 pshared决定信号量的共享范围 0表示仅在当前进程内的所有线程共享 非0值在多个进程内共享需要将该信号量放在共享内存中 value信号量计数器的初值 用于实现互斥初始化为1 用于实现同步初始化为实际的资源数量 */获取资源P操作原子操作将信号量值减1获取资源如果信号量值为0该操作会堵塞此时将线程放入等待队列int sem_wait(sem_t *sem); // 计数器-1若小于0则阻塞 int sem_trywait(sem_t *sem); // 非阻塞操作计数器大于0则减1正确返回否则没有资源报错返回 int sem_timedwait(sem_t *sem, const struct timespec *abc_timeout); //限时阻塞释放资源V操作原子操作将信号量值加1释放资源随机从等待队列中唤醒一条线程int sem_post(sem_t *sem);销毁销毁初始化的信号量释放所有与信号量相关的资源int sem_destroy(sem_t *sem);3.条件变量1本质阻塞队列 让进程/线程阻塞的接口、唤醒进程/线程的接口2作用让线程在获取资源的时候若不满足获取条件则调用接口让线程阻塞其他线程促使资源获取的条件满足后通过调用唤醒接口唤醒所有阻塞的线程3使用初始化int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *restrict attr); pthread_cond_t cond PTHREAD_COND_INITIALIZER;阻塞线程在不满足资源获取的情况下阻塞线程// 阻塞接口 int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); // 限时阻塞接口超时后阻塞也会被唤醒 int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *restrict abstime);唤醒线程促使资源获取条件满足后唤醒阻塞的线程pthread_cond_signal(pthread_cond_t *cond); // 至少唤醒一个阻塞的线程 pthread_cond_broadcast(pthread_cond_t *cond); // 唤醒所有阻塞的线程销毁pthread_cond_destroy(pthread_cond_t *cond);注意事项1条件变量是搭配互斥锁一起使用的条件判断的过程是一个临界资源访问的过程需要加锁保护条件变量的阻塞保证在获取资源不满足条件的时候先解锁然后再休眠2条件判断的过程是一个循环判断的过程多个线程被唤醒只有一个加锁成功其他的就会卡在加锁上因此需要在被唤醒后加锁成功后重新再次判断是否满足条件满足则处理不满足则重新陷入休眠3若同步的过程中有多种不同的角色需要陷入休眠进行阻塞需要创建多个条件变量让不同的角色等待在不同的条件变量上避免唤醒角色错误信号量与条件变量在实现同步时的差别信号量在使用的时候不需要搭配互斥锁信号量在实现同步的时候是通过自身内部的计数器对资源进行计数计数判断过程不需要程序员参与通过自身内部的计数器的值判断有没有资源可供获取sem_wait 内部就包含了计数判断不满足则阻塞条件变量在实现同步的时候条件判断过程是程序员实现的因此同步的时候程序员进行条件判断在合适的时候调用 pthread_cond_wait 进行阻塞4.读写锁1使用初始化int pthread_rwlock_init(pthread_rwlock_t *rwlock, pthread_rwlockattr_t *attr);加读锁int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);加写锁int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);解锁int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);销毁int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);2读者写者模型应用场景读共享、写互斥的场景读共享大家可以一起读写互斥当前写的时候既不能有人读也不能有人写实现使用读写锁主要在于满足读共享、写互斥需求实现原理读写锁内部有两个计数器读者计数和写者计数加读锁写者计数为0就能加加写锁保证两个计数都为0才能加三生产者与消费者模型1.关键要素生产者产生数据的线程消费者处理数据的线程线程安全的任务队列缓冲区作用1解耦合 2支持并发 3削峰填骨削峰填骨在高速生产数据的时候数据来不及处理就先在队列中缓存起来在数据量少的时候再慢慢处理生产者与生产者之间的关系互斥生产者与消费者之间的关系互斥同步消费者与消费者之间的关系互斥2.应用场景程序中既要生产数据又要对数据进行处理的场景3.原理将数据的生产与处理进行解耦实现一个线程安全的任务队列生产者生产数据后将数据抛入任务队列中消费者不断从任务队列中取出数据进行处理4.优点解耦合支持并发支持忙闲不均的削峰填骨5.实现/* 使用条件变量互斥锁模拟厨师与顾客的同步关系: 厨师: 有饭则阻塞没饭则做饭 顾客: 没饭则阻塞有饭则吃饭 */ #include stdio.h #include unistd.h #include pthread.h int bowl 0; // 0-没饭; 1-有饭 pthread_mutex_t mutex; pthread_cond_t cond_cus; // 顾客等待的大厅 pthread_cond_t cond_che; // 厨师等待的后厨 void *customer(void *arg) { while (1) { pthread_mutex_lock(mutex); while (bowl 0) { pthread_cond_wait(cond_cus, mutex); } // 能走下来代表当前bowl是1 printf(我吃了一碗饭\n); bowl 0; pthread_mutex_unlock(mutex); // 先解锁还是先唤醒都可以 pthread_cond_signal(cond_che); } return NULL; } void *chef(void *arg) { while (1) { pthread_mutex_lock(mutex); while (bowl 1) { pthread_cond_wait(cond_che, mutex); } printf(我做了一碗饭----\n); bowl 1; pthread_mutex_unlock(mutex); pthread_cond_signal(cond_cus); } return NULL; } int main() { pthread_mutex_init(mutex, NULL); pthread_cond_init(cond_che, NULL); pthread_cond_init(cond_cus, NULL); pthread_t cus[4], chef_tid[4]; int i, ret; // 创建4个顾客线程 for (i 0; i 4; i) { ret pthread_create(cus[i], NULL, customer, NULL); if (ret ! 0) { printf(创建顾客线程失败\n); return -1; } } // 创建4个厨师线程 for (i 0; i 4; i) { ret pthread_create(chef_tid[i], NULL, chef, NULL); if (ret ! 0) { printf(创建厨师线程失败\n); return -1; } } // 等待所有顾客线程 for (i 0; i 4; i) { pthread_join(cus[i], NULL); } // 等待所有厨师线程 for (i 0; i 4; i) { pthread_join(chef_tid[i], NULL); } pthread_mutex_destroy(mutex); pthread_cond_destroy(cond_che); pthread_cond_destroy(cond_cus); return 0; }四死锁1.概念多个线程之间因为锁的争抢不当导致线程间相互等待若无外力介入无法继续执行的线程2.产生死锁的产生有四个必要条件互斥条件锁只有一个线程能加不可剥夺条件线程A加的锁其他线程不能解持有并等待条件线程A加了a锁然后去请求b锁加不上b锁我也不释放a锁环路等待条件线程A加了a锁请求b锁线程B加了b锁请求a锁3.预防预防的关键就是破坏四个必要条件破坏持有并等待可以使用非阻塞加锁trylock若加锁不成功则释放已经加的锁破坏环路等待环路等待大多是因为加锁顺序不一致导致的因此保证所有线程加锁顺序一致4.避免避免有两种方法银行家算法、死锁检测算法银行家算法思想使用一个矩阵描述总共有多少资源使用一个矩阵描述给哪些客户分配了哪些资源使用一个矩阵描述哪些客户还需要哪些资源对系统运行使用两种状态进行描述安全、不安全若给一个客户分配了资源会导致系统进入不安全状态则不能分配四锁的种类了解1.乐观锁总是认为在访问临界资源的时候其他线程不会访问2.悲观锁总是认为在访问临界资源的时候有其他线程争抢因此总是在访问前加锁3.递归锁常用于递归函数中同一个线程内可以递归循环加锁4.自旋锁能加锁则加锁不能加锁则循环判断能否加锁适用场景自旋锁对cpu资源消耗非常高因此常用于对效率要求高临界区操作比较短小且时间较短可控的场景