Content Developer II at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
132005 stories
·
29 followers

Understanding Parallel Programming: Thread Management for Beginners

1 Share

\ I am Boris Dobretsov, and this is the fourth part of a series ==Understanding Parallel Programming: A Guide for Beginners==.

\ If you haven’t read the first three parts, have a look Understanding Parallel Programming: A Guide for Beginners, Understanding Parallel Programming: A Guide for Beginners, Part II, Understanding Threads to Better Manage Threading in iOS.

\ Today we are going to continue exploring Threads and will discuss Thread managementThread execution flags and Thread priority management tool. Let’s get started!

Thread Management

Sometimes it is not enough just to execute code in a separate thread, you need to manage it, start and cancel its execution.

The simplest thing you can do with a thread is to start it.

let thread1 = ThreadprintDemon()
thread1.start()

\ The thread will start and begin executing the main method. Sometimes beginners make a mistake: they create a thread and call its main method instead of start. In this case, the method will be executed in the thread in which it was called, without creating a separate one. Be careful. The second possible action with a thread is cancellation. To do this, call the cancel method.

let thread1 = ThreadprintDemon()
thread1.start()
thread1.cancel()

\ This method often causes mistakes among beginners. The fact is that it does not stop the flow, but only gives a stop command, which the developer must process himself during the flow execution.

\ Let's create a thread that will perform an infinite task and experiment with it.

class InfintyLoop: Thread {
    override func main() {
        while true {
            print("😈")
        }
    }
}

\ Let's create an instance of the thread, start it and stop it immediately.

let thread1 = InfintyLoop()
thread1.start()
thread1.cancel()

\ This is how beginners usually check the performance of the cancel method, and here's where the first pitfall awaits them. As a result of executing this code, there will be no messages in the console: the thread will stop, and we will think that the cancel method really stops the thread. But in fact, when calling cancel immediately after start, the thread will not even start executing. Let's change the code, add a 2-second pause between starting and stopping. During this time, the thread will have the time to start.

let thread1 = InfintyLoop()

thread1.start()
sleep(2)
thread1.cancel()

\

Thread execution flags

As you can see after executing this code, the demons have filled the console and show no intention of stopping their expansion. The stop method didn't work. Let's figure out why.

\ The Thread execution flags will help us with this:

isExecuting - Indicates if the thread is running.

isCancelled - Indicates if the thread has been stopped.

isFinished - Indicates if the thread has been completed.

\ It might seem like three flags are too much, as they all represent the state of the thread. However, that’s not true. The first flag, isExecuting, is the only one that reliably reflects the current state of the thread. The second, isCancelled, can be modified by the developer, so the thread's actual state may not align with this flag. The third flag, isFinished, indicates whether the thread has completed its execution.

\ For example, a thread that has not started executing has a state of isExecuting - false, but isFinished is also false, since it has not yet completed either.

\ How do we use these flags to stop a thread?

We need to track the isCancelled flag in the thread code - if it becomes true, we simply stop the task. In this example, we can replace the while loop condition by writing !isCancelled instead of true. This way, the loop will only run until it is stopped.

class InfintyLoop: Thread {
    override func main() {
        while !isCancelled {
            print("😈")
        }
    }
}

\ Now the start stop example will work as planned. This gives some flexibility: if your thread is performing an important task, you can allow it to finish gracefully rather than stopping it immediately upon receiving the command. The remaining flags are informational in nature, and there is no point in using them inside the thread. You can use them outside to see its state.

Thread priority management tool

Threads help separate task execution, offloading the main thread and reducing UI lags. However, simply adding more threads doesn’t always resolve performance issues. Even with hundreds of threads, you're still limited by the device's resources. This means that while you can delegate logic to secondary threads, they may still compete for resources, impacting the performance of the main thread.

\ This is exactly what the thread priority management tool is for. With its help, you can prioritise certain threads based on their resource demands. For instance, a resource-intensive task like applying effects to an image can be moved to a separate thread with a lower priority, allowing the main thread to handle tasks that require immediate processor resources without freezing. Meanwhile, the image processing will be carried out when resources are available.

