一.线程的概念
有些情况需要在一个进程中同时执行多个控制流程,比如实现一个图形界面的下载软件,一方面需要和用户交互,等待和处理用户的鼠标键盘事件,另一方面又需要同时下载多个文件,等待和处理从多个网络主机法来的数据,这些任务都需要一个“等待->处理”的循环,那么如何才能同时进行多项任务?
1.需要线程:是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
2.由于同一个进程的多个线程共享同一地址空间,因此代码块,数据块都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
①文件描述符表
②每种信号的处理方式
③当前工作目录
④用户id和组id
但有些资源是每个线程各有一份的:
①线程id
②上下文,包括各种寄存器的值,程序计数器和栈指针
③栈空间
④errno变量
⑤信号屏蔽字
⑥调度优先级
在Linux上线程函数对于libpthread共享库,在编译时要加上-lpthread
②.线程控制
1.创建线程
返回值:成功返回0,失败返回错误号。以前学过的系统函数都是成功返回0,失败返回-1,而错误号保存再全局变量errno中。
pthread库函数都是通过返回值返回错误号,虽然每个线程也都有一个errno,但这是为了兼容其他函数接口而提供的,pthread库本身并不适用它,通过返回值返回错误码更佳清晰。
2.获得当前线程的id
返回值:总是成功返回,返回调用该函数线程ID
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
void *thr_fn(void *arg) {
printf("%s\n", arg);
return NULL;
}
void printid(char *tip) {
pid_t pid = getpid();
pthread_t tid = pthread_self();
printf("%s pid:%u tid: %u (%p)\n", tip, pid, tid, tid);
//printf("%s thr_fn=%p\n", tip, thr_fn);
return;
}
int main(void) {
pthread_t ntid;
int ret = pthread_create(&ntid, NULL, thr_fn, "new thread");
if (ret) {
printf("create thread err:%s\n", strerror(ret));
exit(1);
}
//sleep(1);
printid("main thread\n");
return 0;
}
3.思考:主线程在一个全局变量ntid中保存了新创建的现场的id,如果新创建的线程不调用pthread_self而是直接打印这个ntid,能不能达到同样效果?
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
pthread_t ntid;
void *thr_fn(void *arg) {
printid(arg);
printf("%s ntid=%p\n", arg, ntid);
return NULL;
}
void printid(char *tip) {
pid_t pid = getpid();
pthread_t tid = pthread_self();
printf("%s pid:%u tid: %u (%p)\n", tip, pid, tid, tid);
return;
}
int main(void) {
int ret = pthread_create(&ntid, NULL, thr_fn, "new thread");
if (ret) {
printf("create thread err:%s\n", strerror(ret));
exit(1);
}
sleep(1);
printid("main thread\n");
return 0;
}
4.如果需要只终止某个线程而不终止整个进程,可以有三种方法?
①从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
②一个线程可以调用pthread_cancel终止同一进程中的另一个线程。
③线程可以调用pthread_exit终止自己。
value_ptr是void*类型,和线程函数返回值用法一样,其他线程可以调用pthread_join获得这个指针。
注意:pthread_exit或者return返回指向的内存单位必须是全局的或者是用malloc分配的,不能再线程函数的栈上分配(因为当其他线程得到这个返回指针,线程函数已经退出了)
5.调用该函数的线程将挂起等待,知道id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,如下:
①如果thread线程通过return返回,value_ptr所指向的单位里存放的是thread线程函数的返回值。
②如果thread线程被别的线程调用pthread_cancel异常终止掉,value_ptr所指向的单元里存放的是常数PTHREAD_CANCLED。
③如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
④如果对thread线程的终止状态不感兴趣,可以传NULL给value_ptr参数。
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
void *thr_fn1(void *arg) {
printf("thread1 returning\n");
return (void *)1;
}
void *thr_fn2(void *arg) {
printf("thread2 exiting\n");
pthread_exit((void *)2);
return NULL;
}
void *thr_fn3(void *arg) {
while (1) {
printf("thread3 is sleeping\n");
sleep(1);
}
return NULL;
}
int main() {
pthread_t tid;
void *sts;
pthread_create(&tid, NULL, thr_fn1, NULL);
pthread_join(tid, &sts);
printf("thread1 exit code: %ld\n", (long)sts);
pthread_create(&tid, NULL, thr_fn2, NULL);
pthread_join(tid, &sts);
printf("thread2 exit code: %ld\n", (long)sts);
pthread_create(&tid, NULL, thr_fn3, NULL);
sleep(3);
pthread_cancel(tid);
pthread_join(tid, &sts);
printf("thread3 exit code: %ld\n", (long)sts);
return 0;
}
三.线程间同步
1.多个线程同时访问共享数据时可能会冲突,这跟前面讲信号时所说的可重入性是同样的问题。比如两个线程都要把某个全局变量增加1,这个从操作在某平台需要三条指令完成:
从内存读变量值到寄存器-->寄存器的值加1-->将寄存器的值写回内存
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
int cnt = 0;
void *cntadd(void *arg) {
for (int i = 0; i < 5000; i++) {
int val = cnt;
printf("%p: %d\n", pthread_self(), val);
cnt = val+1;
}
return NULL;
}
int main(void) {
pthread_t tida, tidb;
pthread_create(&tida, NULL, cntadd, NULL);
pthread_create(&tidb, NULL, cntadd, NULL);
pthread_join(tida, NULL);
pthread_join(tidb, NULL);
return 0;
}
2.对于多线程的程序,访问冲突的问题很普遍,解决办法是引入互斥锁:Mutex(Mutual Exclusive Lock),获得锁的线程可以完成“读-修改-写”的操作,然后释放锁给其他线程,没有获得锁的线程只能等待而不能访问共享数据,这样“读-修改-写”三步操作组成一个原子操作,要么都执行,要么都不执行,不会执行到中间被打断,也不会在其他处理器上并行做这个操作。
pthread_mutex_init函数对mutex做初始化,参数attr设定mutex属性,如果attr为NULL表示缺省属性。
用pthread_mutex_init函数初始化的mutex可以用pthread_mutex_destrory销毁。
如果mutex变量是静态分配(全局变量或static变量),也可以用宏定义PTHREAD_MUTEX_INITIALIZER来初始化,相当于用pthread_mutex_init初始化并且attr参数为NULL。
3.mutex的加锁和解锁操作可以用下列函数:
返回值:成功返回0,失败返回错误号。
一个线程可以调用pthread_mutex_lock获得mutex,如果这时另一个线程已经调用pthread_mutex_lock获得该mutex,则当前线程需要挂起等待,知道另一个线程调用pthread_mutex_unlock释放mutex,当前线程被唤醒,才能获得该mutex并继续执行。
如果一个线程既想获得锁,又不想挂起等待,可以调用pthread_mutex_trylock,如果mutex已经被另一个线程获得,这个函数会失败返回EBUSY,而不会使线程挂起等待。
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
int cnt = 0;
pthread_mutex_t add_lock = PTHREAD_MUTEX_INITIALIZER;
void *cntadd(void *arg) {
for (int i = 0; i < 5000; i++) {
pthread_mutex_lock(&add_lock);
int val = cnt;
printf("%p: %d\n", pthread_self(), val);
cnt = val+1;
pthread_mutex_unlock(&add_lock);
}
return NULL;
}
int main(void) {
pthread_t tida, tidb;
pthread_create(&tida, NULL, cntadd, NULL);
pthread_create(&tidb, NULL, cntadd, NULL);
pthread_join(tida, NULL);
pthread_join(tidb, NULL);
return 0;
}
4.挂起等待和唤醒等待线程的操作如何实现?
每个mutex有一个等待队列,一个线程要在mutex上挂起等待,首先在把自己加入等待队列中,然后置线程状态为睡眠,再调用调度器函数切换到别的线程。一个线程要唤醒等待对队列中的其它线程,只需从等待队列中取出一项,把它的状态从睡眠改为就绪,加入就绪队列,那么下次调度器函数执行时就有可能切换到被唤醒的线程。
如果同一个线程先后两次调用lock,在第二次调用时,由于锁已经被占用,该线程会挂起等待别的线程释放锁,然而锁被自己占用着的,该线程又被挂起而没有机会释放锁,因此就永远处于挂起等待状态,这叫做死锁(deadlock)。
另一种死锁情况:线程A获得锁1,线程B获得锁2,这时线程A调用lock试图获得锁2,结果是需要挂起等待线程B释放锁2,而这时线程B也调用lock试图获得锁1,结果需要挂起等待线程A释放锁1,于是线程A和B都永远处于挂起状态。如果涉及到更多线程和更多锁,有没有可能死锁的问题就会变得复杂和难以判断。
写程序时应尽量避免同时获得多个锁,如果一定有必要这么做,一个原则:
如果所有线程在需要多个锁时都按相同的先后顺序获得锁,则不会出现死锁。比如一个程序用到锁1,锁2,锁3,它们对应的mutex变量是锁1->锁2->锁3,那么所有线程在需要同时获得2个或3个锁时都应该按锁1,锁2,锁3的顺序获得。
如果要为所有锁确定一个先后顺序比较困难,应尽量使用pthread_mutex_trylock调用代替pthread_mutex_lock调用,以免死锁。
5.线程间同步还有一种情况:线程A需要等某个条件成立才能继续往下执行,现在这个条件不成立,线程A就阻塞等待,而线程B在执行过程中使这个条件成立,就唤醒线程A继续执行。在pthread库中通过条件变量来阻塞等待一个条件,或者唤醒等待这个条件的线程。条件变量用pthread_cond_t类型的变量来表示,可以这样初始化和销毁:
返回值:成功返回0,失败返回错误号。
6.条件变量的操作可以用以下函数:
返回值:成功返回0,失败返回错误号。
pthread_cond_timedwait函数还有一个额外参数可以设定等待超时,如果到达abstime所指定的时刻仍然没有别的线程来唤醒当前线程,就返回TIMEDOUT,一个线程可以调用pthread_cond_signal唤醒在某个条件变量上等待另一个线程,也可以调用pthread_cond_broadcast唤醒在这个条件变量上等待的所有线程。
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#include <pthread.h>
typedef struct Goods
{
int data;
struct Goods *next;
} Goods;
Goods *head = NULL;
pthread_mutex_t headlock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t hasGoods = PTHREAD_COND_INITIALIZER;
void *producer(void *arg) {
Goods *ng;
while (1) {
ng = (Goods *)malloc(sizeof(Goods));
ng->data = rand() % 100;
pthread_mutex_lock(&headlock);
ng->next = head;
head = ng;
pthread_mutex_unlock(&headlock);
pthread_cond_signal(&hasGoods);
printf("produce %d\n", ng->data);
sleep(rand() % 3);
}
}
void *consumer(void *arg) {
Goods *k;
while (1) {
pthread_mutex_lock(&headlock);
if (!head) {
pthread_cond_wait(&hasGoods, &headlock);
}
k = head;
head = head->next;
pthread_mutex_unlock(&headlock);
printf("consume %d\n", k->data);
free(k);
sleep(rand() % 3);
}
}
int main(void) {
srand(time(NULL));
pthread_t pid, cid;
pthread_create(&pid, NULL, producer, NULL);
pthread_create(&pid, NULL, consumer, NULL);
pthread_join(pid, NULL);
pthread_join(cid, NULL);
return 0;
}
7.mutex变量非0即1,看作是一种资源可用数量,初始化时mutex是1,表示有一个可用资源,加锁时获得该资源,将mutex减到0,表示不再有可用资源,解锁时释放该资源,将mutex重新加到1,表示又有了一个可用资源。
8.信号量semaphore和mutex类似,表示可用资源数量,和mutex不同的是:这个数量可以大于1,这种信号量不仅课用于同意进程的线程间同步,也可用于不同进程间的同步。
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,同时唤醒挂起等待的线程。
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#define NUM 5
int q[NUM];
sem_t blank_number, goods_number;
int head, tail;
void *producer(void *arg) {
while (1) {
sem_wait(&blank_number);
q[tail] = rand() % 100 + 1;
printf("produce %d\n", q[tail]);
sem_post(&goods_number);
tail = (tail + 1) % NUM;
sleep(rand() % 3);
}
}
void *consumer(void *arg) {
while (1) {
sem_wait(&goods_number);
printf("consume %d\n", q[head]);
q[head] = 0;
sem_post(&blank_number);
head = (head + 1) % NUM;
sleep(rand() % 3);
}
}
int main() {
srand(time(NULL));
sem_init(&blank_number, 0, NUM);
sem_init(&goods_number, 0, 0);
pthread_t pid, cid1, cid2, cid3;
pthread_create(&pid, NULL, producer, NULL);
pthread_create(&cid1, NULL, consumer, NULL);
pthread_create(&cid2, NULL, consumer, NULL);
pthread_create(&cid3, NULL, consumer, NULL);
pthread_join(pid, NULL);
pthread_join(cid1, NULL);
pthread_join(cid2, NULL);
pthread_join(cid3, NULL);
return 0;
}