Linux多线程编程基础
本文将对Linux系统中多线程编程进行介绍。
线程的概念
线程是进程内的一个独立执行单元,共享进程的地址空间、文件描述符、信号处理方式等资源,但拥有独立的程序计数器(PC)、栈空间和寄存器集合。一个进程可以包含多个线程,这些线程并发执行,共同完成进程的任务。
线程的核心优势在于:
- 轻量级:创建和销毁线程的开销远小于进程(无需复制地址空间)。
- 高效通信:线程间共享内存,通信无需通过内核(如管道、Socket)。
- 资源利用率:多线程可充分利用多核 CPU,提升程序吞吐量。
线程模型
目前,Linux上使用的线程模型是NPTL(Native POSIX Thread Library)。NPTL采用了一对一的线程模型,符合了POSIX多线程标准,在稳定性和性能方面都有了很大的提升。NPTL在Linux 2.6版本中被引入。
$ getconf GNU_LIBPTHREAD_VERSION
NPTL 2.43
NPTL的解决方法与LinuxThread类似,内核看到的首要抽象依旧是一个进程,新线程是通过引入Clone()系统调用产生的,clone是Linux独有的,任何其他UNIX系统的版本中都没有CLone函数。clone()的引入了模糊了线程与进程的区别。
线程状态
Linux 线程的状态与进程类似,由内核 task_struct 中的 state 字段定义,主要状态包括:
- 就绪(TASK_RUNNING):线程已准备好执行,等待 CPU 调度。
- 运行(TASK_RUNNING):线程正在 CPU 上执行(就绪和运行在 Linux 内核中统一为 TASK_RUNNING)。
- 阻塞(TASK_INTERRUPTIBLE/TASK_UNINTERRUPTIBLE):线程等待某个事件(如 I/O、锁释放),主动放弃 CPU。
- 可中断阻塞(TASK_INTERRUPTIBLE)可被信号唤醒,不可中断阻塞(TASK_UNINTERRUPTIBLE)不行。
- 终止(EXIT_ZOMBIE):线程执行结束,但资源未被回收(需父线程通过 pthread_join 回收)。
- 停止(TASK_STOPPED):线程被信号暂停(如 SIGSTOP),可通过 SIGCONT 恢复。
线程状态转换的核心流程:
- 创建:通过 pthread_create 创建,初始状态为就绪。
- 调度:内核调度器将就绪线程切换至运行状态。
- 阻塞:线程调用 pthread_mutex_lock(未获取锁)、pthread_cond_wait 等,进入阻塞状态。
- 唤醒:阻塞条件满足(如锁释放、条件变量触发),线程回到就绪状态。
- 终止:线程执行完 start_routine 或调用 pthread_exit,进入终止状态,等待父线程回收。
线程调度
Linux的调度是基于内核线程的。Linux目前采用的调度器为CFS(Complete Fair Scheduler),最新的内核(6.6+)开始默认引入 EEVDF(Earliest Eligible Virtual Deadline First)。
pthread接口
具体定义和声明参考:/usr/include/pthread.h
类型
typedef unsigned long int pthread_t;
typedef union
{
char __size[__SIZEOF_PTHREAD_MUTEXATTR_T];
int __align;
} pthread_mutexattr_t;
typedef union
{
char __size[__SIZEOF_PTHREAD_CONDATTR_T];
int __align;
} pthread_condattr_t;
typedef unsigned int pthread_key_t;
typedef int __ONCE_ALIGNMENT pthread_once_t;
union pthread_attr_t
{
char __size[__SIZEOF_PTHREAD_ATTR_T];
long int __align;
};
#ifndef __have_pthread_attr_t
typedef union pthread_attr_t pthread_attr_t;
# define __have_pthread_attr_t 1
#endif
typedef union
{
struct __pthread_mutex_s __data;
char __size[__SIZEOF_PTHREAD_MUTEX_T];
long int __align;
} pthread_mutex_t;
typedef union
{
struct __pthread_cond_s __data;
char __size[__SIZEOF_PTHREAD_COND_T];
__extension__ long long int __align;
} pthread_cond_t;
接口
- 创建线程
pthread_create
#include <pthread.h>
int pthread_create(
pthread_t *thread, // 输出参数:线程 ID
const pthread_attr_t *attr, // 线程属性(NULL 表示默认)
void *(*start_routine)(void*), // 线程入口函数
void *arg // 传递给入口函数的参数
);
// 返回值:0 成功,非 0 错误码(如 EAGAIN:资源不足
- 退出线程
pthread_exit
void pthread_exit(void *retval); // retval:线程退出状态,可被 pthread_join 获取
- 取消线程
int pthread_cancel(pthread_t thread);
- 等待线程
pthread_join
int pthread_join(
pthread_t thread, // 目标线程 ID
void **retval // 输出参数:线程退出状态(通过 pthread_exit 返回)
);
- 分离线程
pthread_detach
int pthread_detach(pthread_t thread);
-
操作线程属性
pthread_attr_* -
线程调度控制
#include <pthread.h>
int pthread_setschedparam(pthread_t thread, int policy,
const struct sched_param *param);
int pthread_getschedparam(pthread_t thread, int *restrict policy,
struct sched_param *restrict param);
int pthread_setschedprio(pthread_t thread, int prio);
- 设置亲和性
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <pthread.h>
int pthread_setaffinity_np(pthread_t thread, size_t cpusetsize,
const cpu_set_t *cpuset);
int pthread_getaffinity_np(pthread_t thread, size_t cpusetsize,
cpu_set_t *cpuset);
- mutex 操作
#include <pthread.h>
pthread_mutex_t fastmutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t recmutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;
pthread_mutex_t errchkmutex = PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP;
int pthread_mutex_init(pthread_mutex_t *mutex,
const pthread_mutexattr_t *mutexattr);
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);
- 条件变量操作
#include <pthread.h>
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int pthread_cond_init(pthread_cond_t *cond,
pthread_condattr_t *cond_attr);
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
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 *abstime);
int pthread_cond_destroy(pthread_cond_t *cond);
- 屏障
#include <pthread.h>
int pthread_barrier_destroy(pthread_barrier_t *barrier);
int pthread_barrier_init(pthread_barrier_t *restrict barrier,
const pthread_barrierattr_t *restrict attr, unsigned count);
线程的创建
#include <pthread.h>
int pthread_create(pthread_t *restrict thread,
const pthread_attr_t *restrict attr,
void *(*start_routine)(void *),
void *restrict arg);
线程的死亡
线程正常终止
线程的销毁有几种
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
int pthread_detach(pthread_t thread);
void pthread_exit(void *retval);
int pthread_cancel(pthread_t thread);
#include <signal.h>
int pthread_kill(pthread_t thread, int sig);
int sigwait(const sigset_t *restrict set, int *restrict sig);
int sigaddset(sigset_t *set, int signum);
线程正常终止的方法:
1、return从线程函数返回。
2、通过调用函数pthread_exit使线程退出
3、线程可以被同一进程中的其他线程取消(pthread_cancel)
exit/pthread_exit
1、在主线程中,在main函数中return了或是调用了exit函数,则主线程退出,且整个进程也会终止,此时进程中的所有线程也将终止。因此要避免main函数过早结束。
线程不像进程,一个进程中的线程之间是没有父子之分的,都是平级关系。即线程都是一样的, 退出了一个不会影响另外一个。
但是所谓的"主线程"main,其入口代码是类似这样的方式调用main的:exit(main(...))。main执行完之后, 会调用exit()。exit() 会让整个进程终止,那所有线程自然都会退出。
2、在主线程中调用pthread_exit, 只会使主函数所在的线程(可以说是进程的主线程)退出,进程不会结束,进程内的其他线程也不会结束,直到所有线程结束,进程才会终止。而如果是return,编译器将使其调用进程退出的代码(如_exit()),从而导致进程及其所有线程结束运行。
理论上说,pthread_exit()和线程宿体函数退出的功能是相同的,函数结束时会在内部自动调用pthread_exit()来清理线程相关的资源。但实际上二者由于编译器的处理有很大的不同。
按照POSIX标准定义,当主线程在子线程终止之前调用pthread_exit()时,子线程是不会退出的。
When you program with POSIX Threads API, there is one thing about pthread_exit() that you may ignore for mistake. In subroutines that complete normally, there is nothing special you have to do unless you want to pass a return code back using pthread_exit(). The completion won't affect the other threads which were created by the main thread of this subroutine. However, in main(), when the code has been executed to the end, there could leave a choice for you. If you want to kill all the threads that main() created before, you can dispense with calling any functions. But if you want to keep the process and all the other threads except for the main thread alive after the exit of main(), then you can call pthread_exit() to realize it. And any files opened inside the main thread will remain open after its termination.
main()中调用了pthread_exit后,导致住线程提前退出,其后的exit()无法执行了,所以要到其他线程全部执行完了,整个进程才会退出。
3、在任何一个线程中调用exit函数都会导致进程结束。进程一旦结束,那么进程中的所有线程都将结束。
pthread_join
线程终止最重要的问题是资源释放的问题。
线程终止时需要注意线程同步的问题。一般情况下,进程中各个线程的运行是相互独立的,线程的终止不会相互通知,也不会影响其他线程,
终止的线程所占用的资源不会随着线程的结束而归还系统,而是仍为线程所在的进程持有。在Linux中,默认情况下是在一个线程被创建后,
必须使用此函数对创建的线程进行资源回收,但是可以设置Threads attributes来设置当一个线程结束时,直接回收此线程所占用的系统资源,详细资料查看Threads attributes。
函数pthread_join用来等待一个线程的结束,pthread_join的调用者将被挂起并等待thread线程终止。需要注意的是一个线程仅允许一个线程使用pthread_join
等待它结束,并且被等待的线程应该处于可join状态。即非DETACHED状态。DETACHED是指某个线程执行pthread_detach后所处的状态。处于DETACHED状态的线程无法由pthread_join同步。
一个可pthread_join的线程所占用的资源仅当有线程对其执行了pthread_join后才会释放,因此为了防止内存泄漏,所有线程终止时,要么已经被设置为DETACHED状态,要么使用pthread_join来回收资源。
一个线程不能被多个线程等待。否则第一个收到信号的线程成功返回。其余调用pthread_join的线程返回错误码 ESRCH No thread with the ID thread could be found.
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
void* thread_func(void* arg)
{
printf("thread:%lu is running\n", pthread_self());
int rv = 44;
pthread_exit((void*)rv);
}
int main()
{
pthread_t thid;
int rv;
pthread_create(&thid, NULL, thread_func, NULL);
printf("main thread begin join\n");
pthread_join(thid, (void*)&rv);
printf("main thread end join\n");
printf("the thread:%lu exit:%d\n", thid, rv);
return 0;
}
// main thread begin join
// thread:139787486242496 is running
// main thread end join
// the thread:139784005615616 exit:44
将线程的属性称为detached,则线程退出时会自己清理资源。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
void *start_run(void *arg)
{
printf("thread start\n");
sleep(1);
printf("thread end\n");
}
int main()
{
pthread_t thread_id;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&thread_id, &attr, start_run, NULL);
pthread_attr_destroy(&attr);
sleep(5);
exit(0);
}
在线程设置为joinable后,可以调用pthread_detach()使之成为detached。但是相反的操作则不可以。还有,如果线程已经调用pthread_join()后,则再调用pthread_detach()则不会有任何效果。
线程可以通过自身执行结束来结束,也可以通过调用pthread_exit()来结束线程的执行。另外,线程甲可以被线程乙被动结束。这个通过调用pthread_cancel()来达到目的。
pthread_cancel
当然,线程也不是被动的被别人结束。它可以通过设置自身的属性来决定如何结束,线程的被动结束分为两种,一种是异步终结,另外一种是同步终结。异步终结就是当其他线程调用pthread_cancel的时候,线程就立刻被结束。而同步终结则不会立刻终结,它会继续运行,直到到达下一个结束点(cancellation point)。当一个线程被按照默认的创建方式创建,那么它的属性是同步终结。
1 线程取消的定义
一般情况下,线程在其主体函数退出的时候会自动终止,但同时也可以因为接收到另一个线程发来的终止(取消)请求而强制终止。
2 线程取消的语义
线程取消的方法是向目标线程发Cancel信号,但如何处理Cancel信号则由目标线程自己决定,或者忽略(当禁止取消时)、或者立即终止(当在取消点 或异步模式下)、或者继续运行至Cancelation-point(取消点,下面将描述),总之由不同的Cancelation状态决定。
线程接收到CANCEL信号的缺省处理(即pthread_create()创建线程的缺省状态)是继续运行至取消点再处理(退出),或在异步方式下直接 退出。一个线程处理cancel请求的退出操作相当于pthread_exit(PTHREAD_CANCELED)。当然线程可以通过设置为 PTHREAD_CANCEL_DISABLE来拒绝处理cancel请求,稍后会提及。
线程的取消与线程的工作方式(joinable或detached)无关。
3 取消点
根据POSIX标准,pthread_join()、 pthread_testcancel()、pthread_cond_wait()、 pthread_cond_timedwait()、sem_wait()、sigwait() 等函数以及read()、write()等会引起阻塞的系统调用都是Cancelation-point,而其他pthread函数都不会引起Cancelation动作。但是pthread_cancel的手册页声称,由于LinuxThread库与C库结合得不好,因而目前C库函数都不是Cancelation-point;但CANCEL信号会使线程从阻塞的系统调用中退出,并置EINTR错误码,因此可以在需要作为Cancelation-point的系统调用前后调用pthread_testcancel(),从而达到POSIX标准所要求的目标,即如下代码段:
pthread_testcancel();
retcode = read(fd, buffer,length);
pthread_testcancel();
使用前 须判断线程ID的有效性!即判断并保证:thrd != 0 否则有可能会出现“段错误”的异常!
4 程序设计方面的考虑
如果线程处于无限循环中,且循环体内没有执行至取消点的必然路径,则线程无法由外部其他线程的取消请求而终止。因此在这样的循环体的必经路径上应该加入pthread_testcancel()调用。
当pthread_cancel()返回时,线程未必已经取消,可能仅仅将请求发送给目标线程,而目标线程目前没有到达取消点,如果要知道线程在何时中止,就需要在取消它之后调用pthread_join()。有一个例外是当线程被detach后,不能这样处理:a) 当join一个已经detached的线程时,返回EINVAL;b) 如果join后该线程设置为detached,则detach将不起作用。因此,如果知道一个线程可能会以分离方式运行,就不需要在pthread_cancel()后调用pthread_join()。
5 与线程取消相关的pthread函数
int pthread_cancel(pthread_t thread) 发送终止信号给thread线程,如果成功则返回0,否则为非0值。发送成功并不意味着thread会终止。
int pthread_setcancelstate(int state, int *oldstate) 设 置本线程对Cancel信号的反应,state有两种值:PTHREAD_CANCEL_ENABLE(缺省)和 PTHREAD_CANCEL_DISABLE,分别表示收到信号后设为CANCLED状态和忽略CANCEL信号继续运行;old_state如果不为 NULL则存入原来的Cancel状态以便恢复。
int pthread_setcanceltype(int type, int *oldtype) 设 置本线程取消动作的执行时机,type有两种取值:PTHREAD_CANCEL_DEFFERED 和 PTHREAD_CANCEL_ASYCHRONOUS,仅当Cancel状态为Enable时有效,分别表示收到信号后继续运行至下一个取消点再退出和 立即执行取消动作(退出);oldtype如果不为NULL则存入运来的取消动作类型值。
void pthread_testcancel(void) 检查本线程是否处于Canceld状态,如果是,则进行取消动作,否则直接返回。
6 检测一个线程是否还活着的pthread函数
int pthread_kill(pthread_t thread, int sig) 向指定ID的线程发送sig信号,如果线程的代码内不做任何信号处理,则会按照信号默认的行为影响整个进程。也就是说,如果你给一个线程发送了SIGQUIT,但线程却没有实现signal处理函数,则整个进程退出。pthread_kill(threadid, SIGKILL)也一样,他会杀死整个进程。如果要获得正确的行为,就需要在线程内实现signal(SIGKILL,sig_handler)。所以,如果int sig的参数不是0,那一定要清楚到底要干什么,而且一定要实现线程的信号处理函数,否则,就会影响整个进程。那么,如果int sig的参数是0呢,这是一个保留信号,一个作用就是用来判断线程是不是还活着。
我们来看一下pthread_kill的返回值:
线程仍然活着:0
线程已不存在:ESRCH
信号不合法:EINVAL
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <pthread.h>
#include <time.h>
pthread_t tid;
sigset_t set;
void myfunc()
{
printf("hello\n:");
}
void *mythread(void *p)
{
int signum;
while (1) {
sigwait(&set, &signum);
if (SIGUSR1 == signum) {
myfunc();
} else if (SIGUSR2 == signum) {
printf("I will sleep 2 seconds and exit\n");
sleep(2);
break;
}
}
}
int main()
{
char tmp;
void *status;
sigemptyset(&set);
sigaddset(&set, SIGUSR1);
sigaddset(&set, SIGUSR2);
sigprocmask(SIG_SETMASK, &set, NULL);
pthread_create(&tid, NULL, mythread, NULL);
printf(":");
while (1) {
char *p = NULL;
char str[255];
scanf("%c", &tmp);
// Use fgets instead of gets for safety and compatibility
p = fgets(str, sizeof(str), stdin);
//printf("get %c\n", tmp);
if ('a' == tmp) {
pthread_kill(tid, SIGUSR1); //发送SIGUSR1,打印字符串。
sleep(1);
} else if ('q' == tmp) {
//发出SIGUSR2信号,让线程退出,如果发送SIGKILL,线程将直接退出。
pthread_kill(tid, SIGUSR2);
//等待线程tid执行完毕,这里阻塞。
pthread_join(tid, &status);
printf("finish\n");
break;
} else {
printf(":");
}
}
return 0;
}
线程间通信
线程间通信,最主要的目的就是为了实现多线程之间的同步(和互斥)。
-
互斥: 指在某一时刻指允许一个进程运行其中的程序片,具有排他性和唯一性。
对于线程A和线程B来讲,在同一时刻,只允许一个线程对临界资源进行操作,即当A进入临界区对资源操作时,B就必须等待;当A执行完,退出临界区后,B才能对临界资源进行操作。 -
同步: 指的是在互斥的基础上,实现进程之间的有序访问。假设现有线程A和线程B,线程A需要往缓冲区写数据,线程B需要从缓冲区读数据,但他们之间存在一种制约关系,即当线程A写的时候,B不能来拿数据;B在拿数据的时候A不能往缓冲区写,也就是说,只有当A写完数据(或B取走数据),B才能来读数据(或A才能往里写数据)。这种关系就是一种线程的同步关系。
-
临界资源:能够被多个线程共享的数据/资源。
-
临界区:对临界资源进行操作的那一段代码
多线程编程中,难免会遇到多个线程同时访问临界资源的问题,如果不对其加以保护,那么结果肯定是不如预期的。
static int g_val=0;
void* pthread_mem(void* arg)
{
int i=0;
int val=0;
while(i<500000)
{
val = g_val;
i++;
g_val=val+1;
}
return NULL;
}
int main()
{
pthread_t tid1;
pthread_t tid2;
pthread_create(&tid1, NULL, pthread_mem, NULL);
pthread_create(&tid2, NULL, pthread_mem, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
printf("g_val end is :%d\n",g_val);
return 0;
}
// g_val end is :543312
线程1和线程2都需要对g_val进行+1操作,循环500000次,由于每次对g_val进行+1操作时并不是一部完成的(原子操作),在一个线程执行过程中随时都有可能被切出去使另一个线程来操作,假设线程1正在执行时被切出去,此时它已经将g_val累加到3000,而线程2切进来的时候并不知情,可能会将g_val从0开始累加。我们期望的是线程1和线程2能将g_val累加到100 0000,但实际结果确实这样的:
./c_thread
g_val end is :543312
./c_thread
g_val end is :658373
./c_thread
g_val end is :506844
./c_thread
g_val end is :500000
互斥锁(Mutex)
1、互斥锁的本质: 互斥锁实际上是一种变量,在使用互斥锁时,实际上是对这个变量进行置0置1操作并进行判断使得线程能够获得锁或释放锁。
2、作用:互斥锁的作用是对临界区加以保护,以使任意时刻只有一个线程能够执行临界区的代码。实现了多线程之间的互斥。
3、接口:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
- pthread_mutex_lock:如果此时已经有另一个线程已经获得了锁,那么当前线程调用该函数后就会被挂起等待,直到有另一个线程释放了锁,该线程会被唤醒。
- pthread_mutex_trylock:如果此时有另一个贤臣已经获得了锁,那么当前线程调用该函数后会立即返回并返回设置出错码为EBUSY,即它不会使当前线程挂起等待。
通过添加互斥锁,修复上面的程序
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <pthread.h>
#include <time.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
static int g_val = 0;
void *pthread_mem(void *arg)
{
int i = 0;
int val = 0;
while (i < 500000) {
pthread_mutex_lock(&mutex);
val = g_val;
i++;
g_val = val + 1;
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main()
{
pthread_t tid1;
pthread_t tid2;
pthread_create(&tid1, NULL, pthread_mem, NULL);
pthread_create(&tid2, NULL, pthread_mem, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
printf("g_val end is :%d\n", g_val);
return 0;
}
// g_val end is :1000000
这段代码通过对临界区进行加锁与解锁,每个线程在进入临界区的时候获得一把锁,操作执行完成后释放锁资源,使得其他等待的线程能够抱锁进入,这样就确保了每个线程进来执行的操作都是原子的,这样使得最后的结果为1000000.
互斥锁的底层实现
可以参考这里:https://www.zhihu.com/question/332113890
使用互斥锁引入的问题
使用互斥锁可能会导致死锁问题。
-
死锁:
指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待状态。通俗一点来讲,假设A线程持有锁a,B线程持有锁b,而线程访问临界区的条件时同时具有锁a和锁b,那么A就会等待B释放锁b,B会等待A释放锁a,如果没有一种措施,他两会一直等待,这样就产生了死锁。 -
死锁产生的情况
1、系统资源不足:如果系统资源足够,每个申请锁的线程都能后获得锁,那么产生死锁的情况就会大大降低;
2、申请锁的顺序不当:当两个线程按照不同的顺序申请、释放锁资源时也会产生死锁。( 自行执行以下代码观察现象。)
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int a = 0;
int b = 0;
pthread_mutex_t mutex_a;
pthread_mutex_t mutex_b;
void *another(void *arg)
{
pthread_mutex_lock(&mutex_b);
printf("new_thread,got mutex_b,waiting for mutex_a\n");
sleep(5);
++b;
pthread_mutex_lock(&mutex_a);
b += a++;
pthread_mutex_unlock(&mutex_a);
pthread_mutex_unlock(&mutex_b);
pthread_exit(NULL);
}
int main()
{
pthread_t id;
pthread_mutex_init(&mutex_a, NULL);
pthread_mutex_init(&mutex_b, NULL);
pthread_create(&id, NULL, another, NULL);
pthread_mutex_lock(&mutex_a);
printf("main_thread,got mutex_a,waiting for mutex_b\n");
sleep(5);
++a;
pthread_mutex_lock(&mutex_b);
a += b++;
pthread_mutex_unlock(&mutex_b);
pthread_mutex_unlock(&mutex_a);
pthread_join(id, NULL);
pthread_mutex_destroy(&mutex_a);
pthread_mutex_destroy(&mutex_b);
return 0;
}
在上述代码中,主线程先申请了mutex_a,再申请mutex_b,而在新线程中,新线程先申请mutex_b,再申请mutex_a,这样双方各自持有一把锁,并互相等待对方的锁,就产生了死锁。执行结果如下:
./c_thread
main_thread,got mutex_a,waiting for mutex_b
new_thread,got mutex_b,waiting for mutex_a
-
死锁产生的条件
1、互斥属性:即每次只能有一个线程占用资源。
2、请求与保持:即已经申请到锁资源的线程可以继续申请。在这种情况下,一个线程也可以产生死锁情况,即抱着锁找锁。
3、不可剥夺:线程已经得到所资源,在没有自己主动释放之前,不能被强行剥夺。
4、循环等待:多个线程形成环路等待,每个线程都在等待相邻线程的锁资源。 -
死锁的避免:
1、既然死锁的产生是由于使用了锁,那么在能不使用锁的情况下就尽量不使用,如果有多种方案都能实现,那么尽量不选用带锁的这种方案
2、尽量避免同时获得多把锁,如果有必要,就要保证获得锁的顺序相同
条件变量(Condition Variable)
- 条件变量的作用:
虽然互斥锁实现了线程之间的互斥,但是互斥锁也有不足之处,它只能表示两种状态:上锁和非上锁。但是假如有线程A拿着锁进入临界区,并在临界区休眠了。而此时正在等待该锁的线程就会不断轮询,查看锁是否已经被释放。当线程A释放锁后,所有在该锁上阻塞的线程都会变成可运行状态,第一个变成可运行状态的线程会先获得锁,其他线程会继续等待直到变为可用。
引入条件变量一个就是为了避免为了查看条件是否成立而不断轮询的情况,这样也提高了效率;另一个就是为了防止竞争,条件变量用来阻塞一个线程,当条件不满足时,线程往往会解开互斥锁并等待条件发生变化,一旦有某个线程改变了条件变量,它会通知该条件变量下的一个或多个正在被该条件变量阻塞的线程,这些线程会重新上锁并检测条件是否成立。
总结:互斥锁实现的是线程之间的互斥,条件变量实现的是线程之间的同步。
对条件变量的理解:
1、条件变量与互斥锁一样,都是一种数据;
2、条件变量的作用是描述当前资源的状态,即当前资源是否就绪。
3、条件变量是在多线程程序中用来实现“等待->唤醒”逻辑的常用方法。
条件变量的接口函数:
1.初始化:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
如果ConditionVariable是静态分配的,可以用宏定义PTHEAD_COND_INITIALIZER初始化,相当于用pthread_cond_init函数初始化并且attr参数为NULL。
2.销毁:
int pthread_cond_destroy(pthread_cond_t *cond);
与mutex类似,该函数用来销毁一个条件变量。
3.等待:
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
该函数用来在一个ConditionVariable上阻塞等待,做以下三步操作:①释放Mutex;②阻塞等待;③当被唤醒时,重新获得Mutex并返回。
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
pthread_cond_timedwait()函数有一个额外的参数可以设定等待超时,如果到达了abstime所指定的时刻仍然没有别的线程来唤醒当前线程,就返回ETIMEDOUT。
4.唤醒等待线程:
int pthread_cond_signal (pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
pthread_cond_signal():用于唤醒在该条件变量下等待的一个线程,至于哪个被唤醒,取决于线程的优先级和调度策略。
pthread_cond_broadcast():用于唤醒在某个ConditionVariable 上等待的所有线程。使用这种方法时,瞬间唤醒的线程数过多,资源请求过大,容易导致系统不稳定。
条件变量的使用:
条件变量搭配互斥锁可以实现单生产者-单消费者模型.
信号量(Semaphore)
对信号量的理解:
1.信号量是一种特殊的变量,它只能取自然数并且只支持两种操作。
2.具有多个整数值的信号量称为通用信号量,只取 1 和 0 两个数值的称为二元信号量,这里我只讨论二元信号量。
3.信号量的两个操作:P(passeren,传递-进入临界区)、V(vrijgeven,释放-退出临界区)
假设现有信号量sv,
P操作:如果sv的值大于0,就将其减1;如果sv的值为0,就将当前线程挂起;
V操作:如果有其他线程因为等待sv而被挂起,则将其唤醒;如果没有,就将sv加1.
综上:可以理解信号量是一种计数器,P操作就是将计数器的值减1,执行一次P操作意味着请求一次资源,即当前可以获得资源;V操作就是将计数器的值加1,即执行一次V操作意味着释放一个单位资源。
信号量的接口函数:
1.初始化:
int sem_init(sem_t *sem, int pshared, unsigned int value);1
value参数表示可用资源的数量,pshared参数为0表示信号量用于同一进程的线程间同步。
2.销毁:
int sem_destroy(sem_t *sem);1
该函数用于释放与Semaphore相关的资源。
3.等待(P操作):
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);12
sem_wait()被用来阻塞当前线程直到信号量sem的值大于0,解除阻塞后将sem的值将减一,表明公共资源经使用后减少。
sem_trywait()是sem_wait()的非阻塞版本,它直接将信号量sem的值减一。
4.释放(V操作):
int sem_post(sem_t *sem);1
用来增加信号量的值。当有线程阻塞在这个信号量上时,调用这个函数会使信号量的值加1,从而使其中的一个线程不再阻塞,选择机制同样是由线程的调度策略决定的。
- 信号量的使用:
有了信号量之后,我们可以对生产者-消费者模型加以改进。上述Mutex+Condition Variable实现的是线程之间的加锁同步,使用信号量可以实现线程之间的无锁同步,当有资源的时候,让消费者去消费,否则就等待生产者生产资源;当资源足够的时候,生产者就等待消费者消费后再去生产。
参考
POSIX Threads Programming
Mastering Threading in Linux: A Comprehensive Guide
总结一下 linux 中的线程模型
[线程]Linux 原生 POSIX 线程库(NPTL)
Linux线程模型
NTPL

浙公网安备 33010602011771号