\ Let’s create a new example: we’ll define two threads that each loop and print messages to the console. We'll increase the number of iterations to 100 to make the output more noticeable.

class ThreadprintDemon: Thread {
    override func main() {
        for _ in (0..<100) {
            print("😈")
        }
    }
}
class ThreadprintAngel: Thread {
    override func main() {
        for _ in (0..<100) {
            print("😇")
        }
    }
}

\ Let's run both threads at the same time. We'll see that demons and angels interchange. There may be more demons at the very beginning - this is because their thread was launched earlier.

let thread1 = ThreadprintDemon()
let thread2 = ThreadprintAngel()

thread1.start()
thread2.start()

😈
😈
😈
😇
😈
😇
😈
😇
😈
😇
😈
😇
😈

\ Let's change the priorities for the threads. There are 5 priorities:

UserInteractive - the highest priority. Used when the result is needed as soon as possible, preferably right now.

UserInitiated - lower priority. Used when the result is important, but a small delay is acceptable.

Utility - almost the minimum priority - for situations when the result can wait.

Background - the lowest priority, used for background tasks.

Default - the default priority, selected between userInitiated and Utility. Let's assign a low priority (Utility) to the thread with daemons, and the highest (userInteractive) to the thread with angels.

let thread1 = ThreadprintDemon()
let thread2 = ThreadprintAngel()

thread1.qualityOfService = .utility
thread2.qualityOfService = .userInteractive

thread1.start()
thread2.start()

\ The console output has changed dramatically. Now almost all angels are at the beginning, and demons are at the end. The thing is that all resources were given to the thread with angels, and the thread with demons was waiting for them to be available. This demonstrates priorities very well, but we have considered a situation where threads are very greedy, and a regular loop seeks to get all available resources.

\ Let's add a pause to the loop: in this case, the thread will need few processor resources to execute.

class ThreadprintDemon: Thread {
    override func main() {
        for _ in (0..<100) {
            print("😈")
            Thread.sleep(forTimeInterval: 1)
        }
    }
}

class ThreadprintAngel: Thread {
    override func main() {
        for _ in (0..<100) {
            print("😇")
            Thread.sleep(forTimeInterval: 1)
        }
    }
}

let thread1 = ThreadprintDemon()
let thread2 = ThreadprintAngel()

thread1.qualityOfService = .utility
thread2.qualityOfService = .userInteractive

thread1.start()
thread2.start()

\ Even though the thread priorities are still different, the console output has returned to the view where angels and demons interchange. This is because there are enough resources to run both threads at the same time.

RunLoop in Threads

By default, RunLoop is only present in the main thread, and it is not automatically available in any other threads you create. This means that asynchronous code, timers, and certain notifications (like Realm) will not work properly in secondary threads. Without a RunLoop, a thread will terminate as soon as its code finishes executing. However, if you need to run timers or prevent a thread from closing too soon, you can create and start a RunLoop in that thread. The RunLoop is initialised as soon as you try to access it, ensuring the thread remains alive and can handle events like timers or asynchronous tasks.

\ The easiest way to access the Event Loop is to try to print the current loop to the screen.

print(RunLoop.current)

\ After that, RunLoop will be created, even if it was not there. But it is in an inactive state. To start it, there is a Run method.

RunLoop.current.run()

\ The code above creates a RunLoop if it doesn't already exist and starts it. Once the event loop is running, it keeps the thread alive infinitely. However, after the loop is started, actions in the thread will only be executed if they are added to the RunLoop sources. This means that any asynchronous code or tasks need to be added to the Event Loop before it starts or from another thread. For example, if we try to set up a timer in a thread without an event loop, it won’t work because the thread won’t have an active loop to manage the timer. Let's look at a simple example that demonstrates this issue.

class TimeThread: Thread {
    override func main() {
        // setup timer
        Timer.scheduledTimer(withTimeInterval: 0.25, repeats: true) { timer in
            print("Tick")
        }
    }
}

\ Add a loop and the timer will start working.

