[Linux C]Linux多线程编程基础

本文介绍了Linux环境下多线程编程的基础知识,包括线程的概念、POSIX线程库的使用方法,以及如何利用互斥量和条件变量进行线程间的同步。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

        进程是参与操作系统(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;
}

        线程1的运行结果就正常了,由此可见,互斥量可以对共享资源起到保护的作用。




        pthread_mutex_trylock这个接口不会导致调用线程阻塞,我们可以使用它来实现忙等待。

        while(EBUSY==pthread_mutex_trylock(pMutex));

         互斥量还有另外一种初始化方式(静态创建方式), 这种方式不适用于用malloc()申请的互斥量。
        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队列中取出来执行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值