一步一步学linux多线程编程

本文介绍了Linux环境下如何创建、等待、取消线程,以及如何使用互斥锁、条件变量和信号量进行线程同步。通过示例代码,解释了线程参数传递、资源释放和线程同步的重要性。

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

windows下的多线程已经很熟悉了,本以为迁移到linux很容易,但总是感觉心里没底,函数名都不一样。所以现在把linux下的多线程完整的走一遍。

1、创建线程

windows下用_beginthreadex来开启一个线程,那么linux呢? 那就用pthread_create。相比之下,pthread_create参数更少,参数是pid, attr, startaddr, param。

头文件

#include<pthread.h>

函数声明
int pthread_create(pthread_t *tidp,const pthread_attr_t *attr,(void*)(*start_rtn)(void*),void *arg);

编译链接参数
-pthread

返回值


若线程创建成功,则返回0。若线程创建失败,则返回出错编号,并且*thread中的内容是未定义的。返回成功时,由tidp指向的内存单元被设置为新创建线程的线程ID。attr参数

用于指定各种不同的线程属性。新创建的线程从start_rtn函数的地址开始运行,该函数只有一个万能指针参数arg,如果需要向start_rtn函数传递的参数不止一个,那么需要把这

些参数放到一个结构中,然后把这个结构的地址作为arg的参数传入。

按照UNIX惯例,函数成功时返回0,失败时返回-1,但pthread_XXX系列函数并未遵循这个惯例,在pthread_XXX失败时返回的是错误码。

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

void *thread_fun(void *param)
{
    for ( int i = 0; i < 10; ++i)
    {
        printf("this is thread fun %d...\n", i);
    }
}

int main()
{
    pthread_t tid;
    if ( 0 != pthread_create(&tid, NULL, thread_fun, NULL) )
    {
        printf("create thread failed!\n");
    }
    for ( int i = 0; i < 10; ++i)
    {
        printf("this is main function %d ...\n", i);
    }
}



怎么全都是main function,难道线程创建失败了?做了判断了啊。其实这就是线程的第一个问题,和线程的调度有关,就详细说了,简单一句话:thread线程还没开始执行main就已经执行完了,由于main退出则进程结束,进程中的所有线程也就结束了,如果还要看,则可以把main中的 循环次数加大,则会看到thread也打印了。


2、等待线程

由前面可以知道,thread还没来得急执行main就退出了,那有没有什么办法呢?当然有,而且很多,只要能让main阻塞的操作都可以,比如:等待输入,sleep,空转循环等等。但这些始终感觉是小门小道,难登大雅之堂。在windows中有WaitForSingleObject来等待对象,linux中有pthread_join来等待。

函数pthread_join用来等待一个线程的结束。
头文件 :
#include <pthread.h>
函数定义: 

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

描述 :
pthread_join()函数,以阻塞的方式等待thread指定的线程结束。当函数返回时,被等待线程的资源被收回。如果线程已经结束,那么该函数会立即返回。并且thread指定的线程必须是joinable的。

参数 :
thread: 线程标识符,即线程ID,标识唯一线程。retval: 用户定义的指针,用来存储被等待线程的返回值。

返回值 : 
0代表成功。 失败,返回的则是错误号。

代码中如果没有pthread_join主线程会很快结束从而使整个进程结束,从而使创建的线程没有机会开始执行就结束了。加入pthread_join后,主线程会一直等待直到等待的线程结束自己才结束,使创建的线程有机会执行。所有线程都有一个线程号,也就是Thread ID。其类型为pthread_t。通过调用pthread_self()函数可以获得自身的线程号另外需要说明的是一个线程不能被多个线程等待,也就是说对一个线程只能调用一次pthread_join,否则只有一个能正确返回,其他的将返回ESRCH 错误。

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

void *thread_fun(void *param)
{
    for ( int i = 0; i < 10; ++i)
    {
        printf("this is thread fun %d...\n", i);
    }
    const static char *p = "I'am finish, thank you call me...";
    return (void *)p;
}