class TimeThread: Thread {
    override func main() {
        // setup timer
        Timer.scheduledTimer(withTimeInterval: 0.25, repeats: true) { timer in
            print("Tick")
        }
        // run loop
        RunLoop.current.run()
    }
}

\ ==Important==: in the example, the timer is added before the event loop is started. After that, RunLoop will block the thread with its loop, and the code execution will not continue. This is how the timer ==will not work==:

class TimeThread: Thread {
    override func main() {
        // run loop
        RunLoop.current.run()
        // setup timer
        Timer.scheduledTimer(withTimeInterval: 0.25, repeats: true) { timer in
            print("Tick")
        }
    }
}

\ A thread started with a RunLoop will run indefinitely, as the loop will keep the thread alive. If you need to stop the thread after a certain period, you can set a time limit for the RunLoop to run.  For example, for 20 seconds:

RunLoop.current.run(until: Date() + 20)

\ The event loop will start, but after 20 seconds the thread will be stopped. You can set any date as a limit, and even run the event loop for a week. Stopping the event loop is a bit more complicated with the cancel method. Typically, you create an infinite while loop that runs the RunLoop for one second if the thread is not stopped.

while !isCancelled {
    RunLoop.current.run(until: Date() + 1)
}

\ The while loop will continue to execute until the thread receives a cancel command and its flag becomes true. Inside this loop, another loop runs for exactly one second. Afterward, it stops, and the loop checks whether there was a command to stop the thread. If not, the loop will start again.

Conclusion

In today's lesson, we explored Thread managementThread execution flags and Thread priority management tool. We covered how to create and manage threads, including how to start and cancel them. We also discussed the common pitfalls, such as misusing the cancel method, and how to handle thread flags like isExecutingisCancelled, and isFinished to manage thread states properly. We looked at how to control thread priorities to optimise resource usage and prevent UI sluggishness, along with the importance of RunLoop in managing asynchronous tasks and timers within threads.

Understanding these concepts is essential for efficient multithreading in iOS applications.

\ Next time we will discuss Grand Central Dispatch library (GCD) and simplifying thread management. Stay tuned!

Read the whole story
alvinashcraft
8 hours ago
reply
West Grove, PA
Share this story
Delete

Connection Between Web App and O365 Resources: Using SharePoint as an Example

1 Share

TOC

  • Introduction
  • [not recommended] Application permission

  • [not recommended] Delegate permission with Device Code Flow
  • Managed Identity
  • Multi-Tenant App Registration
  • References

 

Introduction

In late 2024, Microsoft Entra enforced MFA (Multi-Factor Authentication) for all user login processes. This change has caused some Web Apps using delegated permissions to fail in acquiring access tokens, thereby interrupting communication with O365 resources. This tutorial will present various alternative solutions tailored to different business requirements.

We will use a Linux Python Web App as an example in the following sections.

[not recommended] Application permission

Traditionally, using delegated permissions has the advantages of being convenient, quick, and straightforward, without being limited by whether the Web App and Target resources (e.g., SharePoint) are in the same tenant. This is because it leverages the user identity in the SharePoint tenant as the login user.

However, its drawbacks are quite evident—it is not secure. Delegated permissions are not designed for automated processes (i.e., Web Apps), and if the associated connection string (i.e., app secret) is obtained by a malicious user, it can be directly exploited.

Against this backdrop, Microsoft Entra enforced MFA for all user login processes in late 2024. Since delegated permissions rely on user-based authentication, they are also impacted. Specifically, if your automated processes originally used delegated permissions to interact with other resources, they are likely to be interrupted by errors similar to the following in recent times.

AADSTS50076: Due to a configuration change made by your administrator, or because you moved to a new location, you must use multi-factor authentication to access '00000003-0000-0000-c000-000000000000'

 

The root cause lies in the choice of permission type. While delegated permissions can technically be used for automated processes, there is a more appropriate option—application permissions, which are specifically designed for use in automated workflows.

Therefore, when facing such issues, the quickest solution is to create a set of application permissions, align their settings with your previous delegated permissions, and then update your code to use the new app ID and secret to interact with the target resource.

