本文简单说明了linux操作系统中线程的概念,以及简单的操作。

线程的概念

进程在各自独立的地址空间中运行,进程之间共享数据需要用mmap或者进程间通信机制。有些情况需要在一个进程中同时执行多个控制流程,这时候线程就派上了用场。

每个线程都会执行相同的程序代码,共享同一数据区域和堆。同时,每个线程都有自己的栈,用来存储本地变量和函数调用链接信息。线程之间可以通过共享的全局变量进行通讯,除此之外,各线程还共享以下进程资源和环境:

  • 文件描述符表
  • 每种信号的处理方式(SIG_IGN、SIG_DFL或者自定义的信号处理函数)
  • 当前工作目录
  • 用户id和组id

但有些资源是每个线程各有一份的:

  • 线程id
  • 上下文,包括各种寄存器的值、程序计数器和栈指针
  • 栈空间
  • errno变量
  • 信号屏蔽字
  • 调度优先级

线程控制

线程的创建

程序开始启动时,它是以单进程中的单个控制线程启动的。在创建多个线程之前,程序的行为与传统的进程并没有什么区别。新增的线程可以调用pthread_create函数创建。

#include <pthread.h>

int pthread_create(pthread_t thread, const pthread_attr_t *attr,
	               void *(*start_routine) (void *), void *arg);

pthread_create成功返回时,新创建线程的线程 ID 会被存入thread指向的内存单元。attr用于定制不同的线程属性,这里我们设为NULL,创建一个具有默认属性的线程。新创建的线程会从start_toutine函数的地址开始运行,将函数的参数存入arg指针指向的地址,如果参数数量大于1,那么需要把参数放入结构体中,然后传入结构体的指针。

我们写一个打印线程ID的简单小程序:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

pthread_t ntid;

/* print thread information */
void printids(const char *s)
{
	pid_t pid;
	pthread_t tid;
	pid = getpid();
	tid = pthread_self();
	printf("%s pid %u tid %u (0x%x)\n", s, (unsigned int)pid, 
	                      (unsigned int)tid, (unsigned int)tid);
}

/* thread function */
void *thr_fn(void *arg)
{
	printids(arg);
	return NULL;
}

int main(void)
{
	int err;
    // create a new thread
	err = pthread_create(&ntid, NULL, thr_fn, "new thread: ");
	if (err) {
		fprintf(stderr, "can't create thread: %s\n", strerror(err));
		exit(1);
	}
	printids("main thread:");
	sleep(1);

    return 0;
}

编译运行结果如下:

$ gcc 35-1.c -lpthread -o thread-1
$ ./thread-1
main thread: pid 45 tid 1461106432 (0x5716b700)
new thread:  pid 45 tid 1452906240 (0x56999700)

从上面的输出结果可以看到,每个线程都有一个线程ID,进程ID在整个系统中是唯一的,但线程ID不同,线程ID只有在它所属进程上下文中才有意义。

线程终止

如果进程中的任意线程调用了exit_exit_Exit,那么整个进程就会终止。在不终止整个进程控制的情况下,单个线程可以通过3种方式退出:

  • 线程简单的从启动例程中返回
  • 线程可以调用pthread_cancel,终止同一进程中的另一个线程
  • 线程调用pthread_exit终止自己
#include <pthread.h>

void pthread_exit(void *value_ptr);

需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

#include <pthread.h>

int pthread_join(pthread_t thread, void **value_ptr);

调用pthread_join的线程将会阻塞,直到线程thread结束。我们可以根据value_ptr中的内容,查看线程的退出状态。

pthread_createpthread_join函数可以通过结构指针传递不止一个参数。但是这个结构所使用的内存在调用完成后必须仍然有效。

线程可以调用pthread_cancel函数来请求取消同一进程中的其他线程

#include <pthread.h>

int pthread_cancel(pthread_t thread);

线程的实际终止和pthread_cancel函数调用时异步的。函数成功得到返回值,仅仅代表成功发出请求,并不代表线程已经终止。

线程同步

对于多线程的程序,访问冲突的问题是很普遍的,解决的办法是引入互斥锁(Mutex,Mutual Exclusive Lock),获得锁的线程可以完成“读-修改-写”的操作,然后释放锁给其它线程,没有获得锁的线程只能等待而不能访问共享数据,这样“读-修改-写”三步操作组成一个原子操作,要么都执行,要么都不执行,不会执行到中间被打断,也不会在其它处理器上并行做这个操作。

Mutex用pthread_mutex_t类型的变量表示,可以这样初始化和销毁:

#include <pthread.h>

int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
       const pthread_mutexattr_t *restrict attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_init函数初始化的Mutex可以用pthread_mutex_destroy销毁。如果Mutex变量是静态分配的(全局变量或static变量),也可以用宏定义PTHREAD_MUTEX_INITIALIZER来初始化,相当于用pthread_mutex_init初始化并且attr参数为NULL。Mutex的加锁和解锁操作可以用下列函数:

#include <pthread.h>

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获得Mutex,如果这时另一个线程已经调用pthread_mutex_lock获得了该Mutex,则当前线程需要挂起等待,直到另一个线程调用pthread_mutex_unlock释放Mutex,当前线程被唤醒,才能获得该Mutex并继续执行。

如果一个线程既想获得锁,又不想挂起等待,可以调用pthread_mutex_trylock,如果Mutex已经被另一个线程获得,这个函数会失败返回EBUSY,而不会使线程挂起等待。以下为一个简单的双线程计数器代码:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

#define NLOOP 5000

int counter;
pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;

void *doit(void *);

int main(int argc, char *argv[])
{
	pthread_t tidA, tidB;
	pthread_create(&tidA, NULL, doit, NULL);
	pthread_create(&tidB, NULL, doit, NULL);
	pthread_join(tidA, NULL);
	pthread_join(tidB, NULL);

	return 0;
}

void *doit(void *vptr)
{
	int i, val;
	for (i = 0; i < NLOOP; i++) {
		pthread_mutex_lock(&counter_mutex);

		val = counter;
		printf("%x: %d\n", (unsigned int)pthread_self(), val+1);
		counter = val + 1;

		pthread_mutex_unlock(&counter_mutex);
	}

	return NULL;
}

以上程序的运行结果为10000,不会出现多线程访问冲突的情况。假如将Mutux部分注释掉运行程序,结果可能会出现错误。