int main()
{
    pthread_t tid;
    if ( 0 != pthread_create(&tid, NULL, thread_fun, NULL) )
    {
        printf("create thread failed!\n");
    }
    for ( int i = 0; i < 10; ++i)
    {
        printf("this is main function %d ...\n", i);
    }

    char *p = NULL;
    pthread_join(tid, (void **)&p);
    printf("all finish! thank you! %s\n", p);
}

调用了pthread_join来等待线程返回,为了更具客观性,顺便获取线程的返回值。


由此可见,线程也执行完全了,可能有人会问,这儿怎么显示main函数执行,再是线程函数执行,是不是有顺序问题?当然没有,执行是随机的,只不过这儿只循环10次,次数很少了,一下子就执行完了。pthread_join除了具有等待的效果外,还有资源释放的关系,简单说就是用默认属性创建的线程如果不调用pthread_join,那么它的资源就不会得到释放,反过来说,这样的线程必须有别的线程对它进行pthread_join,否则就是出现内存泄露。但是还有一些线程,更喜欢自己来清理退出的状态,他们也不愿意主线程调用pthread_join来等待他们。我们将这一类线程的属性称为detached。如果我们在调用pthread_create()函数的时候将属性设置为NULL,则表明我们希望所创建的线程采用默认的属性,也就是joinable。如果需要将属性设置为detached,

pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);


可以通过这段代码来构造一个detached线程的属性,在调用pthread_create的时候传递这个属性就可以了。除此之外我们可以调用pthread_detach对已经创建的

joinable且还没有调用pthread_join的线程设成detached,但如果线程已经调用了join,那么再调用detach是无效的,且需要注意的是你不能用pthread_join将一个

detached线程设成可joinable的。

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

void *thread_fun(void *param)
{
    for ( int i = 0; i < 10; ++i)
    {
        printf("this is thread fun %d...\n", i);
    }
    const static char *p = "I'am finish, thank you call me...";
    return (void *)p;
}

int main()
{
    pthread_t tid;
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    if ( 0 != pthread_create(&tid, &attr, thread_fun, NULL) )
    {
        printf("create thread failed!\n");
    }
    for ( int i = 0; i < 10; ++i)
    {
        printf("this is main function %d ...\n", i);
    }

    char *p = NULL;
    if (0 != pthread_join(tid, (void **)&p) )
    {
        printf("join failed!\n");
    }
    printf("all finish! thank you! %s\n", p);
}



虽然调用pthread_join来等待线程结束,但由于线程是detached的,所以join失败。所以一旦线程设成了detached,那就它就没办法再回到joinable状态了。


3、取消线程pthread_cancel

这个函数可以向目标线程发送一个终止信号,感觉应该比较有用。目前我的理解就是一个线程可以向另一个线程发送cancel信号,请求退出,当然这也要看那个线程愿不愿意,所以这里还有一个函数

int pthread_setcancelstate(int state, int *oldstate)

int pthread_cancel(pthread_t thread)

发送终止信号给thread线程,如果成功则返回0,否则为非0值。发送成功并不意味着thread会终止。

int pthread_setcancelstate(int state, int *oldstate)设置本线程对Cancel信号的反应,state有两种值:PTHREAD_CANCEL_ENABLE(缺省)和 PTHREAD_CANCEL_DISABLE,分别表示收到信号后设为CANCLED状态和忽略CANCEL信号继续运行;old_state如果不为 NULL则存入原来的Cancel状态以便恢复。

int pthread_setcanceltype(int type, int *oldtype)

设置本线程取消动作的执行时机,type由两种取值:PTHREAD_CANCEL_DEFERRED和 PTHREAD_CANCEL_ASYNCHRONOUS,仅当Cancel状态为Enable时有效,分别表示收到信号后继续运行至下一个取消点再退出和立即执行取消动作(退出);oldtype如果不为NULL则存入原来的取消动作类型值。至于什么叫“下一个取消点”,我目前还不知道。综上,要似pthread_cancel生效,需要三步走,首先你自己要能响应cancel的信号,所以需要用pthread_setcancelstate来设置cancel的状态。其次你对cancel设成enable之后你还的指定什么时候退出,是立即退出呢还是等到所谓的下一个取消点再退出。最后,无论你把状态设置的怎么完美,最重要的是有人给你发送cancel信号,也就是别的线程调用pthread_cancel。了解了这些,看个例子。

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

void *thread_fun(void *param)
{
    if ( 0 != pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL) )
    {
        printf("set cancel state failed!\n");
    }
    if ( 0 != pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL) )
    {
        printf("set cancel type failed!\n");
    }
    for ( int i = 0; i < 500; ++i)
    {
        printf("this is thread fun %d...\n", i);
    }
}

int main()
{
    pthread_t tid;
    if ( 0 != pthread_create(&tid, NULL, thread_fun, NULL) )
    {
        printf("create thread failed!\n");
    }
    for ( int i = 0; i < 5; ++i)
    {
        printf("this is main function %d ...\n", i);
    }
    if ( 0 != pthread_cancel(tid) )
    {
        printf("cancel failed!\n");
    }
    getchar();
}

由于输出结果比较多,就不贴图了。注意看,最后用了个getchar()来阻塞main函数,假如没有对线程进行cancel处理,那么由于main阻塞,线程会运行结束。但加了cancel处理之后,线程会立刻结束。

根据我的经验,我觉得这样的场景还是比较常见的。线程的创建取消就说到这儿,估计差不多够用了,再说说同步问题。linux线程同步方法有互斥锁、条件变量、信号量,其实这些windows都有。

1、互斥锁。

先来一个直白的理解。我要访问一个公共资源,那么可以设置一个标志量,当有人使用的时候让她设为1,没人使用的时候让它设为0。这看看起来是完美的方法(事实也是完美

的办法),但关键的问题在于并发,你无法保证当你把标志量设为1的过程中恰好也有别的线程在使用这个标志量。所以关键的问题在于怎么样在我使用标志量的时候别的线程

无法访问,答案是“原子操作”。用户无法实现原子操作,所以系统提供了。


有两种方法创建互斥锁,静态方式和动态方式。

POSIX定义了一个宏PTHREAD_MUTEX_INITIALIZER来静态初始化互斥锁,方法如下: pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER; 在LinuxThreads实现中,pthread_mutex_t是一个结构,而PTHREAD_MUTEX_INITIALIZER则是一个结构常量。

动态方式是采用pthread_mutex_init()函数来初始化互斥锁,API定义如下: int pthread_mutex_init(pthread_mutex_t *mutex, constpthread_mutexattr_t *mutexattr) 其中mutexattr用于指定互斥锁属性(见下),如果为NULL则使用缺省属性。

pthread_mutex_destroy()用于注销一个互斥锁,API定义如下: int pthread_mutex_destroy(pthread_mutex_t *mutex) 销毁一个互斥锁即意味着释放它所占用的资源,且要求锁当前处于开放状态。由于在Linux中,互斥锁并不占用任何资源,因此LinuxThreads中的pthread_mutex_destroy()除了检查锁状态以外(锁定状态则返回EBUSY)没有其他动作。

互斥锁属性:
互斥锁的属性在创建锁的时候指定,在LinuxThreads实现中仅有一个锁类型属性,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同。有四个值可供选择:
PTHREAD_MUTEX_TIMED_NP,这是缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。
PTHREAD_MUTEX_RECURSIVE_NP,嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。
PTHREAD_MUTEX_ERRORCHECK_NP,检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。
PTHREAD_MUTEX_ADAPTIVE_NP,适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。

加锁。对共享资源的访问,要对互斥量进行加锁,如果互斥量已经上了锁,调用线程会阻塞,直到互斥量被解锁。
int pthread_mutex_lock(pthread_mutex *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
解锁。在完成了对共享资源的访问后,要对互斥量进行解锁。
int pthread_mutex_unlock(pthread_mutex_t *mutex);
销毁锁。锁在是使用完成后,需要进行销毁以释放资源。
int pthread_mutex_destroy(pthread_mutex *mutex);

下面仿照孙鑫讲解多线程时的模拟火车票订购

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

int tickets = 0;
void *thread_fun(void *param)
{
    int index = *(int *)param;
    while (tickets < 10)
    {
        printf("%d seal ticket %d\n", index, tickets);
        tickets += 1;
    }
}

int main()
{
    pthread_t tid[4];
    for ( int i = 0; i < 4; ++i )
    {
        if ( 0 != pthread_create(&tid[i], NULL, thread_fun, &i))
        {
            printf("create thread failed!");
        }
    }
    getchar();
}


对于输出结果,很明显可以知道是错的,注意在代码的main中并没有用pthread_join而是在最后用了getchar()来阻塞,因为pthread_join会让main线程阻塞,执行了第一条join后面的join就要就要等被阻塞。

对于上面问题的分析很简单,4个线程同时访问了一个tickets公共变量而没有任何加锁同步措施,接下来用mutex改进。

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

int tickets = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void *thread_fun(void *param)
{
    int index = *(int *)param;
    while (true)
    {
        pthread_mutex_lock(&mutex);
        if ( tickets > 10 )
        {
            return NULL;
        }
        printf("%d seal ticket %d\n", index, tickets);
        tickets += 1;
        pthread_mutex_unlock(&mutex);
    }
}

int main()
{
    pthread_t tid[4];
    for ( int i = 0; i < 4; ++i )
    {
        if ( 0 != pthread_create(&tid[i], NULL, thread_fun, &i))
        {
            printf("create thread failed!");
        }
    }
    getchar();
}


由结果可以知道,整个逻辑是对的,但是为什么全是4?因为只循环10次,你可以把次数加大看看。你有没有发现我创建线程的时候是for(int i = 0 ; i < 4; ++i)而给线程传递的参数是i,也就是说线程接收到的参数是在[0, 4)这个区间内,怎么会有4???答案是这样的,在线程中使用了int index = *(int *)param来获取传递的参数,很明显,是根据地址获取的,而这个地址就是i的地址,那么在调用pthread_create之后index并没有被马上赋值,而是main中的循环继续执行,这个时候i已经递增了。这就是线程传递参数的问题,我通常的做法是创建线程后sleep一下,让新线程有足够的时间去拷贝参数,当然,这种做法是不可取的,正确的做法应该是在传递参数的时候用同步机制,当然这是后话。

如果pthread_mutex_lock获取锁失败,则习线程被阻塞,而是用pthread_mutex_trylock时会马上返回,根据返回值判断是否加锁成功,如果不成功,可以打印相应信息。


2、条件变量

看了一下条件变量,实在没看懂。下面是别人博客上的:

互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。条件变量分为两部分: 条件和变量。条件本身是由互斥量保护的。线程在改变条件状态前先要锁住互斥量。条件变量使我们可以睡眠等待某种条件出现。条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。条件的检测是在互斥锁的保护下进行的。如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。如果两进程共享可读写的内存,条件变量可以被用来实现这两进程间的线程同步。

初始化条件变量。
静态态初始化,pthread_cond_t cond = PTHREAD_COND_INITIALIER;
动态初始化,int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
等待条件成立。释放锁,同时阻塞等待条件变量为真才行。timewait()设置等待时间,仍未signal,返回ETIMEOUT(加锁保证只有一个线程wait)
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);
激活条件变量。pthread_cond_signal,pthread_cond_broadcast(激活所有等待线程)
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond); //解除所有线程的阻塞
清除条件变量。无线程等待,否则返回EBUSY
int pthread_cond_destroy(pthread_cond_t *cond);

我的疑惑在于在使用条件变量的时候为什么还要一个互斥量,而且奇怪的是在cont_wait的时候mutex是作为参数的,但cond_signal的时候没有mutex作为参数。“cond一旦进入wai t就会自动release mutex,当有信号是cond 会自动获取mutex。wait 内部操作:一进入wait 就unlock,在wait 结束前就lock”。只能这么理解了。。。