This method resolves the issue caused by the mandatory MFA process interruption. However, it is still not entirely secure, as the app secret, if obtained by a malicious user, can be exploited directly. Nonetheless, it serves as a temporary solution while planning for a large-scale modification or refactor of your existing system.

[not recommended] Delegate permission with Device Code Flow

Similarly, here's another temporary solution. The advantage of this approach is that you don't even need to create a new set of application permissions. Instead, you can retain the existing delegated permissions and resolve the issue by integrating Device Code Flow. Let's see how this can be achieved.

First, navigate to Microsoft Entra > App Registration > Your Application > Authentication, and enable "Allow public client flows".

 

Next, modify your code to implement the following method to acquire the token. Replace [YOUR_TENANT_ID] and [YOUR_APPLICATION_ID] with your own values.

import os, atexit, msal, sys def get_access_token_device(): cache_filename = os.path.join( os.getenv("XDG_RUNTIME_DIR", ""), "my_cache.bin" ) cache = msal.SerializableTokenCache() if os.path.exists(cache_filename): cache.deserialize(open(cache_filename, "r").read()) atexit.register(lambda: open(cache_filename, "w").write(cache.serialize()) if cache.has_state_changed else None ) config = { "authority": "https://login.microsoftonline.com/[YOUR_TENANT_ID]", "client_id": "[YOUR_APPLICATIOM_ID]", "scope": ["https://graph.microsoft.com/.default"] } app = msal.PublicClientApplication( config["client_id"], authority=config["authority"], token_cache=cache, ) result = None accounts = app.get_accounts() if accounts: print("Pick the account you want to use to proceed:") for a in accounts: print(a["username"]) chosen = accounts[0] result = app.acquire_token_silent(["User.Read"], account=chosen) if not result: flow = app.initiate_device_flow(scopes=config["scope"]) print(flow["message"]) sys.stdout.flush() result = app.acquire_token_by_device_flow(flow) if "access_token" in result: access_token = result["access_token"] return access_token else: error = result.get("error") if error == "invalid_client": print("Invalid client ID.Please check your Azure AD application configuration") else: print(error)

 

Demonstrating the Process

  1. Before acquiring the token for the first time, there is no cache file named my_cache.bin in your project directory.
  2. Start the test code, which includes obtaining the token and interacting with the corresponding service (e.g., SharePoint) using the token.
  3. Since this is the first use, the system will prompt you to manually visit https://microsoft.com/devicelogin and input the provided code. Once the manual process is complete, the system will obtain the token and execute the workflow.
  4. After acquiring the token, the cache file my_cache.bin will appear in your project directory. This file contains the access_token and refresh_token.
  5. For subsequent processes, whether triggered manually or automatically, the system will no longer prompt for manual login.

 

The cached token has a validity period of approximately one hour, which may seem insufficient. However, the acquire_token_silent function in the program will automatically use the refresh token to renew the access token and update the cache. Therefore, as long as an internal script or web job is triggered at least once every hour, the token can theoretically be used continuously.

Managed Identity

Using Managed Identity to enable interaction between an Azure Web App and other resources is currently the best solution. It ensures that no sensitive information (e.g., app secrets) is included in the code and guarantees that only the current Web App can use this authentication method. Therefore, it meets both convenience and security requirements for production environments.

Let’s take a detailed look at how to set it up.

 

Step 1: Setup Managed Identity

You will get an Object ID for further use.

 

Step 2: Enterprise Application for Managed Identity

Your Managed Identity will generate a corresponding Enterprise Application in Microsoft Entra. However, unlike App Registration, where permissions can be assigned directly via the Azure Portal, Enterprise Application permissions must be configured through commands.

 

Step 3: Log in to Azure via CloudShell

Use your account to access Azure Portal, open a CloudShell, and input the following command. This step will require you to log in with your credentials using the displayed code:

Connect-MgGraph -Scopes "Application.ReadWrite.All", "AppRoleAssignment.ReadWrite.All"

