x86/Debian Linux/gcc
此笔记涉及的线程库函数是由POSIX标准定义的,称为POSIXthread或者pthread。其它也有很多关于线程的库如C++ Boost的线程库。
pthread_create ---- 创建一个新的线程
#include <pthread.h> int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
Compile and link with -pthread. |
在一个线程中调用pthread_create()创建新的线程后,当前线程从pthread_create()返回继续往下执行,而新的线程从start_routine()开始执行。start_routine()函数接收一个参数,是通过pthread_create的arg参数传递给它的,该参数的类型为void *,这个指针按什么类型解释由调用者自己定义。start_routine()的返回值类型也是void*,这个指针的含义同样由调用者自己定义。
如果需要只终止某个线程如新进程终止则有以下几种方式:
任何线程调用exit(3)函数,或者主线程从main()函数中返回,会引起统一进程中的所有线程终止。
attr参数指向一个pthread_atrr_t结构体,这个结构体的内容被用在线程创建的时间以决定被创新线程的属性;这个结构体被pthread_init(3)及相关的函数初始化。如果attr为NULL,那么被创建的线程为默认属性。
成功调用pthread_reate()函数,在返回前,将新线程的ID保存在thread指向的缓冲区中;在同一进程中,这个ID可以唯一的标识这个线程,可供其它线程中的函数使用。
pthread_create()函数执行成功则返回0;否则返回错误号,此时*thread内容不定。
man pthread_create下的例子:
#include <pthread.h> #include <string.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <ctype.h> #define handle_error_en(en, msg) \ do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0) #define handle_error(msg) \ do { perror(msg); exit(EXIT_FAILURE); } while (0) struct thread_info { /* Used as argument to thread_start() */ pthread_t thread_id; /* ID returned by pthread_create() */ int thread_num; /* Application-defined thread # */ char *argv_string; /* From command-line argument */ }; /* Thread start function: display address near top of our stack, and return upper-cased copy of argv_string */ static void * thread_start(void *arg) { struct thread_info *tinfo = arg; char *uargv, *p; printf("Thread %d: top of stack near %p; argv_string=%s\n", tinfo->thread_num, &p, tinfo->argv_string); uargv = strdup(tinfo->argv_string); if (uargv == NULL) handle_error("strdup"); for (p = uargv; *p != '\0'; p++) *p = toupper(*p); return uargv; } int main(int argc, char *argv[]) { int s, tnum, opt, num_threads; struct thread_info *tinfo; pthread_attr_t attr; int stack_size; void *res; /* The "-s" option specifies a stack size for our threads */ stack_size = -1; while ((opt = getopt(argc, argv, "s:")) != -1) { switch (opt) { case 's': stack_size = strtoul(optarg, NULL, 0); break; default: fprintf(stderr, "Usage: %s [-s stack-size] arg...\n", argv[0]); exit(EXIT_FAILURE); } } num_threads = argc - optind; /* Initialize thread creation attributes */ s = pthread_attr_init(&attr); if (s != 0) handle_error_en(s, "pthread_attr_init"); if (stack_size > 0) { s = pthread_attr_setstacksize(&attr, stack_size); if (s != 0) handle_error_en(s, "pthread_attr_setstacksize"); } /* Allocate memory for pthread_create() arguments */ tinfo = calloc(num_threads, sizeof(struct thread_info)); if (tinfo == NULL) handle_error("calloc"); /* Create one thread for each command-line argument */ for (tnum = 0; tnum < num_threads; tnum++) { tinfo[tnum].thread_num = tnum + 1; tinfo[tnum].argv_string = argv[optind + tnum]; /* The pthread_create() call stores the thread ID into corresponding element of tinfo[] */ s = pthread_create(&tinfo[tnum].thread_id, &attr, &thread_start, &tinfo[tnum]); if (s != 0) handle_error_en(s, "pthread_create"); } /* Destroy the thread attributes object, since it is no longer needed */ s = pthread_attr_destroy(&attr); if (s != 0) handle_error_en(s, "pthread_attr_destroy"); /* Now join with each thread, and display its returned value */ for (tnum = 0; tnum < num_threads; tnum++) { s = pthread_join(tinfo[tnum].thread_id, &res); if (s != 0) handle_error_en(s, "pthread_join"); printf("Joined with thread %d; returned value was %s\n", tinfo[tnum].thread_num, (char *) res); free(res); /* Free memory allocated by thread */ } free(tinfo); exit(EXIT_SUCCESS); }
编译运行此程序gccpthread_create.c -o pthread_reate -pthread
./pthread -s 0x1000000 you me him
输出结果:
Thread 1: top of stack near 0xb7591384; argv_string=you Thread 3: top of stack near 0xb558f384; argv_string=him Joined with thread 1; returned value was YOU Thread 2: top of stack near 0xb6590384; argv_string=me Joined with thread 2; returned value was ME Joined with thread 3; returned value was HIM |
-s是可选择参数,紧跟其后的0x1000000是可选择参数-s的参数,optind的值为3,optarg指向0x1000000。you me him的数量是pthread_create要创建的线程数量,并以它们为名。
在主线程中调用pthread_join()来获取子线程结束时(return方式)的返回状态。线程1结束时,线程2还未运行。后运行的线程2运行结束后线程3才运行结束。操作系统会在各线程之间调度和切换,就像在多个进程之间调度和切换一样。
多个线程同时访问共享数据时可能会冲突。对于多线程的程序,访问冲突的问题是很普遍的,解决的办法是引入互斥锁(Mutex,Mutual Exclusive Lock),获得锁的线程可以完成“读-修改-写”的操作,然后释放锁给其它线程,没有获得锁的线程只能等待而不能访问共享数据,这样“读-修改-写”三步操作组成一个原子操作,要么都执行,要么都不执行,不会执行到中间被打断,也不会在其它处理器上并行做这个操作。
书中代码(p.663- 664):
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #define NLOOP 5000 int counter; /* incremented by threads */ 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); /* wait for both threads to terminate */ pthread_join(tidA, NULL); pthread_join(tidB, NULL); return 0; } void *doit(void *vptr) { int i, val; /* * Each thread fetches, prints, and increments the counter NLOOP times. * The value of the counter should increase monotonically. */ 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; }
一个线程可以调用pthread_mutex_lock获得Mutex(操作系统调度执行此线程),如果这时另一个线程已经调用pthread_mutex_lock获得了该Mutex,则当前线程需要挂起等待(操作系统仍可能在调度执行此线程),(操作系统调度切换运行另一线程)直到另一个线程调用pthread_mutex_unlock释放Mutex,(另一线程释放mutex时操作系统调度运行此线程)当前线程被唤醒(即从此线程上次执行的地方继续执行),才能获得该Mutex并继续执行。
Figure2.trylock运行结果
线程间的同步还有这样一种情况:线程A需要等某个条件成立才能继续往下执行,现在这个条件不成立,线程A就阻塞等待,而线程B在执行过程中使这个条件成立了,就唤醒线程A继续执行。在pthread库中通过条件变量(ConditionVariable)来阻塞等待一个条件,或者唤醒等待这个条件的线程。
一个Condition Variable总是和一个Mutex搭配使用的。一个线程可以调用pthread_cond_wait在一个Condition Variable上阻塞等待,这个函数做以下三步操作[结合操作系统对线程的调度理解]:
pthread_cond_timedwait函数还有一个额外的参数可以设定等待超时,如果到达了abstime所指定的时刻仍然没有别的线程来唤醒当前线程,就返回ETIMEDOUT。一个线程可以调用pthread_cond_signal唤醒在某个Condition Variable上等待的另一个线程,也可以调用pthread_cond_broadcast唤醒在这个Condition Variable上等待的所有线程。
信号量(Semaphore)和Mutex类似,表示可用资源的数量,和Mutex不同的是这个数量可以大于1。这种信号量不仅可用于同一进程的线程间同步,也可用于不同进程间的同步。
POSIX semaphore库函数:
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value); int sem_wait(sem_t *sem); int sem_trywait(sem_t *sem); int sem_post(sem_t * sem); int sem_destroy(sem_t * sem); |
semaphore变量的类型为sem_t,sem_init()初始化一个semaphore变量,value参数表示可用资源的数量,pshared参数为0表示信号量用于同一进程的线程间同步,本节只介绍这种情况。在用完semaphore变量之后应该调用sem_destroy()释放与semaphore相关的资源。
调用sem_wait()可以获得资源,使semaphore的值减1,如果调用sem_wait()时semaphore的值已经是0,则挂起等待。如果不希望挂起等待,可以调用sem_trywait()。调用sem_post()可以释放资源,使semaphore的值加1,同时唤醒挂起等待的线程。
用挂起等待的方式解决访问冲突不见得是最好的办法,因为这样毕竟会影响系统的并发性,在某些情况下解决访问冲突的问题可以尽量避免挂起某个线程,例如Linux内核的Seqlock、RCU(read-copy-update)等机制。[可参见UNIX系统高级环境编程]
线程在进程的地址空间中,一个进程的中可执行多个线程,每个线程是一个控制流(负责一件事情),多线程的控制流程可以长期并存,操作系统会在各线程之间调度和切换,就像在多个进程之间调度和切换一样。
同一进程的多个线程共享同一地址空间,因此TextSegment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
但有些资源是每个线程各有一份的: