多线程锁详解之【互斥量】

更多的锁介绍可以先看看这篇文章:多线程锁详解之【序章】

正文:
互斥量,一个应用起来跟临界区十分相似的锁,但互斥量是可以命名的,可以跨进程使用,而临界区只能在单个进程内使用,效率方面互斥量往往也比临界区要低,这是什么原因导致的呢?让我们先来学习一遍互斥锁的源码,再讨论上面提出的问题。

源码:
先来看看互斥锁是如何创建的:


typedef struct _KMUTANT {
  DISPATCHER_HEADER Header;
  LIST_ENTRY MutantListEntry; //是个链表节点,用来作为线程互斥门链表的节点
  struct _KTHREAD *RESTRICTED_POINTER OwnerThread; //正在拥有互斥门的线程
  BOOLEAN Abandoned; //发生一次抛弃事件
  UCHAR ApcDisable; //是否禁用内核APC
} KMUTANT, *PKMUTANT, *RESTRICTED_POINTER PRKMUTANT, KMUTEX, 
*PKMUTEX, *RESTRICTED_POINTER PRKMUTEX;

/* 
//提供一个 wdm.h (微软)的定义给大家看看,但我们以 ReactOS 的定义作为讲解
typedef struct _KMUTANT {
  DISPATCHER_HEADER Header;
  LIST_ENTRY        MutantListEntry;
  struct _KTHREAD   *OwnerThread;
  union {
    UCHAR MutantFlags;
    struct {
      UCHAR Abandoned : 1;
      UCHAR Spare1 : 7;
    } DUMMYSTRUCTNAME;
  } DUMMYUNIONNAME;
  UCHAR             ApcDisable;
} KMUTANT, *PKMUTANT, *PRKMUTANT, KMUTEX, *PKMUTEX, *PRKMUTEX;
*/