Continue by inputting the following command to target the Enterprise Application corresponding to your Managed Identity that requires permission assignment:

$PrincipalId = "<Your web app managed identity object id>" $ResourceId = (Get-MgServicePrincipal -Filter "displayName eq 'Microsoft Graph'" | Select-Object -ExpandProperty Id)

 

Step 4: Assign Permissions to the Enterprise Application

Execute the following commands to assign permissions. Key Points:

This example assigns all permissions with the prefix Sites.*. However, you can modify this to request only the necessary permissions, such as:

  • Sites.Selected
  • Sites.Read.All
  • Sites.ReadWrite.All
  • Sites.Manage.All
  • Sites.FullControl.All

If you do not wish to assign all permissions, you can change { $_.Value -like "*Sites.*" } to the specific permission you need, for example: { $_.Value -like "*Sites.Selected*" }

Each time you modify the permission, you will need to rerun all the commands below.

$AppRoles = Get-MgServicePrincipal -Filter "displayName eq 'Microsoft Graph'" -Property AppRoles | Select -ExpandProperty AppRoles | Where-Object { $_.Value -like "*Sites.*" } $AppRoles | ForEach-Object { $params = @{ "PrincipalId" = $PrincipalId "ResourceId" = $ResourceId "AppRoleId" = $_.Id } New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $PrincipalId -BodyParameter $params }

Step 5: Confirm Assigned Permissions

If, in Azure Portal, you see a screen similar to this: (Include screenshot or example text for granted permissions) This means that the necessary permissions have been successfully assigned.

 

Step 6: Retrieve a Token in Python

In your Python code, you can use the following approach to retrieve the token:

from azure.identity import ManagedIdentityCredential def get_access_token(): credential = ManagedIdentityCredential() token = credential.get_token("https://graph.microsoft.com/.default") return token.token

Important Notes:

When permissions are assigned or removed in the Enterprise Application, the ManagedIdentityCredential in your Python code caches the token for a while.

These changes will not take effect immediately. You need to restart your application and wait approximately 10 minutes for the changes to take effect.

 

Step 7: Perform Operations with the Token

Finally, you can use this token to perform the desired operations. Below is an example of creating a file in SharePoint:

You will notice that the uploader’s identity is no longer a person but instead the app itself, indicating that Managed Identity is indeed in effect and functioning properly.

While this method is effective, it is limited by the inability of Managed Identity to handle cross-tenant resource requests. I will introduce one final method to resolve this limitation.

 

Multi-Tenant App Registration

In many business scenarios, resources are distributed across different tenants. For example, SharePoint is managed by Company (Tenant) B, while the Web App is developed by Company (Tenant) A. Since these resources belong to different tenants, Managed Identity cannot be used in such cases.

 

Instead, we need to use a Multi-Tenant Application to resolve the issue.

 

The principle of this approach is to utilize an Entra ID Application created by the administrator of Tenant A (i.e., the tenant that owns the Web App) that allows cross-tenant use. This application will be pre-authorized by future user from Tenant B (i.e., the administrator of the tenant that owns SharePoint) to perform operations related to SharePoint.

 

It should be noted that the entire configuration process requires the participation of administrators from both tenants to a certain extent. Please refer to the following demonstration.

 

This is a sequential tutorial; please note that the execution order cannot be changed.

 

Step 1: Actions Required by the Administrator of the Tenant that Owns the Web App

1.1. In Microsoft Entra, create an Enterprise Application and select "Multi-Tenant." After creation, note down the Application ID.

 

1.2. In App Registration under AAD, locate the previously created application, generate an App Secret, and record it.

 

1.3. Still in App Registration, configure the necessary permissions. Choose "Application Permissions", then determine which permissions (all starting with "Sites.") are needed based on the actual operations your Web App will perform on SharePoint. For demonstration purposes, all permissions are selected here.

 

Step 2: Actions Required by the Administrator of the Tenant that Owns SharePoint

2.1. Use the PowerShell interface to log in to Azure and select the tenant where SharePoint resides.

az login --allow-no-subscriptions --use-device-code

 

