学习关于线程

一.线程的概念

有些情况需要在一个进程中同时执行多个控制流程,比如实现一个图形界面的下载软件,一方面需要和用户交互,等待和处理用户的鼠标键盘事件,另一方面又需要同时下载多个文件,等待和处理从多个网络主机法来的数据,这些任务都需要一个“等待->处理”的循环,那么如何才能同时进行多项任务?

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;
}

上一篇:windows装python的坑 - microsoft store‘s python


下一篇:2022-2-1 牛客C++项目 —— 线程分离