thread1:
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond, &mutex)
pthread_mutex_unlock(&mutex)

thread2
pthread_mutex_lock(&mutex);
pthread_cond_signal(&cond)
pthread_mutex_unlock(&mutex)

这个地方就不举例了,条件变量我还是理解的不深。


3、信号量

在前面讨论过一个进程可以给另一个进程发送cancel信号而让它退出,那么线程呢?线程能不能这样做?

#include<semaphore.h>

函数原型
int sem_init(sem_t *sem, int pshared, unsigned int value);
说明
sem_init() 初始化一个定位在 sem 的匿名信号量。value 参数指定信号量的初始值。 pshared 参数指明信号量是由进程内线程共享,还是由进程之间共享。如果 pshared 的值为 0,那么信号量将被进程内的线程共享,并且应该放置在这个进程的所有线程都可见的地址上(如全局变量,或者堆上动态分配的变量)。
由此可以看出,信号量不仅可以用来做线程间的同步,还可以做进程间的同步。

下面在介绍sem_wait和sem_post两个操作函数

#include <semaphore.h>
int sem_wait(sem_t * sem);
int sem_post(sem_t * sem);

这两个函数的形式完全一样。下面是摘自百度百科的一段话:

sem_wait函数也是一个原子操作,它的作用是从信号量的值减去一个“1”,但它永远会先等待该信号量为一个非零值才开始做减法。也就是说,如果你对一个值为2的信号量调用sem_wait(),线程将会继续执行,这信号量的值将减到1。如果对一个值为0的信号量调用sem_wait(),这个函数就 会地等待直到有其它线程增加了这个值使它不再是0为止。如果有两个线程都在sem_wait()中等待同一个信号量变成非零值,那么当它被第三个线程增加 一个“1”时,等待线程中只有一个能够对信号量做减法并继续执行,另一个还将处于等待状态。

从这儿可以得到两点:首先sem_wait是个原子操作,素所以不需要额外的同步措施,前面说的条件变量在使用时需要另外的一个mutex,我怀疑是pthread_cond_wait不是原子操作,所以才需要另外的一个mutex。另外一点sem_wait是阻塞的,如果要用非阻塞,可以用sem_trywait()执行成功返回0,执行失败返回 -1且信号量的值保持不变,此外还有sem_timewait()表示阻塞一定的时间。

再看看sem_post

#include <semaphore.h>
int sem_post(sem_t *sem);
sem_post函数的作用是给信号量的值加上一个“1”,它是一个“原子操作”---即同时对同一个信号量做加“1”操作的两个线程是不会冲突的;而同时对同一个文件进行读、加和写操作的两个程序就有可能会引起冲突。信号量的值永远会正确地加一个“2”--因为有两个线程试图改变它。 当有线程阻塞在这个信号量上时,调用这个函数会使其中一个线程不在阻塞,选择机制是有线程的调度策略决定的。
sem_post() 成功时返回 0;错误时,信号量的值没有更改,-1 被返回,并设置 errno 来指明错误。

这也是百度百科的一段话,由此可见,sem_post也是原子操作,如果把信号量的初始值设成1,它就退化成互斥量了。

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

int tickets = 0;
sem_t sem;

void *thread_fun(void *param)
{
    int index = *(int *)param;
    while (true)
    {
        sem_wait(&sem);
        if ( tickets > 10 )
        {
            return NULL;
        }
        printf("%d seal ticket %d\n", index, tickets);
        tickets += 1;
        sem_post(&sem);
    }
}

int main()
{
    if ( 0 != sem_init(&sem, 0, 1 ) )
    {
        printf("init semsphore failed!\n");
        return 0;
    }
    pthread_t tid[4];
    for ( int i = 0; i < 4; ++i )
    {
        if ( 0 != pthread_create(&tid[i], NULL, thread_fun, &i))
        {
            printf("create thread failed!");
        }
    }
    getchar();
}





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值