NTSTATUS NTAPI NtCreateMutant(OUT PHANDLE MutantHandle,
               IN ACCESS_MASK DesiredAccess,
               IN POBJECT_ATTRIBUTES ObjectAttributes  OPTIONAL,
               IN BOOLEAN InitialOwner)
{
    KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
    HANDLE hMutant;
    PKMUTANT Mutant;
    NTSTATUS Status;
    PAGED_CODE();
    DPRINT("NtCreateMutant(0x%p, 0x%x, 0x%p)\n",
            MutantHandle, DesiredAccess, ObjectAttributes);

    /* Check if we were called from user-mode */
    if (PreviousMode != KernelMode)
    {
        /* Enter SEH Block */
        _SEH2_TRY
        {
            /* Check handle pointer */
            ProbeForWriteHandle(MutantHandle);
        }
        _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
        {
            /* Return the exception code */
            _SEH2_YIELD(return _SEH2_GetExceptionCode());
        }
        _SEH2_END;
    }

    /* Create the Mutant Object*/
    Status = ObCreateObject(PreviousMode,
                            ExMutantObjectType,
                            ObjectAttributes,
                            PreviousMode,
                            NULL,
                            sizeof(KMUTANT),
                            0,
                            0,
                            (PVOID*)&Mutant);

    /* Check for success */
    if(NT_SUCCESS(Status))
    {
        /* Initalize the Kernel Mutant */
        DPRINT("Initializing the Mutant\n");
        KeInitializeMutant(Mutant, InitialOwner);

        /* Insert the Object */
        Status = ObInsertObject((PVOID)Mutant,
                                NULL,
                                DesiredAccess,
                                0,
                                NULL,
                                &hMutant);

        /* Check for success */
        if (NT_SUCCESS(Status))
        {
            /* Enter SEH for return */
            _SEH2_TRY
            {
                /* Return the handle to the caller */
                *MutantHandle = hMutant;
            }
            _SEH2_EXCEPT(ExSystemExceptionFilter())
            {
                /* Get the exception code */
                Status = _SEH2_GetExceptionCode();
            }
            _SEH2_END;
        }
    }

    /* Return Status */
    return Status;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
如果你已经看过了事件对象的创建过程,就会发现互斥锁创建的过程是如此的熟悉。这里我们不过多赘述这个函数的流程是做了写什么了,都是一些三板斧动作。主要是讲述一些与其他锁对象不同的地方。

首先是函数名,不知道你发现没,其他的内核锁,都是在 CreateXXX 函数加上 Nt 两个字母,而互斥量的Nt 层函数不叫 NtCreateMutex 而叫 NtCreateMutant ,为什么这样叫?说实话我也不知道,毕竟只是一个命名而已。一般 CreateMutex 函数称之为创建互斥量, NtCreateMutant 函数称之为创建互斥门,当然它们也可以统称为创建互斥锁(哈哈,有点嚼舌根)。

而 NtCreateMutant 构建出来的对象,也是以 DISPATCHER_HEADER 结构体作为第一个成员,所以说 DISPATCHER_HEADER 这个结构体是构成锁的核心要素。而为了更好地了解 KMUTANT 这个结构体,我们先看看 KeInitializeMutant 如何对它初始化:

VOID
NTAPI
KeInitializeMutant(IN PKMUTANT Mutant,
                   IN BOOLEAN InitialOwner)
{
    PKTHREAD CurrentThread;
    KIRQL OldIrql;

    /* Check if we have an initial owner */
    if (InitialOwner)
    {
        /* We also need to associate a thread */
        CurrentThread = KeGetCurrentThread();
        Mutant->OwnerThread = CurrentThread;

        /* We're about to touch the Thread, so lock the Dispatcher */
        OldIrql = KiAcquireDispatcherLock();

        /* And insert it into its list */
        InsertTailList(&CurrentThread->MutantListHead,
                       &Mutant->MutantListEntry);

        /* Release Dispatcher Lock */
        KiReleaseDispatcherLock(OldIrql);
    }
    else
    {
        /* In this case, we don't have an owner yet */
        Mutant->OwnerThread = NULL;
    }

    /* Now we set up the Dispatcher Header */
    Mutant->Header.Type = MutantObject;
    Mutant->Header.Size = sizeof(KMUTANT) / sizeof(ULONG);
    Mutant->Header.DpcActive = FALSE;
    Mutant->Header.SignalState = InitialOwner ? 0 : 1;
    InitializeListHead(&(Mutant->Header.WaitListHead));

    /* Initialize the default data */
    Mutant->Abandoned = FALSE; //是否发生了抛弃事件
    Mutant->ApcDisable = 0; //禁用APC
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
呀,比事件对象信号灯这些锁的初始化要复杂点。主要是多了个 InitialOwner 参数的判断(本质是 CreateMutex 的第二个参数),当 InitialOwner 为 FALSE 的时候,明显是用来第一次初始化结构体的,而当 InitialOwner 为 TRUE 时,KeInitializeMutant 函数除了初始化互斥门对象,还能把当前线程标记为拥有了互斥锁的使用权。而当线程拥有使用权后,会把互斥门对象保存到 CurrentThread->MutantListHead 队列当中,这有什么作用呢?
大家还记得互斥量的一个特性吗?就是当拥有互斥锁权限的线程,在结束时没有释放锁权限的话,系统会帮其释放锁权限。这就是为什么当线程拥有了锁时,要把互斥门存放到 CurrentThread->MutantListHead 的原因。
那为什么系统要主动帮线程释放互斥锁权限呢?因为互斥锁常用于跨进程操作,有时候进程崩溃是不可控的,如果系统不主动帮忙释放互斥锁的话,将会导致其他访问同一个互斥锁的进程无辜卡死。当系统主动帮我们释放互斥锁后,下一次的 WaitForSingleObject 函数的调用将会返回 WAIT_ABANDONED 错误。
而 KMUTANT 结构当中的 OwnerThread 成员也比较简单,它跟临界区的 OwnerThread 成员变量一样,都是用来标记哪条线程拥有了锁权限。但临界区还有一个 RecursionCount 的成员变量,用来作为线程重入计数的。互斥门同样支持线程重入(也就是一个线程可以多次嵌套调用 WaitForSingleObject 函数),那互斥门的 RecursionCount 跑哪里去了呢?
其实互斥门的重入计数,是利用了 SignalState 变量作计算。哈哈,看来 SignalState 变量的作用真不简单,而至于 SignalState 变量是怎么运算的呢?我们先来看看 NtReleaseMutant 和 KeReleaseMutant 函数吧。

NTSTATUS NTAPI NtReleaseMutant(IN HANDLE MutantHandle,
                IN PLONG PreviousCount OPTIONAL)
{
    PKMUTANT Mutant;
    KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
    NTSTATUS Status;
    PAGED_CODE();
    DPRINT("NtReleaseMutant(MutantHandle 0x%p PreviousCount 0x%p)\n",
            MutantHandle,
            PreviousCount);

     /* Check if we were called from user-mode */
    if ((PreviousCount) && (PreviousMode != KernelMode))
    {
        /* Entry SEH Block */
        _SEH2_TRY
        {
            /* Make sure the state pointer is valid */
            ProbeForWriteLong(PreviousCount);
        }
        _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
        {
            /* Return the exception code */
            _SEH2_YIELD(return _SEH2_GetExceptionCode());
        }
        _SEH2_END;
    }

    /* Open the Object */
    Status = ObReferenceObjectByHandle(MutantHandle,
                                       MUTANT_QUERY_STATE,
                                       ExMutantObjectType,
                                       PreviousMode,
                                       (PVOID*)&Mutant,
                                       NULL);

    /* Check for Success and release if such */
    if (NT_SUCCESS(Status))
    {
        /*
         * Release the mutant. doing so might raise an exception which we're
         * required to catch!
         */
        _SEH2_TRY
        {
            /* Release the mutant */
            LONG Prev = KeReleaseMutant(Mutant,
                                        MUTANT_INCREMENT,
                                        FALSE,
                                        FALSE);

            /* Return the previous count if requested */
            if (PreviousCount) *PreviousCount = Prev;
        }
        _SEH2_EXCEPT(ExSystemExceptionFilter())
        {
            /* Get the exception code */
            Status = _SEH2_GetExceptionCode();
        }
        _SEH2_END;

        /* Dereference it */
        ObDereferenceObject(Mutant);
    }

    /* Return Status */
    return Status;
}


LONG NTAPI KeReleaseMutant(IN PKMUTANT Mutant,
                IN KPRIORITY Increment, //MUTANT_INCREMENT : 1
                IN BOOLEAN Abandon, //FALSE
                IN BOOLEAN Wait //FALSE
                )
{
    KIRQL OldIrql;
    LONG PreviousState;
    PKTHREAD CurrentThread = KeGetCurrentThread();
    BOOLEAN EnableApc = FALSE;
    ASSERT_MUTANT(Mutant);
    ASSERT_IRQL_LESS_OR_EQUAL(DISPATCH_LEVEL);

    /* Lock the Dispatcher Database */
    OldIrql = KiAcquireDispatcherLock();//加锁

    /* Save the Previous State */
    PreviousState = Mutant->Header.SignalState; //保存下来

    /* Check if it is to be abandonned */
    if (Abandon == FALSE)
    {
        /* Make sure that the Owner Thread is the current Thread */
        if (Mutant->OwnerThread != CurrentThread)
        {
            //如果当前线程不是锁的持有者,则抛出异常
            /* Release the lock */
            KiReleaseDispatcherLock(OldIrql);释放锁

            /* Raise an exception */
            ExRaiseStatus(Mutant->Abandoned ? STATUS_ABANDONED :
                                              STATUS_MUTANT_NOT_OWNED);
        }

        /* If the thread owns it, then increase the signal state */
        Mutant->Header.SignalState++; //信号量加 1 
    }
    else /*if (Abandon == TRUE)*/
    {
        /* It's going to be abandonned */
        //Abandon 为 TRUE,是线程在退出前,没有完全释放锁的缘故,这个时候
        //系统会帮我们调用一次 KeReleaseMutant ,把锁释放掉
        //既然某个线程已经彻底弃用这个锁,那么不管它曾经重入多少次
        //到了这里 SignalState 都强行置 1 ,SignalState 为 1 就代表有信号了
        //SignalState 有信号就能够唤醒另外一条休眠的线程(如果阻塞队列不为空)
        Mutant->Header.SignalState = 1;
        Mutant->Abandoned = TRUE; //没有被正确释放,设置标记
    }

    /* Check if the signal state is only single */
    if (Mutant->Header.SignalState == 1)
    {
        /* Check if it's below 0 now */
        if (PreviousState <= 0) 
        {
            //如果当前 SignalState 为 1,且之前为 0 ,则表示已经当前线程已经完全释放锁
            //所以线程结构体也不需要再记录互斥门,把互斥门从线程对象中移除
            /* Remove the mutant from the list */
            RemoveEntryList(&Mutant->MutantListEntry);

            /* Save if we need to re-enable APCs */
            EnableApc = Mutant->ApcDisable;
        }
        //else :


        /* Remove the Owning Thread and wake it */
        Mutant->OwnerThread = NULL; //SignalState 为 TRUE 表示没线程占用了

        /* Check if the Wait List isn't empty */
        //如果阻塞队列不为空,则表示有其他线程等待这个互斥锁
        if (!IsListEmpty(&Mutant->Header.WaitListHead))
        {
            /* Wake the Mutant */
            KiWaitTest(&Mutant->Header, Increment);//唤醒一条正在等待的线程
        }
    }

    /* Check if the caller wants to wait after this release */
    if (Wait == FALSE)
    {
        /* Release the Lock */
        KiReleaseDispatcherLock(OldIrql);
    }
    else
    {
        /* Set a wait */
        CurrentThread->WaitNext = TRUE;
        CurrentThread->WaitIrql = OldIrql;
    }

    /* Check if we need to re-enable APCs */
    if (EnableApc) KeLeaveCriticalRegion();

    /* Return the previous state */
    return PreviousState;//返回之前的状态
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
由于 NtReleaseMutant 都是一些三板斧套路,我们跳过 NtReleaseMutant 函数直接分析 KeReleaseMutant 函数。
可以看到 KeReleaseMutant 对 SignalState 的处理是递增,那么就意味着 WaitForSingleObject 函数里面是递减操作了。我们动动手指计算一下,
初始化的互斥门(线程没申请锁权限)的 SignalState 值为 1
然后调用 WaitForSingleObject 拿到锁权限时,SignalState 递减等于 0
那线程重入时,再一次调用 WaitForSingleObject ,这个函数为什么没发生阻塞呢?我摘取一小段 KeWaitForSingleObject 函数的代码给大家分析一下:

NTSTATUS
NTAPI
KeWaitForSingleObject(IN PVOID Object,
    IN KWAIT_REASON WaitReason, //线程上次被切出原因
    IN KPROCESSOR_MODE WaitMode,
    IN BOOLEAN Alertable,
    IN PLARGE_INTEGER Timeout OPTIONAL)
{
    /* Check if it's a mutant */
    //先检测所等待的对象是否有信号了,不过互斥对象需要特殊判断。
    if (CurrentObject->Header.Type == MutantObject)//包含Mutant结构的信号处理
    {
        /* Check its signal state or if we own it */
        if ((CurrentObject->Header.SignalState > 0) ||
            (Thread == CurrentObject->OwnerThread))
        {
            /* Just unwait this guy and exit */
            if (CurrentObject->Header.SignalState != (LONG)MINLONG)
            {
                /* It has a normal signal state. Unwait and return */
                //KiSatisfyMutantWait 负责把目标对象已经收到的信号消耗掉,它主要用于互斥量消耗,
                //互斥量收到信号后 SignalState 会加 1, 那么所谓消耗掉,就是把 SignalState 减 1
                //因为互斥量是线程可重入,所以它会对 SignalState 进行加减计算,重入时是加 1
                KiSatisfyMutantWait(CurrentObject, Thread);//包含Mutant结构的信号消耗
                WaitStatus = (NTSTATUS)Thread->WaitStatus;//唤醒原因
                goto DontWait;//退出循环,函数准备返回
            }
            else
            {
                /* Raise an exception */
                //这里就是 if (CurrentObject->Header.SignalState == (LONG)MINLONG) 条件的处理代码
                //SignalState 到达 MINLONG 表示正整数溢出,变为负数,意味着这个时候锁计数已经太多了。
                KiReleaseDispatcherLock(Thread->WaitIrql); //先执行 执行所有Pending中的内核APC

          //重入次数已经太多了,这里只能抛出一个异常
                ExRaiseStatus(STATUS_MUTANT_LIMIT_EXCEEDED);
            }
        }
    }
}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
哦,原来当 SignalState 大于 0,或者 OwnerThread 为当前线程,WaitForSingleObject 函可以被唤醒,而被唤醒时,还会执行 KiSatisfyMutantWait 这个函数,额,准确来说,它是一个宏,我们来看看定义:

//
// Satisfies the wait of a mutant dispatcher object
//
#define KiSatisfyMutantWait(Object, Thread)                                 \
{                                                                           \
    /* Decrease the Signal State */                                         \
    (Object)->Header.SignalState--;                                         \
                                                                            \
    /* Check if it's now non-signaled */                                    \
    if (!(Object)->Header.SignalState)                                      \
    {                                                                       \
        /* Set the Owner Thread */                                          \
        (Object)->OwnerThread = Thread;                                     \
                                                                            \
        /* Disable APCs if needed */                                        \
        Thread->KernelApcDisable = Thread->KernelApcDisable -               \
                                   (Object)->ApcDisable;                    \
                                                                            \
        /* Check if it's abandoned */                                       \
        if ((Object)->Abandoned)                                            \
        {                                                                   \
            /* Unabandon it */                                              \
            (Object)->Abandoned = FALSE;                                    \
                                                                            \
            /* Return Status */                                             \
            Thread->WaitStatus = STATUS_ABANDONED;                          \
        }                                                                   \
                                                                            \
        /* Insert it into the Mutant List */                                \
        InsertHeadList(Thread->MutantListHead.Blink,                        \
                       &(Object)->MutantListEntry);                         \
    }                                                                       \
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
原来 WaitForSingleObject 对互斥门的递减,就是在 KiSatisfyMutantWait 这个宏里面完成的,而当 SignalState 由 1 变 0 时,意味着它被一个新的线程占用,所以 OwnerThread 被设置为新线程,而互斥门也被加入到线程的 MutantListHead 队列。之所以需要加入 MutantListHead 队列,之前也说过了,是为了让线程结束时,系统能够知道哪些互斥门还没有被正确释放,以便系统帮我们调用 KeReleaseMutant,而系统调用 KeReleaseMutant 函数时, Abandon 参数为 TRUE,这个时候互斥门内部的 Abandoned 也会被设置为TRUE,表示在此之前,发生了至少一次不愉快的事情(一个或多个线程没有正确释放互斥锁)。而当我们察觉到发生不愉快的事情后,会把内核的错误码设置为 STATUS_ABANDONED (转化到应用层为 WAIT_ABANDONED ),再把这件不愉快的事情抹去(把互斥门的 Abandoned 重置为 FALSE)。

疑问解答:
对于互斥量的实现源码,就介绍到这里了。接下来我们来解释一下文章开始时提出的疑问:

a1. 为什么互斥量比临界区效率要低
其实说互斥量的效率完全比临界区要低这个说法肯定是错误的,无论是临界区还是互斥量,他们陷入等待的方式都是调用 WaitForSingleObject 函数,虽然在 WaitForSingleObject 内部处理逻辑稍有差异,但肯定是难以判断出性能差距的。互斥量在锁碰撞时(至少两个线程在抢锁),是直接陷入内核等待,而临界区是用户层自旋4000次抢锁,用户层抢锁失败再陷入内核等待。
很明显,当你发现同样的代码,用临界区加锁比互斥量要快时,证明了临界区处于自旋状态时就把锁使用权拿到手了。如果临界区在自旋状态没能够把锁拿到手呢?那就意味着,他将跟互斥量一样陷入内核等待。由于临界区的等待过程比互斥量还多了自旋这个操作,也就是说,如果临界区不能在自旋状态把锁抢到手,那么它的效率必将比互斥量还要低。
如果是锁碰撞十分频繁且加锁粒度较大时(指代码很多或时间开销很大),可以考虑使用互斥量,说不定效率会更高。

a2. 为什么互斥量能跨进程而临界区不能跨进程
如果你问出这个问题,就要理解一下用户空间和内核空间的概念了。简单来说,用户空间是进程独占的,内核空间是所有进程共享的。也就是说,当不同的进程访问互斥量句柄,进入内核后它们最终访问的都是同一块内存地址,而不同进程的临界区,很明显他们地址不同,无法为不同进程提供同一个对象,也就无法实现跨进程加锁的动作。
那如果我把 CRITICAL_SECTION 结构放到内核层,并且以句柄的形式返回应用层呢? 答案是可以的,这个时候你的临界区将支持跨进程访问。但这样做有意义吗?
这个我没测试过,但看上去意义不大,也不能说这样的临界区性能是否比互斥量要好。因为内核临界区需要进入内核,查找对象,释放对象,退出内核等操作。这些动作的时间消耗,将会加大另外一个线程,在临界区自旋期间不能抢锁成功的概率,也就是这种场景下自旋锁可能会变成一个累赘,让内核临界区比内核互斥门更慢。嗯,理论上是这样的,实际上大家需要测试一下才知道实际效果。

a3. 互斥量如何解决死锁
在临界区的文章我们提及到,临界区解决死锁,可以自行添加一个链表,然后通过链路检查来判断死锁。死锁的判断条件有了,也是通用的。那么问题是当判断出产生死锁以后,如何解决死锁呢?
因为临界区中 unlock 函数并没有判断调用线程是否就是拥有锁的线程,所以在哪个线程解锁都没关系,然而互斥锁的 unlock 大家也看到了,它是会判断当前调用锁的线程是否就是锁的拥有者,这就意味着不能在其他线程调用互斥锁的 unlock 了。联想拥有互斥锁的线程在结束时没有释放锁权限的话,系统会帮其释放锁权限的特性。我们可以得出两个解决方案:
一是在抢锁线程判断出自己将要发生死锁,放弃自己已经抢到手的锁(死锁产生的条件,主要都是因为两个以上的锁没有保证调用顺序,一般都是在第二个或往后的锁才能判断产生了死锁),当然了,连带放弃的,还有本次线程需要执行的任务。
二是死锁产生后,在其他线程判断出来,将产生死锁的一条或多条线程终结掉,让系统帮我们释放互斥锁。

a4. 线程真的可以强行终结吗
在一些编程规范中会明确提到:强行终结线程,会引起内存泄露或数据损坏的问题,所以不允许强行终结线程;如果想结束一条线程,请使用 return 关键字让线程主动结束。
这个规范的另外一个意思,就是说不允许强行终结线程是因为害怕出现不可控的问题。反过来说,只要我们能够保证线程可控,线程是可以强行结束的。
且看以下代码:

int thread_A(void*)
{
    int a = 0;
    a++;
    return 0;
}

int thread_B(void*)
{
    int a = 0;
    void* p = malloc(100);
    a++;
    free(p);
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
请问 thread_A 线程强行结束会引起不可控后果吗,答案是不会,它跟正常 return 没有任何差别。强行结束 thread_B 呢?
强行结束 thread_B 可能会引起两个问题,一个是内存泄露,另外一个是如果 thread_B 正处于 malloc 或 free 内部被结束掉,会引起其他线程将无法再调用这两个函数,因为 malloc 和 free 内部是有锁(不是互斥锁)的,如果 thread_B 被终结时正处于 malloc / free 的加锁状态,那么其他调用这两个函数的线程将会死锁。
但在我们设计的场景中,上面第二个问题真的会发生吗?答案是不会的,因为当我们检测到线程死锁,那么这些线程百分百处于 lock 函数中,不会存在于其他地方!而且我们是有能力知道线程执行到哪一步的,所以我们主要关注内存泄露问题就可以了。
关于内存泄露的处理方案,比较简单的一个方法是,提供一个 struct _ttev (线程任务环境)的结构体,将需要用到的资源都放到里面,强行终结线程后,再将对应线程的 struct _ttev 对象回收即可。(参考进程终结为什么不会引起内存泄露的原因)
其实强行终结线程,还是十分危险的操作,对开发者的要求也非常高,大家在设计强行终结线程,一定要多加思考和检测!
————————————————
版权声明:本文为CSDN博主「园中猪吃菜」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq492927689/article/details/125638935

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值