2.2. Add the Multi-Tenant Application to this tenant.

az ad sp create --id <App id get from step 1.1>

 

2.3. Visit the Azure Portal, go to Enterprise Applications, locate the Multi-Tenant Application added earlier, and navigate to Permissions to grant the permissions specified in step 1.3.

 

Step 3: Actions Required by the Administrator of the Tenant that Owns the Web App

3.1. In your Python code, you can use the following method to obtain an access token:

from msal import ConfidentialClientApplication def get_access_token_cross_tenant(): tenant_id = "your-sharepoint-tenant-id" # Tenant ID where the SharePoint resides (i.e., shown in step 2.1) client_id = "your-multi-tenant-app-client-id" # App ID created in step 1.1 client_secret = "your-app-secret" # Secret created in step 1.2 authority = f"https://login.microsoftonline.com/{tenant_id}" app = ConfidentialClientApplication( client_id, authority=authority, client_credential=client_secret ) scopes = ["https://graph.microsoft.com/.default"] token_response = app.acquire_token_for_client(scopes=scopes) return token_response.get("access_token")

 

3.2. Use this token to perform the required operations.

References

Read the whole story
alvinashcraft
10 hours ago
reply
West Grove, PA
Share this story
Delete

Advent of Code 2024 Day 25: Code Chronicle in C# ✅✅

1 Share
From: Martin Zikmund
Duration: 11:45
Views: 20

It's the end of Advent of Code 2024! For the first time I was able to finish on 25th December, which makes me really happy 😀 ! Thank you all for joining me on this journey, have great holidays and an amazing year of 2025!

Source code: https://github.com/MartinZikmund/advent-of-code
Join my private leaderboard: 1510830-cced4bef at https://adventofcode.com/2024/leaderboard/private

#adventofcode #puzzle #coding #csharp #dotnet #programming

Contents:
0:00 - Intro
0:05 - Problem
1:45 - Solution
10:30 - We are done!

Read the whole story
alvinashcraft
10 hours ago
reply
West Grove, PA
Share this story
Delete

Advent of Code 2024 Day 24: Crossed Wires in C# ✅✅

1 Share
From: Martin Zikmund
Duration: 27:53
Views: 18

Definitely the hardest day of Advent of Code 2024, had to solve the second part asynchronously, but still included some explanation of the solution. Feel free to dive into it in the source code below! Also don't mind me incorrectly calling wires gates and gates connections 🤣

Source code: https://github.com/MartinZikmund/advent-of-code
Join my private leaderboard: 1510830-cced4bef at https://adventofcode.com/2024/leaderboard/private

#adventofcode #puzzle #coding #csharp #dotnet #programming

Contents:
0:00 - Intro
0:05 - Part 1
1:07 - Solving part 1
20:10 - Part 2
27:30 - Summary

Read the whole story
alvinashcraft
10 hours ago
reply
West Grove, PA
Share this story
Delete

How can I check if two GUIDs are equal when they are provided as strings?

1 Share

A customer asked if there was a helper function in the system that accepted two strings and reported whether the GUIDs they represent are equal.

This is a tricky question, because you first have to decide what “represent” means.

There are many ways of representing a GUID as a string. It could be just 32 case-insensitive hexadecimal digits. Or maybe there are internal hyphens to separate the groups. And the whole thing might be enclosed in braces or parentheses. External and interior whitespace might be allowed. Trailing garbage might be accepted (and ignored). And then there’s the special format {0x00000000,0x0000,0x0000,{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}} supported by System.Guid.Parse. That last one is tricky because it means you can’t just use a naïve algorithm of just discarding anything that is not a hexadecimal digit.

The simplest solution is to pass both strings to a GUID parser that supports the formats that you want to support, then compare the resulting GUIDs. You could write something fancy (say, ignore anything that’s not a hexadecimal digit, or the sequence 0x), but the straightforwardness of just deferring to another parser likely outweighs the risk that your custom GUID comparison function will mess something up.

