进程是参与操作系统(OS)资源分配的最小单位。
线程,有时被称为轻量级进程(Lightweight Process),是参与CPU调度的最小单位。
POSIX线程(POSIX threads),简称Pthreads,是线程的POSIX标准。在类Unix操作系统(Unix、Linux、Mac OS X等)中,都使用Pthreads作为操作系统的线程。在Linux下实现多线程编程也是通过使用pthread库来实现。
POSIX(Portable Operating System Interface of Unix)可移植性操作系统接口,是一套标准接口,其正式称呼为IEEE 1003,国际标准名称为ISO/IEC 9945。一个POSIX兼容的操作系统编写的程序,可以在任何其他POSIX操作系统上编译执行。
下面来看下创建线程的套路:
首先,需要#include<pthread.h>这个头文件。下面来看创建线程最简单的例程。
#include<stdio.h>
#include<pthread.h>
void *fun1(void *pArg)
{
printf("This is thread 1,tid is %u\n",(unsigned int)pthread_self());
}
void *fun2(void *pArg)
{
printf("This is thread 2,tid is %u\n",(unsigned int)pthread_self());
}
int main(void)
{
pthread_t pid1 = 0,pid2 = 0;
printf("PID is %u,tid is %u\n",getpid(),(unsigned int)pthread_self());
pthread_create(&pid1,NULL,fun1,NULL);
pthread_create(&pid2,NULL,fun2,NULL);
pthread_join(pid1,NULL);
pthread_join(pid2,NULL);
printf("Main function exit!\n");
return 0;
}
在编译过程中必须使用 -l pthread参数指定使用thread库,否则会报错。
运行结果如下。
从上述例程我们可以看出,当一个进程没有创建线程时,其默认就是单线程状态运行,它也有自己的线程id。
上面用到了几个POSIX 线程的标准接口:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void*(*start_routine)(void *), void *arg);
int pthread_join(pthread_t tid, void **rval_ptr);
其中pthread_create用于创建线程。
第一个参数是用于存放线程id的地址;
第二个参数是用于设置线程属性的pthread_attr_t结构体;
第三个参数是分配给线程执行的任务,是一个返回类型为void *,接受参数类型为void *的函数指针;
第四个参数为传递给线程的参数。
pthread_join用于访问指定线程的结束信息,当目标线程未结束时会导致调用的线程阻塞,通常用它来等待目标线程的退出。
关于线程的退出,总共有3种方式:
1. 线程执行的任务函数结束,正常退出。
2. 线程被另外一个线程取消。相关接口: int pthread_cancel(pthread_t thread);
3. 线程自己主动退出。相关接口: void pthread_exit(void *retval);
在Linux内核中,任务的管理是通过task_struct结构体进行管理的。
struct task_struct {
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
void *stack;
atomic_t usage;
unsigned int flags; /* per process flags, defined below */
unsigned int ptrace;
int lock_depth; /* BKL lock depth */
……
int prio, static_prio, normal_prio;
unsigned int rt_priority;
const struct sched_class *sched_class;
struct sched_entity se;
struct sched_rt_entity rt;
……
unsigned int policy;
cpumask_t cpus_allowed;
……
struct list_head tasks;
struct plist_node pushable_tasks;
struct mm_struct *mm, *active_mm;
/* task state */
int exit_state;
int exit_code, exit_signal;
int pdeath_signal; /* The signal sent when the parent dies */
/* ??? */
unsigned int personality;
unsigned did_exec:1;
unsigned in_execve:1; /* Tell the LSMs that the process is doing an
* execve */
unsigned in_iowait:1;
/* Revert to default priority/policy when forking */
unsigned sched_reset_on_fork:1;
pid_t pid;
pid_t tgid;
struct task_struct *real_parent; /* real parent process */
struct task_struct *parent; /* recipient of SIGCHLD, wait4() reports */
/*
* children/sibling forms the list of my natural children
*/
struct list_head children; /* list of my children */
struct list_head sibling; /* linkage in my parent's children list */
struct task_struct *group_leader; /* threadgroup leader */
/*
* ptraced is the list of tasks this task is using ptrace on.
* This includes both natural children and PTRACE_ATTACH targets.
* p->ptrace_entry is p's link on the p->parent->ptraced list.
*/
struct list_head ptraced;
struct list_head ptrace_entry;
/* PID/PID hash table linkage. */
struct pid_link pids[PIDTYPE_MAX];
struct list_head thread_group;
struct completion *vfork_done; /* for vfork() */
int __user *set_child_tid; /* CLONE_CHILD_SETTID */
int __user *clear_child_tid; /* CLONE_CHILD_CLEARTID */
cputime_t utime, stime, utimescaled, stimescaled;
cputime_t gtime;
……
unsigned long nvcsw, nivcsw; /* context switch counts */
struct timespec start_time; /* monotonic time */
struct timespec real_start_time; /* boot based time */
/* mm fault and swap info: this can arguably be seen as either mm-specific or thread-specific */
unsigned long min_flt, maj_flt;
struct task_cputime cputime_expires;
struct list_head cpu_timers[3];
/* process credentials */
const struct cred *real_cred; /* objective and real subjective task
* credentials (COW) */
const struct cred *cred; /* effective (overridable) subjective task
* credentials (COW) */
struct mutex cred_guard_mutex; /* guard against foreign influences on
* credential calculations
* (notably. ptrace) */
struct cred *replacement_session_keyring; /* for KEYCTL_SESSION_TO_PARENT */
char comm[TASK_COMM_LEN]; /* executable name excluding path
- access with [gs]et_task_comm (which lock
it with task_lock())
- initialized normally by setup_new_exec */
/* file system info */
int link_count, total_link_count;
……
/* CPU-specific state of this task */
struct thread_struct thread;
/* filesystem information */
struct fs_struct *fs;
/* open file information */
struct files_struct *files;
/* namespaces */
struct nsproxy *nsproxy;
/* signal handlers */
struct signal_struct *signal;
struct sighand_struct *sighand;
sigset_t blocked, real_blocked;
sigset_t saved_sigmask; /* restored if set_restore_sigmask() was used */
struct sigpending pending;
unsigned long sas_ss_sp;
size_t sas_ss_size;
int (*notifier)(void *priv);
void *notifier_data;
sigset_t *notifier_mask;
struct audit_context *audit_context;
#ifdef CONFIG_AUDITSYSCALL
uid_t loginuid;
unsigned int sessionid;
#endif
seccomp_t seccomp;
/* Thread group tracking */
u32 parent_exec_id;
u32 self_exec_id;
/* Protection of (de-)allocation: mm, files, fs, tty, keyrings, mems_allowed, mempolicy */
spinlock_t alloc_lock;
……
/* journalling filesystem info */
void *journal_info;
/* stacked block device info */
struct bio_list *bio_list;
/* VM state */
struct reclaim_state *reclaim_state;
struct backing_dev_info *backing_dev_info;
struct io_context *io_context;
unsigned long ptrace_message;
siginfo_t *last_siginfo; /* For ptrace use. */
struct task_io_accounting ioac;
……
/*
* time slack values; these are used to round up poll() and
* select() etc timeout values. These are in nanoseconds.
*/
unsigned long timer_slack_ns;
unsigned long default_timer_slack_ns;
struct list_head *scm_work_list;
……
};
Linux任务的各种状态(进程/线程)
/*
* Task state bitmask. NOTE! These bits are also encoded in fs/proc/array.c: get_task_state().
*
* We have two separate sets of flags: task->state is about runnability, while task->exit_state are
* about the task exiting. Confusing, but this way modifying one set can't modify the other one by
* mistake.
*/
#define TASK_RUNNING 0 //运行状态
#define TASK_INTERRUPTIBLE 1 //可中断等待
#define TASK_UNINTERRUPTIBLE 2 //不可中断等待
#define __TASK_STOPPED 4 //停止状态
#define __TASK_TRACED 8
/* in tsk->exit_state */
#define EXIT_ZOMBIE 16 //僵尸状态
#define EXIT_DEAD 32 //消亡状态
/* in tsk->state again */
#define TASK_DEAD 64
#define TASK_WAKEKILL 128
#define TASK_WAKING 256
#define TASK_STATE_MAX 512
在Linux内核中是使用轻量级进程的方式来实现多线程的。内核里的每个轻量级进程对应用户空间的一个线程,轻量级的进程也有自己的进程控制结构,也是一个进程调度的单位。
多个轻量级进程共享某些资源,如地址空间、全局变量等。但是轻量级进程在内核中使用的堆栈是独立的。也就是说,线程的堆栈空间是独立的。
由于多个线程共享进程中的资源,这使得线程之间的通信变得非常容易,但因此又会发生资源竞争的问题。为了解决这个问题,Linux提供了线程的同步机制。
我们把上面的那段代码稍微修改一下。
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
int glb = 0; //增加一个全局共享资源
void *fun1(void *pArg)
{
printf("This is thread 1,tid is %u\n",(unsigned int)pthread_self());
glb = 10;
sleep(1);
printf("glb is %d\n",glb);
}
void *fun2(void *pArg)
{
printf("This is thread 2,tid is %u\n",(unsigned int)pthread_self());
sleep(1);
glb++;
printf("glb is %d\n",glb);
}
int main(void)
{
pthread_t pid1 = 0,pid2 = 0;
printf("PID is %u,tid is %u\n",getpid(),(unsigned int)pthread_self());
pthread_create(&pid1,NULL,fun1,NULL);
pthread_create(&pid2,NULL,fun2,NULL);
pthread_join(pid1,NULL);
pthread_join(pid2,NULL);
printf("Main function exit!\n");
return 0;
}
不难发现,线程1的执行结果并非想要的结果。由于共享资源,在线程1休眠的过程中,有其他线程把资源修改了,导致线程1在醒来之后打印的值发生了变化。
互斥量
互斥量 pthread_mutex_t, 是一种锁,在访问共享资源时对其加锁,结束访问时释放。
备注: restrict 是C语言中的一种类型限定符,表示对象已经被指针引用,不能通过除该指针以外的其他直接或间接的方式修改该对象的内容。(C99标准)
int pthread_mutex_init(pthread_mutex_t * restrict mutex,const pthread_mutexattr * restrict attr); //初始化互斥量
int pthread_mutex_lock(pthread_mutex_t *mutex); //尝试获得锁,如果锁已经被其他线程获得,则会导致线程阻塞
int pthread_mutex_trylock(pthread_mutex_t *mutex); //尝试获得锁,不会导致线程阻塞,而会立即返回一个EBUSY的值
int pthread_mutex_unlock(pthread_mutex_t *mutex); //释放锁
使用互斥量把上述代码修改一下。
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
int glb = 0;
pthread_mutex_t *pMutex = NULL;
void *fun1(void *pArg)
{
pthread_mutex_lock(pMutex);
printf("This is thread 1,tid is %u\n",(unsigned int)pthread_self());
glb = 10;
sleep(1);
printf("glb is %d\n",glb);
pthread_mutex_unlock(pMutex);
}
void *fun2(void *pArg)
{
pthread_mutex_lock(pMutex);
printf("This is thread 2,tid is %u\n",(unsigned int)pthread_self());
sleep(1);
glb++;
printf("glb is %d\n",glb);
pthread_mutex_unlock(pMutex);
}
int main(void)
{
pthread_t pid1 = 0,pid2 = 0;
pMutex = (pthread_mutex_t *)malloc(sizeof(pthread_mutex_t));
pthread_mutex_init(pMutex,NULL);
printf("PID is %u,tid is %u\n",getpid(),(unsigned int)pthread_self());
pthread_create(&pid1,NULL,fun1,NULL);
pthread_create(&pid2,NULL,fun2,NULL);
pthread_join(pid1,NULL);
pthread_join(pid2,NULL);
printf("Main function exit!\n");
pthread_mutex_destroy(pMutex);
free(pMutex);
return 0;
}
pthread_mutex_trylock这个接口不会导致调用线程阻塞,我们可以使用它来实现忙等待。
while(EBUSY==pthread_mutex_trylock(pMutex));
pthread_mutex_t mutex = PHTREAD_MUTEX_INITIALIZER;
线程同步的另一种方式是使用条件变量pthread_cond_t。
由于使用互斥量会导致线程进入阻塞状态,被阻塞线程无法自己唤醒,必须等待内核的调度。这种方法实时性不足,因此需要使用条件变量的机制。
pthread_cond_t condarg = PTHREAD_COND_INITIALIZER; //静态初始化条件变量
int pthread_cond_init(pthread_cond_t *pcond, pthread_condattr_t * pcond_attr); //动态初始化条件变量
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_signal(pthread_cond_t *cond); //发送一个条件变量信号
int pthread_cond_broadcast(pthread_cond_t *cond); //广播一个条件变量
int pthread_condattr_destroy(pthread_condattr_t *attr); //销毁一个条件变量
我们把上述的代码再修改一下,把条件变量加进去。
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
int glb = 0;
pthread_mutex_t *pMutex = NULL;
pthread_cond_t *pCond = NULL;
void *fun1(void *pArg)
{
pthread_mutex_lock(pMutex);
pthread_cond_wait(pCond,pMutex);
printf("This is thread 1,tid is %u\n",(unsigned int)pthread_self());
glb = 10;
printf("glb is %d\n",glb);
pthread_mutex_unlock(pMutex);
}
void *fun2(void *pArg)
{
sleep(1);
pthread_mutex_lock(pMutex);
printf("This is thread 2,tid is %u\n",(unsigned int)pthread_self());
glb++;
printf("glb is %d\n",glb);
pthread_mutex_unlock(pMutex);
pthread_cond_signal(pCond);
}
int main(void)
{
pthread_t pid1 = 0,pid2 = 0;
pMutex = (pthread_mutex_t *)malloc(sizeof(pthread_mutex_t));
pthread_mutex_init(pMutex,NULL);
pCond = (pthread_cond_t *)malloc(sizeof(pthread_cond_t));
pthread_cond_init(pCond,NULL);
printf("PID is %u,tid is %u\n",getpid(),(unsigned int)pthread_self());
pthread_create(&pid1,NULL,fun1,NULL);
pthread_create(&pid2,NULL,fun2,NULL);
pthread_join(pid1,NULL);
pthread_join(pid2,NULL);
printf("Main function exit!\n");
pthread_mutex_destroy(pMutex);
pthread_cond_destroy(pCond);
free(pMutex);
free(pCond);
return 0;
}
得到运行结果。
我们可以看到,线程1是等待线程2执行完毕之后再继续执行。线程1被阻塞在pthread_cond_wait()上面了,直到线程2调用了pthread_cond_signal()才被唤醒。
条件变量通常跟互斥量配合使用。在内核中有两个pthread_mutex和pthread_cond两个等待队列。
当线程1调用完pthread_mutex_lock()之后,接着调用pthread_cond_wait(),这时内核会做两件事:
第一是把当前线程塞到pthread_cond等待队列中。
第二是把当前占用的互斥量释放,给其他线程使用。
在pthread_cond_wait()返回后又会做两件事:
第一是把互斥量上锁。
第二是把当前线程从pthread_cond队列中取出来执行。