If comparing GUID-holding strings is a bottleneck in your code, then the solution is not to improve your “comparing two GUID-holding strings” function, but rather to change your code so that GUIDs are passed as GUIDs.¹

¹ If the string arrives from an external source (say, JSON), then convert it to a GUID immediately upon parsing the JSON, and then use the parsed value from then on.

The post How can I check if two GUIDs are equal when they are provided as strings? appeared first on The Old New Thing.

Read the whole story
alvinashcraft
10 hours ago
reply
West Grove, PA
Share this story
Delete

Xmas Special: Running Experiments Over Managing A Tasklist, aka The Backlog | Vasco Duarte

1 Share

Xmas Special: Running Experiments Over Managing A Tasklist, aka The Backlog With Vasco Duarte

In the third episode of the “5 Wishes for 2025” series, Vasco Duarte takes aim at one of the most common anti-patterns in software development: the obsession with managing tasks instead of discovering what truly works. 

He calls on teams to shift their mindset from backlog management to running experiments, creating a culture of learning and rapid innovation.

From Backlog Secretary to Product Scientist

“Managing a backlog is like planning a road trip by focusing on the gas stops instead of the destination.”

Vasco reflects on how teams often lose sight of their goals, becoming bogged down in task management instead of pursuing real customer value. He humorously compares this approach to being a “backlog secretary,” organizing tasks while forgetting why the project began in the first place.

His solution? A radical shift from task obsession to a learning-first approach driven by rapid experiments.

The Power of 24-Hour Experiments

“Why wait for weeks to learn something you could test in a day?”

Vasco shares real-world success stories of teams embracing a rapid experimentation mindset:

The Skeptical Client: In just 48 hours, this team launched two market experiments and gained actionable feedback.

The Experiment-First Startup: Meeting twice weekly to design and run experiments, this startup learns more in a week than most teams do in a month.

These examples showcase how rapid testing leads to faster learning and greater customer impact.

The Build-Measure-Learn Framework

“Or as I like to call it, the ‘Question-Experiment-Insight’ cycle – it’s like having a GPS for product development.”

Vasco introduces a three-step approach to running experiments:

1. Start with a Concrete Goal: Define measurable business targets using a Business Value Equation.

2. Create a Metrics Tree: Break down goals into daily metrics that track progress.

3. Experiment, Experiment, Experiment: Test new features, tweaks, and ideas quickly to gain insights and adjust course.

He highlights a team’s transformation from a “feature factory” to “experiment mode,” where the Product Owner posed questions and the team creatively solved them. This cycle drives meaningful insights instead of aimless task completion.

A Wish for 2025: From Features to Insights

“A backlog full of tasks is like a restaurant full of recipes – it means nothing until you know what your customers actually want to eat!”

Vasco’s third wish for 2025 is a world where teams prioritize learning over task management. By embracing the “Question-Experiment-Insight” cycle, teams can focus on solving customer problems and creating real value.

This mindset shift transforms teams from task managers into product scientists, driving faster, smarter innovation.

See It in Action: Global Agile Summit 2025

“Want to learn how real teams are running experiments and making an impact? Join us in Tallinn!”

Vasco invites listeners to the Global Agile Summit 2025 in Tallinn, Estonia, where teams will share stories about adopting rapid experimentation. Holiday listeners can snag a special “White Wednesday” deal: a 75% discount on tickets. Visit GlobalAgileSummit.com to claim your Super Early Bird ticket.

About Vasco Duarte

Vasco Duarte is a thought leader in the Agile space, co-founder of Agile Finland, and host of the Scrum Master Toolbox Podcast, which has over 10 million downloads. Author of NoEstimates: How To Measure Project Progress Without Estimating, Vasco is a sought-after speaker and consultant helping organizations embrace Agile practices to achieve business success.

You can link with Vasco Duarte on LinkedIn.





Download audio: https://traffic.libsyn.com/secure/scrummastertoolbox/20241225_Vasco_Duarte_W.mp3?dest-id=246429
Read the whole story
alvinashcraft
22 hours ago
reply
West Grove, PA
Share this story
Delete
Next Page of Stories